コードで学ぶドメイン駆動設計入門 〜エンティティとバリューオブジェクト編〜 - じゅんいち☆かとうの技術日誌
からの連投エントリ。振る舞いとサービス編です。今回もコードを使って解説したいと思います。
サービスとは、モノとして扱うと不自然なものをサービスに分類しようという考えです。
ドメインで扱う概念の中には、1つの機能や処理が単体で存在していて、もの(オブジェクト)として扱うのが不自然なものもある。そうしたものは、サービスという形でユビキタス言語に組み込む。サービスは基本的に状態をもたない(stateless)。
佐藤さんが語られている通りに、従来のサービスとDDDのサービスはレイヤーの位置関係が違います。今までのサービスはDDDのアプリケーション層です。このエントリでは、今までのサービス(DDDのアプリケーション層)の話は一切出てこないので、今までのサービスの概念を一旦忘れましょうwそういう意味では、サービスは非常に誤解されやすい言葉なので注意。*1
(PofEAAのService Layerパターンとは異なる概念なので注意。Service LayerパターンはDDDのアプリケーション層に相当するものを言っているが、DDDのサービスはドメインモデルの中にあるサービス的なものを指している。邦訳はないが、Fowler氏の記事「Evansの分類」も参考になる)
DDDにおける振る舞いとは
若干 横道に逸れますが、前回のエントリでは、エンティティやバリューオブジェクトが登場しました。ここでは、ドメイン層での振る舞いの定義について触れたいと思います。
前回のエントリでは、エンティティとバリューオブジェクトの関係性が登場しただけで、「あ、ただのデータクラスなのか」と誤解したかもしれませんが、それらのオブジェクトは属性を保持するだけなく、ドメインの問題を解決するための、振る舞いも持ちます。*2
DDD以前の考え方では、属性と振る舞いを分離する手続き型(トランザクションスクリプトと呼ばれます)の手法で設計されることが多かったのですが、DDDではドメイン層のエンティティやバリューオブジェクト自身が振る舞いを持ちます。
たとえば、先日のプレゼンで紹介したschema-generatorのActionsImplやSqlAction, EchoActionのexecuteメソッドを見れば、そのクラス自身で振る舞いを実装していることがわかります。
ActionsImpl(エンティティ)では、対象のデータソースに接続し、内包しているActionをすべて実行する振る舞いを持ちます。
public class ActionsImpl implements Actions { private final String identity; private final DataSource dataSource; private final List<Action<?>> actions; private final DataSourceConnectService dataSourceConnectService; // 中略 @Override public void execute() { Connection connection = null; try { connection = dataSourceConnectService.connect(dataSource); for (Action<?> action : actions) { ActionContext actionContext = new ActionContext(LoggerFactory.getLogger(action .getClass()), connection); action.execute(actionContext); } } finally { CloseableUtil.close(connection); } } // 以下略 }
EchoActionImpl(バリューオブジェクト)では、出力するべきメッセージをログに出力する振る舞いを持ちます。
public final class EchoActionImpl implements Action<EchoActionImpl> { private final String text; // 中略 @Override public void execute(ActionContext actionContext) { Validate.notNull(actionContext); actionContext.getLogger().info(text); } // 以下略 }
SqlActionImpl(バリューオブジェクト)では、コンテキストのデータソースに対してSQLを実行する振る舞いを持ちます。
public final class SqlActionImpl implements Action<SqlActionImpl> { private final String sql; // 中略 @Override public void execute(ActionContext actionContext) { Validate.notNull(actionContext); Statement statement = null; try { statement = actionContext.getConnection().createStatement(); if (statement.executeUpdate(sql) > 0) { actionContext.getLogger().debug("sql = " + sql); } } catch (SQLException e) { throw new SQLRuntimeException(e); } finally { CloseableUtil.close(statement); } } // 以下略 }
このコードから、エンティティ、バリューオブジェクトのそれぞれのオブジェクトは、「自分でやるべきことは自分でやる」という原則がわかると思います。
これとは反対に属性と振る舞いを分離したドメインモデルは、ドメインモデル貧血症と言われています。これはアンチパターンのひとつです。
Martin Fowler's Bliki in Japanese - ドメインモデル貧血症
属性と振る舞いが異なるオブジェクトに分散(散らばること)させて分かりにくくするのではなく、ある責務を、属性と振る舞いで表現したモデルを作っていく必要があるわけです。*3
サービスって必要?
では、エンティティやバリューオブジェクトが振る舞いを持つのに、サービスって必要なの?って疑問が浮上します。
それは、schema-generatorでのサービスを見ていきましょう。
DataSourceConnectServiceImpl(サービス)は、指定されたデータソース(DataSource)に接続しConnectionを返す役割を持ちます。具体的には以下のコードです。
public class DataSourceConnectServiceImpl implements DataSourceConnectService { @Override public Connection connect(DataSource dataSource) { Validate.notNull(dataSource); try { Class.forName(dataSource.getDriverClassName()); Connection connection = DriverManager.getConnection(dataSource.getUrl(), dataSource.getUserName(), dataSource.getPassword()); return connection; } catch (ClassNotFoundException e) { throw new ClassNotFoundRuntimeException(e); } catch (SQLException e) { throw new SQLRuntimeException(e); } } }
ActionsImplクラスのexecuteメソッド内で、上記のconnectメソッドは呼ばれています。そもそも、ActionsImplクラス内でデータソースへの接続を行ってしまえば、このサービスはいらないのでは?という見方もありますが、ここではActionsImplクラスにその責務を負わせるのは、単一責務の原則からよくないと判断しサービスに実装しています。ドメインモデルを使ったスクリプト=手続きをイメージして貰えばちょうどいいと思います。
最初に、「サービスとは、モノとして扱うと不自然なものをサービスに分類する」と触れましたが、エンティティやバリューオブジェクトというモノに分類できないような振る舞いを切り出す場所と考えるとよいでしょう。つまり、そういう振る舞いを分類する時に、例外的、消極的に使うモデルと考えてください。
逆に積極的にサービスを使ってしまうとどうなるかというと、それは前述したドメインモデル貧血症に近づいていくでしょう。その事についてはMartin Fowler氏が以下のように言及しています。
現在よくある過ちは、適切なオブジェクトに振る舞いを割り当てることを、あまりにも簡単に諦めてしまっていることです。徐々に手続き型プログラミングになっているのです。
つまり、振る舞いから適切なオブジェクトを導きだす努力は絶対に必要で、安易にサービスに頼ってしまうのはよくないということです。
次のエントリでファクトリとリポジトリを紹介します。
追記:
先のエントリでは汎用的に利用できるインターフェイスや抽象クラスを紹介しましたが、サービスではそのようなものは紹介していません。あるとしたら目的に特化したサービスのインターフェイスや抽象クラスというのはあると思います。
あわせて読みたい
コードで学ぶドメイン駆動設計入門 〜エンティティとバリューオブジェクト編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜ファクトリ編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜リポジトリ編〜 - じゅんいち☆かとうの技術日誌
コードで学ぶドメイン駆動設計入門 〜アグリゲート編〜 - じゅんいち☆かとうの技術日誌