(Java)Value Objectの値がnull可の場合のString型変換のより良い対処法

こんにちは！株式会社OPTiMに所属しています川田剣士です。本記事ではOPTiMでアルバイトをしていてかなり悩んだ課題について共有しようかなと思います。テーマはValue Objectの値がnullの場合のString型変換についてです。では、よろしくお願いいたします。

# 目次
[背景](#背景)
[課題1.urlがnullの時にNullPointExceptionが生じる](#課題1urlがnullの時にnullpointexceptionが生じる)
・[対処法1.String.valueOf()を利用する](#対処法1stringvalueofを利用する)
・[対処法2.Objects.toString()を利用する](#対処法2objectstostringを利用する)
・[課題1の結論](#課題1の結論)
[課題2.null時にnullが挿入されない](#課題2null時にnullが挿入されない)
・[対処法1.文字列"null"が渡された場合、ExampleUrlのvalueにnullを渡すよう実装する](#対処法1文字列nullが渡された場合exampleurlのvalueにnullを渡すよう実装する)
・[対処法2.ExampleApi.javaでString型変換せずにURIの型をそのまま利用する](#対処法2exampleapijavaでstring型変換せずにuriの型をそのまま利用する)
[他に考えることのできる実装方法](#他に考えることのできる実装方法)
[今回の件で学んだこと](#今回の件で学んだこと)
[終わりに](#終わりに)

## 背景
プレゼンテーション層でURIの型を持つ値オブジェクトExampleUrlを生成したいという課題がありました。この課題を解決するために最初にとった方法は、
1.httpリクエストによって送られたデータExampleHttpRequestから必要なデータ(ここではurl)を抽出して取り出す。
2.urlはURI型であるため、プレゼンテーション層で文字列に変換する。（urlの形式が正しいかvalidationをかけるため。検証時はURIではなく、Stringデータを渡さなければいけない。）
3.URIからStringに型変換したデータexampleUrlStringをExampleUrl()の引数に渡す。
という方法です。上記の方法をコードで表すと以下のようになります。(catchする具体的な例外や例外時の処理など、本趣旨と関係のないコードは極力省いておりますのでご了承ください。)

```Java:ExampleHttpRequest.java
import lombok.Getter;
import java.net.URI;

@Getter
class ExampleHttpRequest {
    Integer    id;
    String     name;
    Integer    age;
    URI        url;
    Integer    price;
    String     profile;
}
```
```Java:ExampleUrl.java
import org.apache.commons.validator.routines.UrlValidator;
import java.net.URI;

class ExampleUrl {
    private URI value;

    public ExampleUri() {
        this.value = null;
    }

    public ExampleUrl(String url) {
        var urlValidator = new UrlValidator();
        // urlの値が正しい形式になっているかvalidationチェック。
        // validationに引っかかった場合、エラーを返す。
        if (!urlValidator.isValid(url)) {
            throw new ~
        } else {
            this.value = URI.create(url);
        }
    }
}
```
```Java:ExampleApi.java
import ExampleUrl;
import ExampleHttpRequest;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.RestController;

@Log4j2
@RestController
public class ExampleApi {
    public ResponseEntity ExampleApi(ExampleHttpRequest exampleHttpRequest) {
        String exampleUrlString = exampleHttpRequest.getExampleUrl().toString();
        ExampleUrl exampleUrl = new ExampleUrl();
        try {
            exampleUrl = new ExampleUrl(exampleUrlString);
        } catch (Exception) {
            ~
        }
        ~
    }
}
```
と最初は実装していました。しかし、現状の実装だと複数の課題が生じたため、ここから変更していきます。

## 課題1.urlがnullの時にNullPointExceptionが生じる
ExampleApi.javaで
```
String exampleUrlString = exampleHttpRequest.getExampleUrl().toString();
```
という実装がありますが、exampleHttpRequest.getExampleUrl()がnullの時に、NullPointExceptionというエラーが発生します。この問題を解決するために以下の対処をしました。

### 対処法1.String.valueOf()を利用する
最初に試みたのは、toString()の代わりにString.valueOf()を利用するという方法です。
```Java:ExampleApi.java
import ExampleUrl;
import ExampleHttpRequest;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.RestController;

@Log4j2
@RestController
public class ExampleApi {
    public ResponseEntity ExampleApi(ExampleHttpRequest exampleHttpRequest) {
        String exampleUrlString = String.valueOf(exampleHttpRequest.getExampleUrl());
        ExampleUrl exampleUrl = new ExampleUrl();
        try {
            exampleUrl = new ExampleUrl(exampleUrlString);
        } catch (Exception) {
            ~
        }
        ~
    }
}
```
上記のように実装すると、NullPointExceptionが返却されず、成功？と思いました。しかし、
String.valueOf(null)の時、文字列"null"を返すことが判明しました。
そのため、ExampleUrl("null")となってしまい、ExampleUrlのvalueにnullを渡すことができませんでした。よって、String.valueOf()の利用はなしとなりました。

### 対処法2.Objects.toString()を利用する

Objects.toString(exampleHttpRequest.getExampleUrl(), null)の使用を検討しました。これは、exampleHttpRequest.getExampleUrl()がnull以外の値を持つ場合、文字列の型に変換します。一方、exampleHttpRequest.getExampleUrl()がnullの場合(Objects.toString(null, null)の場合)はnullをそのまま返すというメソッドだと捉えていました。(後で間違いだと気がつく。)

```Java:ExampleApi.java
import ExampleUrl;
import ExampleHttpRequest;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.RestController;

@Log4j2
@RestController
public class ExampleApi {
    public ResponseEntity ExampleApi(ExampleHttpRequest exampleHttpRequest) {
        String exampleUrlString = Objects.toString(exampleHttpRequest.getExampleUrl(), null);
        ExampleUrl exampleUrl = new ExampleUrl();
        try {
            exampleUrl = new ExampleUrl(exampleUrlString);
        } catch (Exception) {
            ~
        }
        ~
    }
}
```
これでうまくいった！と思ったのですが、Objects.toString(null, null)が文字列"null"を返すことが判明しました。元々、Objects.toString(null, null)がそのままnullを返すという情報は個人がまとめたブログで得たのですが、公式ドキュメントでは文字列"null"を返すという情報が記載されており、公式ドキュメントにアクセスすることの重要性がわかりました。

### 課題1の結論

課題1の結論として、文字列"null"になってしまう問題を解決するメソッドはないということがわかりました。

## 課題2.null時にnullが挿入されない

課題1の続きで、文字列"null"が返却されるため、ExampleUrl(null)という状態にできないという課題がありました。そこで、null時の対処法を考えました。(以下に紹介する対処法は全て問題なく要件を満たします。)

### 対処法1.文字列"null"が渡された場合、ExampleUrlのvalueにnullを渡すよう実装する

文字列"null"が渡された場合、ExampleUrlのvalueにnullを渡すよう実装することで解決しようと試みました。具体的には、
```Java:ExampleApi.java
import ExampleUrl;
import ExampleHttpRequest;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.RestController;

@Log4j2
@RestController
public class ExampleApi {
    public ResponseEntity ExampleApi(ExampleHttpRequest exampleHttpRequest) {
        String exampleUrlString = Objects.toString(exampleHttpRequest.getExampleUrl(), null);
        ExampleUrl exampleUrl = new ExampleUrl();
        try {
            exampleUrl = new ExampleUrl(exampleUrlString);
        } catch (Exception) {
            ~
        }
        ~
    }
}
```
```Java:ExampleUrl.java
import org.apache.commons.validator.routines.UrlValidator;
import java.net.URI;

class ExampleUrl {
    private URI value;

    public ExampleUri() {
        this.value = null;
    }

    public ExampleUrl(String url) {
        var urlValidator = new UrlValidator();
        if (url == null || url.equals("null")) {
            this.value = null;
        } else if (!urlValidator.isValid(url)) {
            throw new ~
        } else {
            this.value = URI.create(url);
        }
    }
}
```
と実装し、null,"null"どちらが来ても、ExampleUrlのvalueにnullを持たせることができました。しかし、コードレビューで上司から訂正していただきました。対処法2で上司が実装したコードを紹介したいと思います。

### 対処法2.ExampleApi.javaでString型変換せずにURIの型をそのまま利用する

ExampleApi.javaでString型変換せずにURIの型をそのまま利用することで解決しています。そして、ExampleUrl.javaでnullチェックを行い、String型変換も行われています。具体的には、
```Java:ExampleApi.java
import ExampleUrl;
import ExampleHttpRequest;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.RestController;

@Log4j2
@RestController
public class ExampleApi {
    public ResponseEntity ExampleApi(ExampleHttpRequest exampleHttpRequest) {
        ExampleUrl exampleUrl;
        try {
            exampleUrl = new ExampleUrl(exampleHttpRequest.getExampleUrl());
        } catch (Exception) {
            ~
        }
        ~
    }
}
```
```Java:ExampleUrl.java
import org.apache.commons.validator.routines.UrlValidator;
import java.net.URI;

class ExampleUrl {
    private URI value;

    public ExampleUri() {
        this.value = null;
    }

    public ExampleUrl(URI url) {
        if (url == null) {
            this.value = null;

            return;
        }

        var urlValidator = new UrlValidator();
        if (!urlValidator.isValid(url.toString())) {
            throw new ~
        } else {
            this.value = URI.create(url);
        }
    }
}
```
と実装し、ExampleApi.javaの実装コードを最小限にしています。また、ExampleUrl.javaでString型変換が行われているのも良いですね。対処法1の場合ですと、ExampleUrlを利用するたびにString変換などを実装しなければならず、同じようなコードが重複してしまいます。元々URIからStringに変換していた理由はurlが正しい形式かvalidationをするためです。その影響が他の層に広がってしまうこと(高結合)を防ぐために、String型変換はExampleUrl.java内で実装するべきでした。

### 他に考えることのできる実装方法
他に考えることのできる実装方法は、
・どのようなオブジェクトでもStringに変換し、かつnull時にそのままnullを返してくれるような関数を自作で作成する。
などですが、やはり対処法2と比較するとよくないと思います。ドメイン層の影響が他の層に影響しており、高結合であるという問題を解決できていないためです。やはり、対処法2が最適かなと思います。

## 今回の件で学んだこと

今回の件で学んだことは以下の内容です。
・String型変換メソッドの中で、nullが与えられたときにnullを文字列"null"ではなく、そのまま返してくれるメソッドは現状ない。
・ある層の影響を別の層に与えないようにする(低結合の実現)

## 終わりに

最後まで読んでいただき、ありがとうございます！他により良い実装方法がございましたら、気軽にコメントしていただけますと幸いです。