値オブジェクトの悩みポイント:どこまで型を用意するか / nullableな値をどう取り扱うか

ライト版はこちら

speakerdeck.com

早速ですがドメイン駆動設計に関係するいくつかの書籍でも紹介されている、「人名」を考えます。

「名前」と「名字」は確実に存在し、「ミドルネーム」無い場合もあるという想定です。

public class PersonName {
    private <Type> firstName;
    private <Type> lastName;
    private <Type> middleName;

   ...

   @Override 
   public String toString() {
      return String.format("%s %s %s", 
        firstName.toString(), 
        middleName.toString(), 
        lastName.toString());
   }

}

このときそれぞれの型はどのようにするのが良いでしょうか。

Stringを採用

public class PersonName {
    private String firstName;
    private String lastName;
    private String middleName;
   ...
}

最も簡単な方法です。

「人名」という値オブジェクトを用意し、「名前」や「名字」はその構成要素でしかないという考え方です。

「名前」や「名字」単体でのでの利用がない場合はこのような設計がシンプルで良いかもしれないです。

メリット

  • 型が増えなくて済む
  • 取り回しが効く
  • 「人名」の生成時に渡す引数が簡単

デメリット

  • 「名前」、「名字」、「ミドルネーム」それぞれの個別の振る舞いは表現できない。
  • 「人名」の生成時に「名前」と「名字」が存在すること、「ミドルネーム」がnullではないことを担保する必要がある。
  • 型で「ミドルネーム」が空の可能性があることを表現できていない。

String + NonEmptyStringを採用

public class PersonName {
    private NonEmptyString firstName;
    private NonEmptyString lastName;
    private String middleName;
    ...
}

Stringのみと同様、「人名」という値オブジェクトを用意し「名前」と「名字」についてはその構成要素でしかないという考え方です。

値が空に可能性を明確に排除しているNonEmptyStringという型を用意することで通常のStringで値が空の可能性があることを示唆しています。

メリット

  • String同様型が増えなくて済む(NonEmptyString1個だけ)
  • 取り回しが効く

デメリット

  • 「名前」、「名字」、「ミドルネーム」それぞれの個別の振る舞いは表現できない。
  • 「ミドルネーム」がnullではないことを担保する必要がある。
  • 「人名」生成時に渡す引数がStringのみと比べて複雑になる。

FirstName / LastName / MiddleNameを採用

public class FirstName {
    private static final int MIN_LENGTH = 1;
    private static final int MAX_LENGTH = 50;

    private String value;

    // コンストラクタや生成メソッドで検証するか利用側で検証してから渡すかはまた別の悩みポイント…
    ...

    @Override 
    public String toString() {
        return value;
   }
}
public class MiddleName {
    private static final int MAX_LENGTH = 100;

    private String value;

    // コンストラクタや生成メソッドで検証するか利用側で検証してから渡すかはまた別の悩みポイント…
    ...

    public boolean isSpecified() {
        // valueが空文字での生成は許してもnullでの生成は許さない必要あり
        return !value.isEmpty();
    }

    @Override 
    public String toString() {
        return value;
   }
}
public class PersonName {
    private FirstName firstName;
    private LastName lastName;
    private MiddleName middleName;
    ...
}

「名前」、「名字」、「ミドルネーム」を表す値オブジェクトをそれぞれ用意します。

メリット

  • 言葉と1対1で対応する
  • 個別の細かな性質や振る舞いもコードで表現することができる。
  • 型が言葉そのもので表されているので意図が明確。

デメリット

  • 型がユビキタス言語の数だけ増え、取り回しが効かない。
  • 似たような内容の型が増える

どれを採用するのが良さそうか

その業務でどう取り扱いかによると言ってしまえばそれまでなのですが、私の場合は「名字」「名前」でそれぞれ個別の性質や振る舞いがあるか否かを考えて、 「命名された文字列を表す」より更にならではの振る舞いがありそうな場合は、FirstName / LastName / MiddleNameを採用し、そうでない場合はただの構成要素なのでStringを採用します。

今回のケースでは「ミドルネーム」という概念が登場した時点で「指定がないことがある」というならではの振る舞いがあることは想像できるため、Stringのみのパターンの採用は見送ります。

String + NonEmptyStringとMiddleNameのどちらを採用するのかは悩みポイントですが、「指定がないこともある」という性質も踏まえて「ミドルネーム」という概念だと考え、MiddleName型を採用するのが一番素直に思えます。

「名字」「名前」についてはString、MiddleNameだけを値オブジェクトとして表現するハイブリッドなパターンも考えられますが、振る舞いを表現する場所が構成要素毎にまちまちなのが気になるため採用を見送ります。

Option VS. 値オブジェクト単体

JavaのOptionalなどでnullである可能性を表現できる型を用いるか、値オブジェクト単体で空である可能性も表現するかも悩みポイントです。

今回のケースでは「指定がないこともある」という性質自体が「ミドルネーム」という概念であるという考え方で、私の場合はMiddleNameを採用しOptionalなどの形は見送ります。

また、指定されていないことの表現は意図していないミスを防ぐため極力nullを避け、文字列ならば空文字で表現したい気持ちがあり値オブジェクト単体での表現を採用します。

終わり

今回検討した内容は正解がなくケースバイケースで様々な答えが考えられると思います。

様々な意見がある中で、どうしてそのようにしたかという考え方についてフォーカスして色々な方の話を伺いたいです。