ITと哲学と

IT系エンジニアによる技術と哲学のお話。

エリックエヴァンスのドメイン駆動設計を読んでいる(第1章 第1部)

エリックエヴァンスドメイン駆動設計を読んでいます。 第1章第1部のまとめと感想をつらつらと書きます。

エリック・エヴァンスのドメイン駆動設計

エリック・エヴァンスのドメイン駆動設計

モデルとは

現実の何らか側面や興味の対象となる概念を表す。それ以外の詳細は無視する現実への解釈。 適切なモデルは情報の持つ意味を明らかにし、その情報を問題に集中させる ドメインエキスパートの頭の中にある知識を厳密に構成し選び抜いて抽象化したものがモデルである。

ドメイン駆動設計におけるモデルの有用性

ドメイン駆動設計におけるモデルの3つの基本的用法

  • モデルと設計の確信が相互に形成し合う
  • モデルはチームメンバ全員が使用する言語の基盤である
  • モデルとは蒸留された知識である

モデルと実装を密接に結びつけることでモデルとドメインが深く関連したものになる。これによりモデルを理解することでプログラムを理解し、モデルを改良することでプログラムの改良が行われる。 また、チームメンバーが統一された言語でコミュニケーションすることができるようになる。

例えば「アカウント」という言葉にはいろいろなイメージや捉え方ができると思うが、チーム内でモデルを共有していればそれらのイメージや捉え方が統一できるためコミュニケーションロスが減るという意味だと理解してます。そのためDDDをやっているチームのメンバーが全員これを理解し尊重していないといけないと思う。

知識をかみ砕く

開発者とドメインエキスパートの会話が書かれている。 この会話の中で開発者がドメインに関する知識を得るのと同時に、ドメインエキスパートは知識を棚卸し、そして整理して厳密に矛盾のないものへと成長させていく。 ドメインに関する知識をある程度成長させたところで、UIや永続化を行わないレベルのプロトタイプの作成を行った。実際にモデルを実現し挙動をドメインエキスパートに見せることで、モデルの重要性を認識させたところ、より積極的にモデルを成長させることに支援をするようになっていった。 モデルを成長させるという行為の結果がどのように現れるかをドメインエキスパートに理解させたことでより協力的になったということか。

効果的なモデリングの要素

  • モデルと実装を結びつける
  • モデルに基づいて言語を洗練させる: エンジニアとドメインエキスパートの間で統一した言語で会話ができるようになったのでコミュニケーションが容易になった
  • 知識豊富なモデルを開発する:オブジェクトには振る舞いと守るべきルールがある。モデルは単なるデータスキーマではない。
  • モデルを蒸留する:必要な知識の積み重ねと、それと同じくらい大切な不要な知識の切り分けが必要
  • ブレインストーミングと実験を行う

知識のかみ砕き

開発者はドメインエキスパートとともに知識をかみ砕き、モデルの蒸留を絶えず行っていく。ドメインエキスパートの中にある知識はもとより、関連するレガシーシステムや既存システムのユーザーなどから情報を引き出したり、初期バージョンやプロトタイプからも情報を引き出して反復的にモデルを蒸留させていく。 従来のWFではアナリストがドメインエキスパートから情報を引き出してモデリングしていたのに対して、ドメイン駆動設計ではプロトタイプや初期バージョンからも情報を引き出すという点でイテレーティブな開発手法の採用が必要になるという理解をしています。 イテレーティブな開発手法であっても、開発者がモデルに興味を払わない場合もあり、これではドメインエキスパートが持っている価値ある知識をプロダクトに組み込むことはできない。もったいない。 そのため、チームのメンバーは、ドメインを中心に据えて議論を進めていくべきだ。これによりドメインエキスパートの知識が詰まったモデルができ、それを反映したプロダクトができるということです。

継続的学習

ソフトウェアを書き始める時、我々は対象を十分に理解しているわけではない。何が大切なことで、何がいらないことなのかを把握できていない状態で開発に向かわなくてはいけない。

知識豊富な設計

貨物輸送の予約システムを考える。 各航海ごとに複数の貨物を運ぶように予約することができる。

class Cargo
class Voyage {
  val cargos:Seq[Cargo] = Seq()
  def addCargo(cargo:Cargo) =??? // cargosのリストに追加する

    def makeBooking(cargo: Cargo, voyage: Voyage) ={
    voyage.addCargo(cargo)
  }
}

これに対して例えば航海において積載量が決まっており、貨物の積み込みに関して制限があるとする。また慣例的に貨物の1.1倍までの積載は許容されるので、制約は「積載量の10%まで積荷の超過を認める」となる。 そのままコードに落とすとこうなる。

case class Cargo(size: Double)
class Voyage {
  val capacity:Double
  val cargos:Seq[Cargo] = Seq()
  def addCargo(cargo:Cargo) = ??? // cargosのリストに追加する
  def bookedCargoSize:Double = ??? //cargosのsizeを全て足し合わせる

  def makeBooking(cargo: Cargo, voyage: Voyage) ={
    val maxBooking = voyage.capacity * 1.1
    if((voyage.bookedCargoSize + cargo.size) <= maxBooking) 
      voyage.addCargo(cargo)

  }
}

ここでは重要なビジネスルール(「積載量の10%まで積荷の超過を認める」)がアプリケーションメソッドの中に隠されてしまっている。 今回の例はまだ単純でわかりやすいが、ここのロジックが複雑になると見通しが悪くなり、良くない。

レイヤー化アーキテクチャに基づいてドメインオブジェクトへこの責務を移すのは第4章で取り上げるそうだが、ここでは この知識を明示的にし、プロジェクトに携わる全ての人が使えるようにすることを目指してリファクタリングする。

前述のルールはポリシーとも呼ばれ、これはストラテジーパターンを当てはめることができる。 このパターンは様々なルールを置き換えたりする際に使われるものであり、今回の要件には当てはまらないが、ここで必要なことはまさにポリシーの意味に適合している。ならばデザインパターンをモデルに紐付けることはコードから意味を理解する上でも重要なことである。

case class Cargo(size:Double)

class Voyage {
  val capacity:Double = 0.0
  val cargos:Seq[Cargo] = Seq()
  def addCargo(cargo:Cargo) = ??? // cargosのリストに追加する
  def bookedCargoSize:Double = ??? //cargosのsizeを全て足し合わせる

  def makeBooking(cargo: Cargo, voyage: Voyage) ={
    val maxBooking = voyage.capacity * 1.1
    if(OverbookingPolicy.isAllowed(cargo, voyage))
      voyage.addCargo(cargo)
  }
}

object OverbookingPolicy{
  def isAllowed(cargo:Cargo, voyage: Voyage)={
    val maxBooking = voyage.capacity * 1.1
    (voyage.bookedCargoSize + cargo.size)<= maxBooking
  }
} 

ここまで設計を進めることでプロジェクトメンバーは積荷の量の計算をビジネス上必要な要件であることが理解できるし、プログラマはビジネスエキスパートにドメインの振る舞いを見せ、フィードバックを得ることができるようになる。

読んで感じたこと

ドメインを中心に会話をすることが大切で、ドメインと実装をしっかりと紐付けることが大切だとチームの中で何度も言われているが、こういう背景があったということが理解できたことが大きいと感じる。

それに、モデルを中心に据えてコミュニケーションが簡単にできるというのは確かにプロジェクトの中で感じていた。 自分がプロジェクトに参画したての頃に、モデルを見ながらの議論には割とすんなりと入れたので。