class: center, middle
- Domaで扱える値オブジェクト
String
やInteger
などの基本型だらけになりがちなカラムのマッピングに値オブジェクトが使える- 「ドメイン」と冠しているけどDDDとは無関係
class: center, middle
部門IDとロールでユーザーを検索する。
String deptId = ...
String role = ...
List<User> users = dao.findBy(deptId, role);
引数の順番を間違えてもコンパイルエラーにならない。
String deptId = ...
String role = ...
List<User> users = dao.findBy(role, deptId);
実行時エラーにもならず気付きにくい😰
部門IDとロールを異なるドメインクラスにすると……
DepartmentId deptId = ...
UserRole role = ...
List<User> users = dao.findBy(deptId, role);
引数の順番を間違えたらコンパイルエラーになる。
DepartmentId deptId = ...
UserRole role = ...
List<User> users = dao.findBy(role, deptId);
実装中にすぐに気がつく😉
住所から市区町村を取得する。
String address = ...
String city = ...
文字列をごにょごにょしたり😧
String address = ...
String city = address.split(" ")[1];
ユーティリティを作ったり😨
String address = ...
String city = AddressUtil.extractCity(address);
でも全部String
だから……😭
String anotherCity = ...
String city = AddressUtil.extractCity(anotherCity);
住所そのものに導出メソッドを持たせられる😎
Address address = ...
City city = address.getCity();
public class Address {
public City getCity() { ... }
...
}
- 引数や戻り値に使っていればシグネチャに現れる
- Javadocや変数名を見なくてもメソッドの把握がしやすい
- デバッグ情報無しにコンパイルされて
method(String arg0, String arg)
とかになってると悲惨 - IDEの補完
- 型で候補が絞られて素早く補完できる
- 抽象度が高くて汎用的
- 具体的な型で会話する方が良い場合がある
- そういう意味で言うと、リストも?
- でもコンテナ系の型は抽象度が高い方が使いやすい気がする
- (この辺、もっとちゃんと言語化したい)
ブラウザ
⇅
Webフレームワーク
⇅
アプリケーション
⇅
DBフレームワーク
⇅
データベース
ブラウザ
⇅
Webフレームワーク👈
⇅
アプリケーション
⇅
DBフレームワーク👈
⇅
データベース
ブラウザ
⇅
Webフレームワーク
⇅
アプリケーション👈
⇅
DBフレームワーク
⇅
データベース
@Domain
で注釈してvalueType
要素に基本型を指定する
@Domain(valueType = String.class)
public class Foo {
private final String value;
public Foo(String value) {
this.value = Objects.requireNonNull(value);
}
public String getValue() {
return value;
}
}
エンティティのフィールドで。
@Entity
public class Hoge {
public Foo foo;
...
}
DAOメソッドの引数で。
@Select
List<Hoge> select(Foo foo);
DAOメソッドの戻り値で。
@Select
List<Foo> select();
基本型は使わず、全てドメインクラスを使います
class: center, middle
- ドメインクラスは型引数を取ることができる
@Domain(valueType = Long.class)
public class Key<ENTITY> {
private final Long value;
public Key(Long value) { this.value = value; }
public Long getValue() { return value; }
}
型引数はドメインクラス内ではまったく使用されないが……
@Select
Account selectById(Key<Account> id);
- このメソッドに渡せるのは
Key<Account>
だけ Key<Task>
やKey<Project>
のように異なる型引数を取るKey
を渡そうとするとコンパイルエラーとなる
@Select
Account selectById(Key id);
コンパイルエラーで検出できない
class: center, middle
- ドメインクラスはインターフェースにもできる
- その場合はコンストラクタが使えないのでstaticファクトリーメソッドを用意する
@Domain(valueType = String.class,
factoryMethod = "valueOf")
public interface Color {
String getValue();
static Color valueOf(String value) {
return new ColorImpl(value);
}
}
決まった値があるけど自由入力も許すという場合に便利
//定義済みの色を表現する
public enum DefinedColor implements Color {
RED, BLUE, GREEN;
public String getValue() { return name(); }
}
//#f90c76 のような16進数表現をする
public class ColorImpl implements Color {
private final String value;
public ColorImpl(String value) { this.value = value; }
public String getValue() { return value; }
}
static Color valueOf(String value) {
//定義済みの色があればDefinedColorを返す
//なければColorImplを返す
return Arrays.stream(DefinedColor.values())
.filter(c -> value.equals(c.getValue()))
.findFirst()
.map(Color.class::cast)
.orElseGet(() -> new ColorImpl(value));
}
class: center, middle
- 画面で入力された値をもとに前方一致検索を行う
- 業務アプリでよくある感じの仕様
曖昧な状態を許容しなくてはいけない
//前方一致の検索条件なので[email protected]ではなく
//backpapみたいな曖昧な状態を許容せざるをえない
@GET
public Response search(
@QueryParam("email") EmailAddress condition) { ... }
- ドメインクラスは制約を守って使うべき
- 曖昧な状態を許すとドメインクラスを使う場面で不安になる
型を String
などの基本型にすると型からはメールアドレスなのかそれ以外の項目なのかが分からなくなる
@GET
public Response search(
@QueryParam("email") String condition) { ... }
※型は大切にしましょう
前方一致の検索条件を表すドメインクラスを導入してみる
@Domain(valueType = String.class)
public class PartOf<T> {
private final String value;
public PartOf(String value) { this.value = value; }
public String getValue() { return value; }
}
このようなドメインクラスを作って……
こう使う
@GET
public Response search(
@QueryParam("email") PartOf<EmailAddress> condition) {
...
}
型変数にドメインクラスをバインドすることで「メールアドレスの一部」ということを型で表現
class: center, middle
ExpressionFunctions.prefix
の引数はString
型だから。
SELECT /*%expand*/*
FROM Hoge
--コンパイルエラーになる
WHERE foo LIKE /* @prefix(foo) */'x'
value
フィールドにアクセスするのは……
SELECT /*%expand*/*
FROM Hoge
--内部の露呈
WHERE foo LIKE /* @prefix(foo.value) */'x'
ExpressionFunctions
を拡張します。
public class MyExpressionFunctions
extends StandardExpressionFunctions {
//Fooを引数にとるprefixメソッドを定義する
public String prefix(Foo foo) {
if (foo != null) {
return super.prefix(foo.getValue());
}
return null;
}
}
javac -Adoma.expr.functions=MyExpressionFunctions ...
これでドメインクラスに対して@prefix
できる。
SELECT /*%expand*/*
FROM Hoge
WHERE foo LIKE /* @prefix(foo) */'x'
StringDomain
的なinterface
を導入する- あるいは、
prefix
メソッドは自動生成してしまうとか
こんなinterface
を作って、
public interface StringDomain {
String getValue();
}
ドメインクラスでimplements
して、
@Domain(valueType = String.class)
public class Foo implements StringDomain {
...
@Override
public String getValue() { ... }
}
ExpressionFunctions
ではStringDomain
に対するprefix
メソッドを定義する。
public class MyExpressionFunctions
extends StandardExpressionFunctions {
public String prefix(StringDomain s) {
if (s != null) {
return super.prefix(s.getValue());
}
return null;
}
}
class: center, middle
- 業務に出てくる名詞はとにかくドメインクラス
- 自動生成 + ジェネレーションギャップパターン
getValue
は明示的には使わないようにする(value
は内部状態)
value
は内部状態- せっかくの値オブジェクトを基本型として扱う事に
Money a = ...
Money b = ...
Money c = new Money(a.getValue() + b.getValue());
👆ではなく、👇のようにすべき。
Money a = ...
Money b = ...
Money c = a.add(b);
class: center, middle
-
型最高 --
-
値クラス最高 --
-
JDBCでシームレスに値クラスを使えるDoma最高
- Author: @backpaper0
- License: The MIT License