Scalaコードでわかった気になるDDD


(@j5ik2o)

 GREE Advent Calendar 2013 18

 Scala(DDD)ScalaDDDScalaDDDDDD(Scala)DDD(SNS)

51f7WXHJYCL._SL160_

DDD


DDDw

( RPG)()RPG使DDDRPG()

DDD(DDD使)

DDD





  





 使





 w...




DDD
DDDModel Exploration Whirlpool()使

""


使

使






(DDD)


使DDD 使

models_s


 (DDD沿scala-dddbaseEntityIdentityscala-dddbase)

3


()

(ID)
コラム:アイデンティティの再利用について
アイデンティティの話は奥が深いです。例えば、人であれば生まれた時からアイデンティティが発生し、死んだ後も故人として残り続けます。ということは、私がジョブズに成り変わることはできないのです。当たり前ですが。つまり、エンティティのライフサイクルを終えても、そのアイデンティティは再利用できないということを意味します。たとえば、再利用され得る”電話番号”を信頼できる識別子として採用した場合、私が別人になれるかもしれません!採用するしないに関わらず、このようなリスクを考慮した上で設計のトレードオフをすべきだと思います。

()

HunterHunterEntityEntityidentity便equals, hashCode(identity)

エンティティの探し方

チームメンバーとシナリオを基に(なければ、ユーザーストーリーをヒントにして)エンティティから探していきます。ホワイトボードに向かって、チームメンバーと議論して、だいたいのイメージがついたら、すぐにトレイトに落とします。最初はホワイトボードにモデル図を書いて残していたのですが、次第にコードとの同期を取るのが面倒になったので、トレイトを図の代わりにしています。また、テストでモック化するときに実装が邪魔になることがあるので、トレイトを先に書く事を優先しています。

実装は次のようなイメージになります。





使





HunternameHunterNameKatoKato

Scalacase class(case classscala-dddbase)case classequals, hashCode(firstName, lastName)

値オブジェクトを探す

エンティティが見つかったら、そこに属性を列挙していくのですが、初期のハンターは次のようなイメージかもしれません。ハンターの姓名やランクなどをそれぞれ保持している形式です。











 HunterName ()
Hunter









HunterDDD


これまでのモデルは"識別する(見分ける)"、"値を説明する"ためのモデルなので名詞的な表現でしたが、サービスは振る舞いを表すモデルなので動詞を表現するようなモデルです。また、振る舞いの名前はユビキタス言語と紐づく必要があり、入出力のための取るモデルはエンティティや値オブジェクトであるべきです。これらのオブジェクトが状態を持つため、原則的にはステートレスであるべきと言われています。

コラム : サービスとするかしないか
宙に浮いた振る舞いを、エンティティや値オブジェクトに属させるか、サービスに含めるかは、すごく難しいところです。
サービスを乱用すると、ドメインモデルから振る舞いを奪い去り ドメインモデル貧血症 に陥る可能性があります。しかし、強引に架空のモデルを作ったり、既存のモデルに押し付けたりするもの問題です。こうやれば解決という万能策はなかなかないので、私もいつも悩んでいます。どちらの設計判断をしたとしても、ユビキタス言語と関連がとれたモデリングをしてくことが現実的だと考えています。
とはいえ、人工的な手続きとしてのサービスではなく、エンティティや値オブジェクトなどのオブジェクトとして表現できないものか、とも考えます。蛇足ですが、DCIというプログラミングパラダイムは、手段の一つになるかもしれません。DCIには、ユースケースに応じた役割と振る舞いをドメインモデルに与えることで、ユーザのメンタルモデルに近づける考え方があるようです。たとえば、アイテムを渡すシーンで考えると、”アイテムを渡す”という場面(ユースケース)において、ハンター(ドメインモデル)それぞれに、”手渡す者(Sender)”や”受取る者(Receiver)”としての振る舞いを持つ役割(ロール)が与えられる、というな考え方です。ここでは詳しく説明しないので、興味がある方はDCIアーキテクチャ - Trygve Reenskaug and James O. Coplienを読んでみるとよいかもしれません。

ドメインモデルをグルーピングするモジュール


使()
entity, valueobject, service(XXXLogic, XXXBean)


net.gree.xxx.domain.entity

Hunter, Monster



net.gree.xxx.domain.valueobject

HunterName, HunterRank, Item, 



net.gree.xxx.domain.service

ItemService, 




