理想と現実と妥協点

dobashi

2019-09-08

理想: DDD

DDDの流派の話は置いとく(どうせ新しいのすぐ出るし)

project/
 - controller/
 - model/
 - repo/
 - service/

*SPAならviewがない
project/
 - controller/
 - model/
   - service/
   - domain/
   - persistence|infrastructure
 - view/

MVC

Simple版

project/lib/
 - project/
   - service/
   - domain/
   - repo
 - project_web/
   - controller/
   - view/

Phoenix

distributed computing

現代ではjsonやprotocol buffersなどの汎用的なプロトコルにして個別に差し替えできるようにすべき

project/
 - api/
   - service/
   - domain/
 - ap/
   - service/
   - repo
 - web/
   - controller
   - view
class UserController(val service: UserService) {
  get("/users"), req, res -> {
    service.list()
  }
}

service apiやdomain定義を言語非依存の設定ファイルにしてWeb/APからそれぞれ見るのはアリ?

-> それならSwaggerとかの方がいい

昔はWeb-AP間通信に言語依存のRPCを使っていた

DDDのモデリング

自分がプロダクトの発案者で、気心のしれた仲間と少人数のプロジェクトなら採用します

業務としてきちんとDDDで完走するためになにが必要か

スキーマ定義

id name email zip prefecture address
1 Akihito a@b.c 100-0000 tokyo Chiyoda1
2 Naruhito x@y.z 602-0881 kyoto Kamigyou

Users

id name email zip prefecture address
1 あさひ株式会社 a@b.c 100-0000 tokyo Chiyoda1
2 夕陽(株) x@y.z 106-0000 tokyo Minato3

Companies

都道府県マスタはPKがid: string

ちゃんとDDDするなら

data class User (
  val id: Long
  val name: String
  val email: String
  val zip: String
  val perfecture: String
  val address: String
)

data class Company (
  val id: Long
  val name: String
  val email: String
  val zip: String
  val perfecture: String
  val address: String
)
typealias UserId = Long
typealias UserName = String
data class User (
  val id: UserId
  val name: UserName
  val email: Email
  val zip: Zip
  val address: Address
)

typealias CompanyId = Long
typealias CompanyName = String
data class Company (
  val id: CompanyId
  val name: CompanyName
  val email: Email
  val zip: Zip
  val address: Address
)

data class email(
  val address: String
){
  fun isValid(): Boolean = ...
  /* 携帯キャリアアドレスかどうか */
  fun isCareer(): Boolean = ...
}

typealias TelArea = Int
typealias TelCity = Int
typealias TelLocal = Int
data class Tel(
  val area: TelArea
  val city: TelCity
  val local: TelLocal
){
  /* 実在しうる電話番号かどうか */
  fun isValid(): Boolean = ...
  fun findPrefecture(): List[Prefecture] = ...
  companion object {
    fun parse(value: String): Tel = ...
  }
}
data class Zip(
  val major: Int
  val minor: Int
){
  /* 実在する郵便番号かどうか */
  fun isValid(): Boolean = ... // 外部のAPI叩く?
  companion object {
    fun isValid(value: String): Boolean = isValid(parse(value))
    fun isValid(major: Int, minor: Int): Boolean = ...
  }
}

data class Address(
  val prefecture: Prefecture
  val value: String // street address
  val building: String
  )

Repo

Domain Model

郵便番号が8桁になってもロジックをZipクラスだけに押し込める

Value Object

(Cのsize_t,time_t)

First Class Collection

(☓User[] ○Users)

設計者依存

 Tel#findPref()について、pref.-areaは一部多対多の関係になるので、PrefectureTelRelationテーブル、クラスを別途用意した方が良い。

Addressクラス内にCountry, Zipを持ちたいという人もいるかもしれない。

キャッシュするなら問題ないが、DB/Network問い合わせが発生するとモデルからサービスの依存関係が発生するのでモデルのメソッドではなくAddressService#findPref(Tel)にすべき。

ビジネスドメインでは曖昧さや朝令暮改が倍増

これを実現するために

ドメインが大きく息の長いプロジェクトほどDDDを採用した方がコードを綺麗に保てる。

  • 設計の経験が十分にある
  • カスタマーと打ち合わせを繰り返しドメインの情報を聞き出す能力がある(交渉力、人当たりの良さ、分析能力)
  • 「この人が言うならそうなんだろうな」と思わせる説得力と人徳がある

という人がアーキテクトとしてプロダクトのライフサイクル終了まで付き合ってくれるならDDDは理想

しかし現実は非情

  • どれかの能力に欠けている
  • そんなすごい人がいるなら今売り出し中の別のプロダクトで使いたいと引っ張り出される
  • あるいは転職される
  • 途中で人が交代していき中途半端な思想が入り乱れ見るも無残に

自分のプロダクトが認められてイスラエルの企業に雇いたいと言われたら行きたいです!

トランザクションスクリプト

テーブルモジュール

トランザクションスクリプト

  • ユースケースごとに処理を頭からお尻まで書く
  • 似たようなロジックが分散する
  • 一箇所の修正が他に影響を与えることはないが、モデルの修正があるとそこを利用している全スクリプトを漏れなく修正する必要
  • DDDの対極にある
  • AWS Lambda上でserverless REST APIをGoで書く、かつ小さなプロジェクトならアリ
  • 大きくなったら後ろのモデルを共通化した方がいい
  • 関連するレコードセット(例えばZIP, Addr, Prefecture)をひとまとめにしてサービス層を用意する
  • ドメインモデルにIntなどコンピュータよりの情報が残ってしまう
  • ロジックが変わった時は概ねそのサービスモジュールだけを直せば済むケースが多い
  • DDDとトランザクションスクリプトの中間

ほとんどのケースの業務システムで妥当な選択だと思われる(※個人の感想です)

テーブルモジュール

  • controller/
  • service/
  • repo/
  • model/

controllerはユースケースと1:1

serviceはこのアプリが提供するビジネスロジックをきれいな形で提供し、modelで返す

repoがmodelとして機能しているようなF/Wを使う場合は、service配下にテーブルモジュールごとにディレクトリを切って、service,repo,modelをその中に置いたほうが見通しがよくなる場合もある

serviceのI/Oはあえてロジックを持たないDTOとしてserviceがAPIサーバーとする方法もアリ

DDDではアンチパターン(FatModel,ThinServiceであるべき)

精緻なドメイン分析よりサービスにロジックがあるほうが楽

部屋とYシャツと私と

By hakaicode

部屋とYシャツと私と

  • 529