理想と現実と妥協点
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 | zip | prefecture | address | |
---|---|---|---|---|---|
1 | Akihito | a@b.c | 100-0000 | tokyo | Chiyoda1 |
2 | Naruhito | x@y.z | 602-0881 | kyoto | Kamigyou |
Users
id | name | 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シャツと私と
- 622