DDDentity, valueobject, servicehuntermonster(使)
  • net.gree.xxx.domain.hunter
    • Hunter, HunterName, HunterRank
  • net.gree.xxx.domain.monster
    • Monster
  • net.gree.xxx.domain.item
    • Item, ItemService
コラム : ドメインモデルはユビキタス言語でグルーピングする
私も慣れないうちは、オブジェクトの種類によってグルーピングしていました。何が不都合かわからなかったというのが正直なところでした。でもよく考えてみると、概念的に同じグループに属するドメインモデルを考えるときに、複数のパッケージを行ったり来たりして、モデルのイメージをつなぎ合わせていることに気づきました。さらに悪いことに、パッケージ間の依存関係は密結合になってしまいました。ということで、テストコードも書きにくい状況を作ってしまいました。そもそも実装技術の文脈で表現されない、ドメインモデルを技術駆動でグルーピングすると、このような弊害が起きる可能性があります。ドメインモデルはユビキタス言語でグルーピングしましょう。

モデルの実装に対する注意点




DDD

DB



使()HunterHunterIdHunterName




Mac100
購入件名 とにかくMacがほしい件
承認限度額 1,000,000円
合計金額 650,000円
購入品目
No. 商品名 個数 金額(円)
1 MacBook Pro 15/Retina カスタマイズ (Aさん用) 1 370,000
2 MacBook Pro 13/Retina カスタマイズ (Bさん用) 1 280,000

さて、ここでチームメンバーのAさん・Bさんそれぞれが、自身のPCのScalaコンパイラを高速化させるために1台55万するMac Proに変更する更新を同時に行うとどうなるか。集約を意識せずに更新するとどうなるかみていきます。

Aさんは次のように合計が100万以内なので申請します。

No. 商品名 個数 金額(円)
1 MacBook Pro 15/Retina カスタマイズ (Aさん用) 1 550,000
2 MacBook Pro 13/Retina カスタマイズ (Bさん用) 1 280,000

同様にBさんも100万以内なので申請します。

No. 商品名 個数 金額(円)
1 MacBook Pro 15/Retina カスタマイズ (Aさん用) 1 370,000
2 Mac Pro 6コア カスタマイズ (Bさん用) 1 550,000

110
No. 商品名 個数 金額(円)
1 Mac Pro 6コア カスタマイズ (Aさん用) 1 550,000
2 Mac Pro 6コア カスタマイズ (Bさん用) 1 550,000

もうわかりますね。自分が変更していない、他の箇所への変更を無視しているため、このような不整合が起きます。購入品目単体をロックするだけでは防ぎようがありません。どうするかというと、それぞれの購入品目を購入申請の単位で一塊のオブジェクトとして扱います。データの更新は、集約単位でロック(ロックは悲観でも楽観でもよい)を獲得して行う必要があります(モデルを後述するリポジトリを通じてRDBMSに保存する際も、集約の境界がトランザクション境界と一致する必要があります)。集約は、オブジェクトを不正な状態に陥らないようにするための、重要な設計要素です。

集約を生成するためのファクトリ

オブジェクトのライフサイクル初期といえば、まずインスタンスの生成を行うフェーズです。その生成処理をモデル自身が担うと、モデルとしての表現にそぐわない場合があります。例えば、車が車自身を生成するのか?工場が車を作るのがふさわしいのではという話です(じゃぁ工場は誰が作るの?は一旦置いておいてーw)。特に複雑な生成処理の場合は、ドメインの知識を表現するドメインモデルの責務に合致しません。かといって、クライアント側に複雑な生成処理を押し付けるわけにもいきません。その場合に使うのがファクトリです。

エンティティのコード例にすでに紹介したのですが、ファクトリはコンパニオンオブジェクトを用いれば簡単に実装できます。単純なものであればこれで十分です。複雑な場合は別途ファクトリクラスとして分けることもあります。そのあたりの設計戦略を変更する基準の詳細については、「エリック・エヴァンスのドメイン駆動設計」の「第6章 ドメインオブジェクトのライフサイクル」P134を参照してください。

集約を永続化するためのリポジトリ

オブジェクトのライフサイクル中期ではすでにオブジェクトは存在していて、そのオブジェクトへの参照を辿る方法が必要となります。もっとわかりやすく言えば、データベースなどに永続化されているオブジェクトをどのように取り出すかという話題です。リポジトリはその用途に利用できます。

