DDDとは

  • ドメイン駆動設計
  • ドメインとはなにか
    • 金を稼ぐもの
  • ドメインに駆動して設計するということはつまり「自分たちのビジネスを中心に考えてソフトウェアの設計をしましょう」

DDDをやる理由

  • 自分たちが儲けるため(課題を解決するため)のソフトウェアをつくるため
  • ソフトウェアの制約のせいで儲けを追求できない->ダメ

ドメイン層(Entity)

  • ビジネス上の戦略や意思決定を反映している

エンティティとは

  • そのIdentityによって区別されるもの
    • Identityをどう表現するかは実装次第
      • 現実ではIDのような一意になるキーを振る
  • 可変である
    • 中身が変わる可能性がある

値オブジェクト

  • エンティティじゃないもの
    • 不変である
    • Identityによって区別しない
  • その値によって価値が決まる

実装寄りの話

  • ドメインモデル貧血症
    • ロジックがドメインに集まってない
  • ドメインロジックが集まっている
 class Human {
  id: string
  height: int
  weight: int
  
  int BMI() {
   return height * height / weight
  }
 }
 
 // 実際に使うとき
 const nanakani = new Human(168, 60)
 nanakani.BMI()
  • ドメインロジックに集まってない
class Human {
 id: string
 height: int
 weight: int
}

// 実際
const nanakani = new Human(168, 60)
const nanakaniBmi = nanakani.height * nanakani.height / nanakani.weight
const jinYang = ...
const jinYangBmi = jinYang.height ...

ドメインサービス

  • (複数の)ドメインモデルを引数にとってドメインモデルを返す(返さなくてもいい)関数
  • 複数のドメインを扱うときにつかう
  • 使いすぎに注意
    • Entityや値オブジェクトにもたせると不自然なもののみ持たせる
  • code:身長比較
function shinchouHikaku(a: Human, b: Human) -> int {
 return a.height - b.height
}

function shinchouHikaku(a: Human, b: Human) -> int {
 if a.height > b.height {
  return a.height - b.height
 } else {
  return b.height - a.height
 }
}
  • code:返さなくてもいい例
class Human {
 id: string
 height: int
 weight: int
 friends: Human[]
 
 void addFriend(other: Human) {
  self.friends.add(other)
 }
}

function makeFriend(a: Human, b: Human) {
 a.addFriend(b)
 b.addFriend(a)
}
  • code:引数が違う場合
class Human {
 id: string
 pets: Pet[]
}

class Pet {
 id: string
 owner: Human
}

function makePet(h: Human, p: Pet) {
 h.addPets(p)
 p.selectOwner(h)
}

Repository

  • Identityを素にEntityを復元(再構築)したり保存したりする
  • ドメイン層(アプリケーション層)では、その振る舞いのみ定義する
  • 存在だけ認知している/どういった概念かだけ知っている
  • Repository例
interface HumanRepository {
 fromIdentity(id: string) -> Human
 find170OverHumans() -> Human[]
 save(humans: Human[])
}

集約

  • その中だけは整合性が取れてないといけないという境界
  • 集約単位で保存しましょうねというRepositoryの方針がある

Factory

  • Entityを作る方法もドメイン知識だよね
    • じゃあそれ専用のオブジェクトを作ろう!

アプリケーション層(Usecase)

  • アプリケーションとは何か
    • お金を稼がないもの
    • 実際にドメインモデルを協調させるやつ
  • ドメインオブジェクトを操作して、ある特定の利用者の目的を達成するように導くサービス
  • フレンド登録ユースケース
class MakeFriendUsecase {
 humanRepository: humanRepository
 
 constructor(humanRepository: HumanRepository) {
  this.humanRepository = humanRepository
 }

 void makeFriend(a: id{string}, others: id{string}[]) {
  const humanA = this.humanRepository.fromIdentity(a)
  const humans = this.humanRepository.fromIdentity(a)
  
  for other in others {
   const human = this.humanRepository.fromIdentity(a)
   domainService.makeFriend(humanA, human)
  }
  
  this.humanRepository.save([humanA, ...humans])
 }
 
  void make170Friend(a: id{string}) {
    const humanA = this.humanRepository.fromIdentity(a)
    const humans = this.humanRepository.find170OverHumans()
    
    for other in others {
     const human = this.humanRepository.fromIdentity(a)
     domainService.makeFriend(humanA, human)
    }
    
    this.humanRepository.save([humanA, ...humans])
   }
}

インフラ層

  • お金を消費するもの
  • 具体的な実装の層
    • DBはMySQLを使う、フレームワークはHono/Railsを使うとか
  • サーバーのエンドポイントがここにある
  • main関数から起動する実装
function main() {
 const db = MySQL::new("127.0.0.1:3306")
 const makeFriendUsecase = new MakeFriendUsecase(new MysqlHumanRepositoryImpl(db))
 
 // テストなら↓
 const testDB = testdb()
 const makeFriendUsecase = new MakeFriendUsecase(new TestMysqlHumanRepositoryImpl(db)
 
 // ↓↓↓クリーンアーキテクチャならこの辺がAdapter層として分離される
 const route = Route::new("get", (req: Request) => {
  const a = req.body.self
  const others = req.body.others
  makeFriendUsecase.makefriend(a, others)
  return new Response({ message: "success" })
 })
 // ↑↑↑
 
 app.run(route)
}
  • Repositoryの実装をここで行う
  • code:RepositoryImpl
class MysqlHumanRepositoryImpl implements HumanRepository {
 db: MysdbqlDB
 
 constructor(db: MysdbqlDB) {
  this.db = db
 }
 
 fromIdentity(id: string) -> Human {
  const record = this.db.from("humans").select("*").where("id = ${id}")
  return new Human({ id: record[0], height: record[1], weight: record[2] })
 }
 
 save(humans: Human[]) {
  省略
 }
 
 170() {}
}
  • code:テストRepositoryImpl
 class TestMysqlHumanRepositoryImpl implements HumanRepository {
  db: []
  
  constructor() {
   this.db = []
  }
  
  fromIdentity(id: string) -> Human {
   const record = this.db.find(id)
   return new Human({ id: record[0], height: record[1], weight: record[2] })
  }
  
  save(humans: Human[]) {
   this.db.append()
  }
  
  170() {}
 }