Doma2 Domainのすゝめ
実務でDoma2を利用させていただいており、積極的にDomainクラスを導入しています。
この記事では、Domainクラスの使い方や使いたい理由についてご紹介します。
この記事を書くにあたり、公式のgetting-startedをDomainクラスに置き換えてみました。
ぜひ見てみてくださいね。
Doma2とは
Java,Kotlinで利用可能なDBアクセスフレームワークです。
詳しい説明は公式ドキュメントに譲らせてください :bow:
Domainクラスという素晴らしい機能
そんなDoma2には、Domainクラスという機能があります。
一言で言うと プリミティブ型やそのラッパークラスを、独自の型でラップできる機能 です。
作ったDomainクラスは、Entityクラスのフィールドとして利用することができます。
Domainクラスの作り方
ユーザを表すテーブルusers
の中に、人名を表すカラムuser_name
があったとしましょう。
user_name
をDomainクラスとして表現すると↓のようになります。
// @Domainアノテーションを付与します。
// valueType属性には、Domainクラスが保持する値の型(この場合はString)を指定します
@Domain(valueType = String.class, factoryMethod = "of")
public class UserName {
private final String value;
// factoryMethodを定義すると、コンストラクタをprivateにできます(publicでも動きます)
private UserName(String value) {
this.value = value;
}
String getValue() {
return value;
}
public static UserName of(String value) {
return new UserName(value);
}
// equals と hashCode も実装しましょう
}
そしてusers
テーブルに対応するエンティティは↓のようになります。
@Entity(immutable = true)
@Table(name = "users")
public class User {
private final UserName userName;
// Domainクラスを使わなかったらこうなります
// private final String userName;
public User(UserName userName) {
this.userName = userName;
}
public UserName getUserName() {
return userName;
}
}
Domainクラスを使いたい理由
いよいよ本題です!
実際にDomainクラスを導入して良かったなーと思う点を紹介します。
引数に型が付く
エンティティのコンストラクタはもちろん、業務ロジックのコードにも型が付くようになります。
たとえばこんな感じです。
// 記事のURLをメールで送信する
public void sendMail(UserEmailAddress mailAddress, ArticleUrl articleUrl) {
...
}
// Domainクラスを使わなかったらこんな感じ
public void sendMail(String mailAddress, String articleUrl) {
...
}
こうすることで「引数を逆にしちゃった」といった間違いがなくなります。
引数が多くなるほど順番に気を使いますし、コードレビューも大変ですよね。
ジェネリクスが分かりやすくなる
ジェネリクスがDomainクラスになることで、ListやMapの中身を推測できるようになります。
また、CollectorやFunctionインターフェースも厳密に定義できるようになります。
実装を見ないとListやMapの中身が分からないのはツライですよね。
collect検索がもっと便利になるのも嬉しいポイントです。(コード例はこちら)
final Map<EmployeeId, Name> employeeNames
= dao.selectAll(toMap(employee -> employee.id, employee -> employee.name));
// コンパイルエラーになる
final Map<Age, Name> employeeNames
= dao.selectAll(toMap(employee -> employee.id, employee -> employee.name));
// Domainクラスを使わなかったらこんな感じ
final Map<Integer, String> employeeNames
= dao.selectAll(toMap(employee -> employee.id, employee -> employee.name));
// Employee#idをキーにしたいが、コンパイルエラーにならない
final Map<Integer, String> employeeNames
= dao.selectAll(toMap(employee -> employee.age, employee -> employee.name));
Domainクラスにロジックが書ける
Domainクラスは「ただのクラス」なのでロジックを書けますし、単体テストも書くことができます。
文字列の変換処理やバリデーションなどがあちこちに書かれている場合は、Dmainクラスや独自型にロジックを移すと良いかもしれません。
ロジック=メソッドなので、名前がつけられるのも嬉しいポイントですね。
すべてに型をつけたくなる
Domainクラスを導入してから、プリミティブやそのラッパークラスが登場することに違和感が出てきました。
例えば、ユーザの「誕生日」から「年齢」を計算する処理があったとしましょう。
「誕生日」のカラムは存在しますが、「年齢」のカラムは存在しないものとします。
この処理を実装する前に「年齢」型を実装し、戻り値を「年齢」型にします。
ただの数字や文字が「現実世界の何に対応するか」を表現するように心がけています。
// 「年齢」を表す型
class Age {
private final int value;
public static Age of(int value) {
this.value = value;
}
}
@Entity(immutable = true)
public class User {
private final BirthDate birthDate;
// コンストラクタ
// 年齢を取得する
public Age getAge() {
return birthDate.calcAge(); // 年齢を計算するロジックはDomainクラスに
}
}
少しずつ導入できる
Domainクラスの導入は1カラムから始めることができます。
まずはDomainクラスを1つ実装しましょう。
既存のエンティティのフィールドを置き換えてもいいですし、新規で作るエンティティにDomainクラスを使うようにしても良いです。
影響の少なそうなカラムから始めるのが良いかもしれませんね。
.var
補完がより便利に
IntelliJ IDEAのこれは意外な副産物でした。
IntelliJ IDEAのPostfix completionという(これまた)素晴らしい機能があります。
Domainクラスや独自型で.var
を使うと、変数名を型の名前にしてくれます。
タイプ数が減ってくれるのは嬉しいですね!
型名が変数名の候補に!
プリミティブだとこうはならない
まとめ
Doma2のDomainクラスについてご紹介しました。
見返してみると「独自型を使いたい理由」が多かったような気がしますが、それをEntityに簡単に持ち込めるのがDomainクラスの良いところだと思います。
どんどん型を作っていきましょう!
Discussion