リポジトリには、次のような集約ルートであるエンティティ(以下 ルートエンティティ)を永続化(保存したり読み込んだり)するI/Fを定義します(Mには、同期型I/Oならscala.util.Try、非同期型I/Oならscala.concurrent.Futureを指定することができます)。それらは、ルートエンティティを”保存する(save/store)”もしくは”識別子で解決する/読み込む”などの永続化の言葉に対応します。SQLのinsert,updateより抽象度が高い言葉です。

リポジトリは、ルートエンティティ単体を永続化するだけではなく、何らかの条件に一致するルートエンティティの集合を要求するためのメソッドを提供します。次の例は、Hunterのランクでフィルターするためのメソッドです(検索結果の集合が大きい場合は、Chunkのような集合の一部分だけを返す工夫が必要かもしれません)。実装的には、ハードコードされたクエリを内部で持ち、そのためのパラメータを受け取るようなものになります。



使9 (SPECIFICATION)P226 

コラム : リポジトリはDAOなのか?
勉強会などで、”リポジトリはDAO(RDBMSの文脈としてのデータアクセスオブジェクト)と同じものですか?”と質問されることがあるのですが、その答えは私の考えでは”いいえ”です。リポジトリの実装にはRDBMSに対応した実装もありますし、他にもオンメモリであったり、NoSQL(Memcached, Redis)など様々な実装があり得ますが、そのI/Fは”保存する”、”IDで読み出す”など抽象度の高い文脈を持っています。それに対して、DAOというのは リポジトリより抽象度の低い、RDBMSに最適化されたI/F(insert, update, delete, selectなどSQLに近いI/F)を持っているはずです。その前提で考えると、DAOはリポジトリより下位のレイヤーに位置するはずです。強いて言えば、RDBMSに対応したリポジトリの実装で利用される可能性があるのがDAOではないでしょうか。
リポジトリのI/Fが固有の永続化手段に依存しないということは、永続化対象となるオブジェクトもまた依存しません。つまり永続化手段ごとにモデルを分けないということを意味します。DDDとしてはユビキタス言語で決めたドメインモデルは永続化手段ごと(MySQL用、Memcached用など)に別々ではなく、常に一つなのです(後に説明するインフラストラクチャ層では、実装都合上、永続化手段ごとにインフラストラクチャ用のオブジェクトを定義することがあります)。

一例として、ScalikeJDBCに対応したリポジトリを実装してみました(ScalikeJDBCがDAOの部分に相当します)。次のとおりです。

クライアントコードのイメージ。ちなみに、コネクションやトランザクションを表すDBSessionはEntityIOContextにラップして渡しています。



ChatWork(...)(User)(ChatRoom)(Message)ChatRoomUserMessage



ChatRoomUserI/OChatRoomUserI/OMessageRepositoryI/OChatRoomUser
MessageRepositoryChatRoomUser




使


DDD4DDD
()

レイヤー名 意味
UI層 ユーザに対しUIを使って情報を表示したりユーザの命令を解釈する層。Webアプリケーションでは、コントローラやビューなどはこの層に分類される。
アプリケーション層 豊かな表現力を持つドメインモデルを用いて問題を解決するためのレイヤ。ドメインオブジェクトを使って実行される処理の進捗管理やタスクのディスパッチなどを行う
ドメイン層 問題領域の知識を表現する層。ドメインオブジェクト同士で相互に連携し、問題を解決するための機能を実現したり、制御したりする。ただし、永続化のようにドメインと直接関係しない汎用的な技術は下位のインフラストラクチャ層に委譲する
インフラストラクチャ層 ほかのすべてのレイヤをサポートする汎用的な技術基盤を提供する層。データベースへのSQLを使った永続化処理など

レイヤーをわきまえてドメインモデルを脱線させないようにする

一般的なウェブフレームワーク(MVC)などでは、モデルにUIや永続化に関する知識が同居しがちです。DDDではそれはレイヤーが違うので、ドメインモデルからは分離する必要があります。例えば次のようにHunter#saveメソッドで、ドメインモデルであるハンターを永続化することは、DDDでは好ましくありません。saveという言葉はドメインの言葉ではなく永続化の言葉であるためです。リポジトリか、インフラストラクチャ層の責務ではないでしょうか。

また、次のように、ドメインモデルにUI層の知識が混入しないようにしましょう。UI層のビューモデルなどで行うべきです。



(´Д)RESTAPI(RDBMS便)FinagleTrinity





CoCCoCScalikeJDBCTrinity...UI



DDD使DDD

DDDDDD

DDD


 (JavaScript)

 ()

 DDD()

 ()


JANOG@OsamuKurokochi 

51f7WXHJYCL._SL160_