ドメイン駆動設計 値オブジェクトの設計(Java)
はじめに
この記事では、ドメイン駆動設計における値オブジェクトをどのように設計すれば保守性が高くなるかを考える。主にvalidationについて記載している。
前提
使う言語とフレームワークは以下
- java
- springboot
値オブジェクトについて
値オブジェクトはドメインモデルを表現するための最小単位のオブジェクトである。文よりコードを見た方が早いので記載する。
//domain.user.User
public class User {
private Id id;
private Name name;
private int role;
}
//domain.user.field.Id
public class Id {
private final int value;
}
//domain.user.field.Name
public class Name {
private final String value;
}
上記のIdやNameが最小単位の値オブジェクトである。Userはドメインオブジェクトである(ここではドメインオブジェクトをUserのようなフィールド変数に値オブジェクトを入れたものをドメインオブジェクトと呼ぶことにする)。上記の特徴はロジックを1つにまとめられるところにある。例えば、Name
にDBで設定している文字数の長さについて制限を入れることができる。
例は以下である。
public class Name {
private final String value;
public Name(String value) {
if(value < 1 || value > 30) {
throw new IllegalArgumentException("文字がおかしいですよ");
}
}
}
とまぁ、上記のような形でオブジェクト作成した段階でvalidationができるわけである。
さて、基本的なところを見たところで上記の良い点と悪い点について個人的に思うことを記載する
良い点
- validationがすぐにかかるので、保守性が高くなる
- DBについて変更があった場合修正箇所が1つになるので修正がしやすくなり保守性が高くなる
悪い点
- 書くのが面倒で、全部書かなくてもいいという制約をつけてもいいが基準を明確にするのが難しく運用が難しい
- リスト取得時に全てのオブジェクトにvalidationがかかるので、リスト数がNであるとしドメインオブジェクトのフィールド数が10であると O(N10validation数)みたく計算数が増える。
以上の観点で保守性が高くなる一方で運用面での懸念がある。そこでこの記事では、悪い点をなんとか解消できるような提案を考えつつ良い点をさらに磨くようにすればどうしたら良いかを考えたいと思う。
関連
ドメイン駆動設計についてはいくつかの参考書と記事がある。ここでそれらを紹介しておく
以上、上記のものを見ると大方学ぶことができる。困った時の要因として以下も一読しても良いと思う
とはいえ最初に勧めた3つを読めば大方基礎はわかると思うので、あとはコードを書いて悩むだけで良いかと思う。
アーキテクチャ設計
validationについての集約
ここでは良い点における、validationの集約について考える。validationをしたいときは主に、ユーザから受け取った時のデータが正しいかどうかについて検査したいときに行うのが最も多いケースであると思われる。
そこで、その例を基に値オブジェクトの活用について記載する。
まずはコードを記載する。
//presentation.v1.auth.signinController
@RequestMapping(value = "/v1/auth/signin", method = RequestMethod.POST)
public SignInResponse signin(@Valid @RequestBody SignInResource signInResource) {
...
}
//presentation.v1.auth.signin.models.SignInResource
@AllArgsConstructor
@Getter
public class SignInResource {
@Valid
@NotNull
@JsonProperty("user_name")
private final Name name;
@Valid
@NotNull
@JsonProperty("password")
private final Password password;
}
//domain.users.fields.Name
@AllArgsConstructor
@Getter
public class Name {
@NotEmpty
@Size(min = 1, max = 30)
private final String value;
}
//domain.users.fields.Password
@AllArgsConstructor
@Getter
public class Password {
@NotEmpty
@Size(min = 1, max = 512)
private final String password;
}
上記のように値オブジェクトを使い、validation自体はName
やPassword
に記載する。validationはjavax.validation
のものを使うことで表現することができる。こうすることで値オブジェクトにvalidationを書くことができ使いまわすことができるようになる。
注意なのが、Resource
の置き場所と値オブジェクトのパッケージの置き場所である。
値オブジェクトはドメインオブジェクトのフィールドなのでdomain.user
のパッケージに置くものとする。一方で、Resource
は最初の受け口となるのでパッケージはcontroller
であることが望ましい。しかし、この考え方はレイヤードアーキテクチャやオニオンアーキテクチャなどに違反する。これらの考え方はcontrollerがドメインのオブジェクトに依存しない形なので、違反する。そのためこのコードは例外的な書き方であることは認識しておく必要がある。
ただ、これらの効力は大きく、値オブジェクトとして活用することで色々な場所ででてきたとしても値オブジェクトを使いまわすことで余計なvalidationの表現を記載する必要がない。
ただしこの書き方は1つ欠点がある。それは以下である
//正常
Password password1 = new Password("hoge")
//本来NotEmptyをつけているので文字がないものはエラーとなってほしい書き方
Password password2 = new Password("")
上記のどちらも実は通ってしまう。では、コンストラクタを書けばいいかといえばそうではなく、javax
とコンストラクタの2重の表現記載する必要があり、面倒である。加えて、エラーになったときにコンストラクタでのエラー表現になってしまうので複数のフィールドでvalidationエラーになったとしても1つしかエラーresponse内容を返せないことになる。
とはいえnew Password
した値オブジェクト単体だけを使うことは稀であり値オブジェクトをつなぎ合わせたUser
のようなドメインオブジェクトと一緒に使うことが多い。さてではドメインオブジェクトを作ったときのvalidationのかけ方について考えてみる。
ドメインオブジェクトにおける値オブジェクト
public class User {
private Password password;
private Name name;
private int role;
public User(Password password, Name name int role ) {
this.password = password;
this.name = name;
this.role = role;
}
}
上記のようにただクラスのドメインオブジェクトを作っただけでは、値オブジェクトのvalidationはかからない。javax
はvalidatorを手動でやらないとvalidationしないためである。
では以下のように考えるのはどうだろうか?
public class User {
@Valid
@NotNull
private Password password;
@Valid
@NotNull
private Name name;
@Valid
@NotNull
private int role;
public User(Password password, Name name int role) {
this.password = password;
this.name = name;
this.role = role;
validate(this);
}
}
//domain.utils.validate
public static <T> void validate(T resoure){
ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
Validator validator = validatorFactory.getValidator();
Set<ConstraintViolation<T>> constraintValidators = validator.validate(resoure);
if (constraintValidators.size() >= 1) {
throw new IllegalArgumentException("バリデーションエラー");
}
}
上記のような感じで記載することで、new User(...)
したときにvalidationが効くようになる。
この方法は値オブジェクトでやりたいような、値オブジェクトだけにvalidation内容を記載し使いまわすことで保守性の高いコードを作る、ことができる。
一方で、計算量の観点やインスタンス変数の数が多くなった時の運用や保守について考える。すると、計算量は最初にあげた悪い点でNのリストがきたときにそれぞれのフィールドに対してvalidationを全て見るので計算量的には少し多くなることがある。これについては、DBからデータを取り出しこのドメインオブジェクトに入れようとするとそういう現象が起きる可能性がある。Mybatisを使うとそうなる。ただしこの問題はSQLを叩くときに、limitやwhereで情報を取り出す、という制約をつければ解消する。とはいえ、意図しない形でデータを多く取得することも否定できないのでチームとしての運用は意識しないといけないポイントになる。
一方で、フィールドの変数が多いと単純にnew User()
をしたときに順番を意識してコードを書くことにもなり、全てが値オブジェクトであれば問題ないが、そうでない場合は誤った場所に変数を書いてしまい気づかない、みたいなことが起きてしまう場合がある。
以上、の問題を扱うためにbuilder
を使うことを考える。これを使い、インスタンス変数を使ってのデータ挿入とbuilderを使っての2つの口を用意しておく。
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder(builderClassName = "Builder", buildMethodName = "build")
public class User {
@Valid
@NotNull
private Password password;
@Valid
@NotNull
private Name name;
@Valid
@NotNull
private int role;
public static class Builder {
public User build() {
User user = new User(password, name, role);
validate(user);
return user;
}
}
}
上記のようにする。こうすることでMybatisから取得するようなときはコンストラクタからデータが挿入されるのでvalidationは働かず(前提にDBに入っているのでvalidationの条件は守られている)、計算量の観点もきにする必要がない。またDBに入れたいときはbuilder
を使うという運用ルールを決めておくことで、きちんとvalidationを行うことができ安全性が保つことができる。
値オブジェクトを作るかについて
1つの指針として考えられるのはRequestBody
のvalidationの存在である。これが複雑になるのであれば、値オブジェクト作って使いまわす方が無難である。複雑である場合はvalidation内容が変更される場合があるためである。条件を緩くして運用を楽にしよう、みたいな考えは想像できる。
一方で、単純にNotNullだけであれば、値オブジェクトを作る必要はないと思う。また、単純なboolean値やDate型のようなものは値オブジェクトを使って表現する必要性は感じない。Date型について、DBに入れるために何かの変換がしたいときは、ドメインオブジェクトに責務を任せても問題ないためである。先ほどのbuilder表現では個々のフィールドに対して行いたい変換も書くことができるため、値オブジェクトである必要性はない。
以上のように、作られた値オブジェクトがあるのであれば、ドメインオブジェクトで使えばいいぐらいで、必ずしも値オブジェクトを使えばいいという問題ではない。保守性や運用面での総合的な判断をしなければならない。validationのやり方やドメインオブジェクトのインスタンスの作り方についてのルールを決めたら、値オブジェクトを作る定義を考えるより、「疎結合性や凝集性」について悩んだ方がコードのスッキリ性は上がるのでこちらを次に手を出す方が良いと思う。
まとめ
この記事では、値オブジェクトとドメインオブジェクトの関わり方について記載し、その上でvaliationのやり方について提案した。これが良い、というエビデンスは対して示せていないが、一度試す価値はあるかなと思っている。
いづれは「パッケージ設計」と「API設計とController」と「外部APIとrepository」の関係も書きたいなと思っている。
Discussion