初めて現場でDDD(ドメイン駆動設計)を実践してみた
はじめに
はじめまして!21年新卒としてSHIFT DAAE(ダーエ)開発グループに所属されました田中です。
DAAEでの新卒研修終了後、初めてアサインされたプロジェクトにてサーバーサイドKotlinを使ったDDD開発に取り組むことになりました。当初DDD開発未経験だった頃からそのプロジェクトに携わり早1年…、DDD開発の経験談やその魅力を少しでも共有できればと思いここにまとめます。
|DDD(ドメイン駆動設計)とは
ドメイン駆動設計の著者エリック・エヴァンスさんは、DDDは以下の4つから成り立つと言っています。
ここで言うモデルとはドメインモデルを指します。
ドメインモデルとは、
業務データとそれを使った判断 / 加工 /計算のロジックを一体にしたものを集めて体系的に整理したものです。
参考:「現場で役立つシステム設計の原則」より
ここから上記の原則を噛み砕くと、ドメイン駆動設計とは、
「システムの対象とする業務領域(ドメイン)に焦点を当てること。そのためにドメインエキスパートは、ソフトウェア開発者とともに共通言語や背景の認識を合わせ、ドメインモデルを練り上げ、それをソフトウェアに落とし込んでいく設計」
ということになります。
現場でもまず初めにこの「ドメインモデル」を作成をし、開発をしている中でも、以前作成したドメインモデルを再度練り上げるなどを繰り返していました。
|DDD開発に取り組んでみて感じた魅力
次にDDDのメリットについて、私の経験も踏まえつつ解説していきます。DDD開発を通して大きく以下の3つの恩恵が得ることができました。
コードの保守改修が楽になる
業務知識がそのままコードに落とされるので、ソースコードを見れば仕様が把握できる
ユーザーとの意思疎通が円滑になる。
上記それぞれのメリットについて解説していきます。
DDDのメリット①コードの保守改修が楽になる
例えば手続き型の設計だと、データクラスと機能クラスに分けてコーディングを進めることになるのですが、それに対しDDDの場合は
業務仕様をコードで表現しようとするので、オブジェクト指向なプログラミングになります。
例)年齢と生年月日
■手続き型の設計の場合
テーブル設計の段階では、年齢を知るための手段である生年月日だけがテーブルのカラムに登場するので、「年齢」を計算するロジックは生年月日データを参照できる場所であればどこにでも記述できることになります。
イメージ
年齢から「大人」か「子供」かを判断するロジックの場合、このようにデータクラスと機能クラスに分けるやり方だと、異なるスクリプトに同じロジックが重複しがちになってしまいます。
その結果、変更の対象箇所が散在し、見通しが悪くなってしまうし、変更の影響範囲も推測しにくくなってしまいます。
■ドメイン駆動設計の場合
ドメインモデルではまず、年齢クラスとして「年齢」という業務の関心事を整理しようとします。つまり、年齢を計算するロジックの置き場所は、年齢クラスだけになります。
イメージ
data class Age private constructor(
/** 年齢 */
val value: Int
) {
companion object {
/**
* 誕生日から年齢クラスを生成する
* @param birthDay 生年月日
* @return Age 年齢オブジェクト
*/
fun of(birthDay: ZonedDateTime):Age {
val now = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"))
val age = now.year - birthDay.year
return when {
now.monthValue Age(age - 1)
now.monthValue == birthDay.monthValue && now.dayOfMonth Age(age - 1)
else -> Age(age)
}
}
}
/**
* 年齢から大人か子供かを判定する
* @return Age 年齢オブジェクト
*/
fun judgeAdultOrChild() {
when {
this.value PersonEnum.CHILD
else -> PersonEnum.ADULT
}
}
}
こうして年齢に関する様々なロジックが年齢クラスに集まりやすくなり、業務ロジックの置き場所が明確になるので、手続き型のプログラムで起こりがちな業務ロジックが散在して重複する問題を解決することができます。
まとめると、ドメイン駆動設計で開発を行うと
業務的な判断 / 加工 / 計算のロジックの一元管理
業務の関心事とコードを直接対応させ、どこに何が書いているかを分かりやすく整理する
業務ルールの変更や追加の時に、変更の影響を狭い範囲に閉じ込める
というメリットを得られる故、コードの保守改修が楽になります。
現場の所感でも、何かシステム上の不具合が発生した時に、その箇所を推測し改修範囲も小さく行えました。
依存性逆転の原則によりDBなどの外部接続との依存を回避できる
依存性逆転の原則とはwikipediaから引用すると下記となります。
上記の依存性逆転の原則が組み込まれたDDD(ドメイン駆動設計)のアーキテクチャの代表例としてオニオンアーキテクチャがあります。
オニオンアーキテクチャ
User Interface(プレゼンテーション)層・・・リクエストを受け付けたり、レスポンス返したりする(外部とのやり取り)
Application Service(ユースケース)層・・・ユースケース、システムの処理の流れを表している。ドメイン層で作成したクラスを呼び出す。
Domain(ドメイン)層・・・ドメインモデルが置かれる層
Infrastructure(インフラストラクチャー)層・・・DBアクセスや、外部サービスとの連携を行う
ここで、業務ロジックが含まれるDomain層がDBアクセスなどの外部アクセスを行うInfrastructure層に依存しなくなることが改修コストを下げているポイントで、DDD開発により受けられた恩恵だと思います。
具体的には、
Domain層に、DBアクセスなどをしている実装リポジトリクラスを抽象化したクラス(インターフェースリポジトリクラス)を置いて
Application Service層からはこの抽象化したクラスを呼び出すことでInfrastructure層との依存を回避しています。
例:ホテルの予約を行う場合
Application Service層
@Service
class ReserveService(
private val userRepository: UserRepository,
private val reserveRepository: ReserveRepository,
private val roomRepository: RoomRepository
){
@Transactional
fun reserve(request: Request) {
/** ユーザー情報の確認 */
userRepository.find(request.userId) ?: throw IllegalArgumentException("該当するユーザーが存在しません userId:${request.userId}")
val room = roomRepository.findByRoomId(request.roomId) ?: throw IllegalArgumentException("該当する部屋が存在しません roomId:${request.roomId}")
/** 予約する宿泊状況の確認 */
if (room.checkIn <= request.checkIn && request.checkOut <= room.checkOut) {
throw IllegalArgumentException("その日の間の予約は既に埋まっております。 roomId:${request.roomId}")
}
/** 部屋の人数を確認 */
if (room.numberOfGuests < request.numberOfGuests) {
throw IllegalArgumentException("宿泊可能人数を超えています。 roomId:${request.roomId}")
}
/** 部屋の予約 */
reserveRepository.reserve(request)
}
}
Domain層にそれぞれのリポジトリの抽象化クラスを設定
interface UserRepository {
fun find(id: Long): User?
}
interface RoomRepository {
fun findByRoomId(roomId: Long): Room?
}
interface ReserveRepository {
fun reserve(request: Request)
}
Infrastructure層でDBアクセスなどの実装クラスを記述する。
@Repository
class UserRepositoryImpl(
private val mapper: UserMapper
) : UserRepository {
override fun find(id: Long): User? {
DBに接続し、該当するデータを取得するロジックを記載
...
}
@Repository
class RoomRepositoryImpl(
private val mapper: RoomMapper
) : RoomRepository {
override fun fun findByRoomId(roomId: Long): Room? {
DBに接続し、該当するデータを取得するロジックを記載
...
}
@Repository
class ReserveRepositoryImpl(
private val mapper: ReserveMapper
) : ReserveRepository {
override fun reserve(request: Request) {
DBに接続し、該当するデータを取得するロジックを記載
...
}
このようにして依存性逆転の原則が実現されることにより
他の層はDBなどの外部接続の変更の影響を受けなくなる
データ取得などの実装クラスをmock化してテストコードを書きやすくする
といったメリットを享受することができます。
DDDのメリット②業務知識がそのままコードに落とされるので、ソースコードを見れば仕様が把握できる
DDDのメリット①で解説したように、DDDでは業務の関心事を整理しようとするため、 出来上がったソースコードは自然とそのコードだけで業務仕様を表すようになります。
これは、開発メンバーの入れ替わりが多い場合に、ソースコードを追えば業務内容を理解しやすくなることもあれば逆も然りで、業務内容をキャッチアップした後ではソースコードを追いやすくなります。
DDDのメリット③ユーザーとの意思疎通が円滑になる
ドメイン駆動設計はユーザーの業務知識(ドメイン)に焦点を当てて開発をするため、開発現場でもまず初めにユーザーの業務内容、使われる言葉、関心ごとの整理に努めました。
そのため、例えばユーザーが「〇〇の業務仕様を変更したい」となった場合でも、ドメインモデリングを共に取り組んできたエンジニアであれば、システムのどこを改修すればいいのかはすぐ推測できるようになるし、改修による影響範囲も狭いので容易にアップデートすることもできます。
さいごに
私のような新米エンジニアだと、正直新しい技術や開発手法に目が行きがちなので、 「どの技術, 手法を駆使してこのシステムを作ってやろうか」といった技術的なアプローチで解決しようとしてしまいがちなのですが、現場でDDD(ドメイン駆動設計)に取り組んでみて、そもそも 「そのソフトウェアを作ることによってユーザーの何の課題を解決したいのか」を考えさせられたのでとてもいい経験ができました!
また、オブジェクト指向プログラミングを実際に経験してその良さを体感できたのもいい勉強になりました!
\もっと身近にもっとリアルに!DAAE公式Twitter/
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/