本記事では、とあるライブラリにドメイン駆動を取り入れた際の反省点について書きます。
変更点
- 2023/7/7:ライブラリに適用するという前提はこの問題において重要そうですので、タイトルにライブラリへの適用を明示しました。
注意点
- 仕事で書いていたプログラムのため、実際とは異なる対象を想定した例を使用して書きます。
- ソースコードの例は C++ で書きます。(C++ の文法が分からなくても意味が分かる程度になっているとは思います。)
例示用の問題
ここでは、実際に仕事で取り組んだのとは異なる次のような問題を例に挙げて書きます。
- 次のデータを使用してログイン処理を行いたい。
- ユーザ名
- パスワード
事象
上記のような問題に対して、ドメイン駆動をもとに以下のようなクラスを作ることにしました。
- 認証を行うサービスのクラス(AuthenticationService)
- 認証に必要なデータのクラス(AuthenticationRequest)
- ユーザ名のクラス(UserName)
- パスワードのクラス(Password)
- 認証結果のクラス(AuthenticationResult)
- 認証成功時に使用するセッションのクラス(Session)
ユーザ名とパスワードは文字列ですが、バリデーションの処理を後から追加できるようにクラス化することになりました。
その結果、ログインをするために次のようなソースコードを書くことになりました。
Session authenticate(const std::string& name, const std::string& password) {
AuthenticationRequest request = AuthenticationRequest(UserName(name), Password(password));
AuthenticationService service = AuthenticationService();
AuthenticationResult result = service.login(request);
return result.session();
}
これの問題点は、ただログインを行うためだけに 6 つのクラスの API を確認しなければならないということです。開発していたのはライブラリのため、開発に関わっていない一般ユーザに上のソースコードを書かせる必要があります。(たとえ、このライブラリの入門者であったとしても。)
なお、実際には上記のような調子で何十個もクラスを書いたため、実装した私本人でも実装を確認しながらでなければ使えないという酷い状態になりました。
なお、「バリデーションの処理を後から追加できるように」と上で書きましたが、バリデーションの処理が入ることはないまま納品となりました。UserName, Password クラスは、ただ内部に C++ 標準の文字列型(std::string)を持つだけのクラスになりました。
反省
ドメイン駆動を取り入れるにあたって、名前の付く値を全てクラスにすると、クラスが増えすぎて管理できなくなります。本当にクラスでないといけないか考え、言語の標準の型で問題のない値をクラス化しないようにしましょう。例えば、上の例は、以下のように済ませることもできたはずです。
Session authenticate(const std::string& name, const std::string& password) {
AuthenticationService service = AuthenticationService();
return service.login(name, password);
}
クラス化しなかった値を後からクラス化する必要が出てきた場合、API の変更により色々な箇所のソースコードを修正する必要があります。しかし、大量のクラスによる分かりにくい API を使用したソースコードを何度も書いたり、お客様も書けるように説明したりするのに比べれば、楽だったのではないかと思っています。
まとめ
ドメイン駆動は多少クラスが増えやすい部分があります。クラスが増えすぎて使いずらいという状態にならないように、気を付けて設計を進めましょう。