Azit株式会社でバックエンドエンジニアとして機能開発の他にDDDの導入やリファクタリングをしている鈴木です。
私が弊社で仕事をするようになってから半年たちましたので、ミノ駆動さんとQiitaさんのキャンペーンを利用して、私がおこなったことを振り返りたいと思います。
システムの説明
最初に開発しているシステムについて説明をします。
ラストワンマイルの配送に特化しているシステムを開発しています。
荷物の配送を実施してくださる配達パートナーと、配送の依頼をしてくださるクライアントという2つの種類のユーザーを扱う配送サービスです。
配達パートナーさんは以下の3種類にわかれます
- ギグワーカーとして単発の案件を実施してくださる方々
- 私たちと連携してクライアントさん専属のシフトに入ってくださる方々
- クライアントさんが直接管理しておられ我々のシステムを通して稼動している方々
また、配送については以下のようなフィルタリングがあります。
- クライアントさんが自社で管理されている方々と弊社からの専属の方々両方に送りたいといった場合や、逆に自社で管理されておられる方のみに送りたいといった、「誰に送りたいか」といったグループでのフィルタリング
- 「冷蔵可能」のような特定のスキルを持った方にだけ送りたいといった配達パートナーの方のスキルでのフィルタリング
他にもルートを最適化して配送するなどの複雑な機能があります。
このため、ドメインモデリングをしながら、DDDの考え方を生かした実装をしていくやりかたにマッチする複雑度が高いシステムと言えると思います。
今回実施した内容について
今回は次の3つのことを実施しました
- 用語を理解するのが難しかったのでユビキタス言語を定着させた
- 何をするのかが名前からでは理解できないFormクラスのリファクタリング
- システムの全体を理解できるようにするためにコンテキストマップを作成した
次にそれぞれの詳細です。
用語を理解するのが難しかったのでユビキタス言語を定着させた
おこなった理由
私たちが開発しているシステムは、配送システムの中でも配達パートナーとクライアントの間の配送というラストワンマイルに特化した配送プラットフォームなのでシステム特有の用語があります。
例えば、配送料金の体型を表す時間立て時間立や件立てなどの普段聞かない名前や停車時間という一般的な名前ですが、いろいろな意味が含まれているものもありました。
そのため、開発に参加したときはシステムのキャッチアップが大変でした。
解決策
システムにに関する用語を理解するには、ビジネスチームとエンジニア間で共通で使用するユビキタス言語を見て理解するのがいいと思いますので、ユビキタス言語集をみてましたがそのユビキタス言語集は以前作成したことはあるが、放置されている状態でした。
そこで、システムの理解と並行する形でユビキタス言語をアップデートすることになりました。
ユビキタス言語は放置されているものの、プロジェクトに関する仕様書を作成するという文化が私がチームに加わる前からしっかりとありました。
仕様書をみながら用語をまとめていき、コードには書いてあるがドキュメントには書いていないことは、ビジネスチームと頻繁にやりとりをしているCTOに質問しながら用語集を作成していき一通り必要なことを網羅してある用語集を作成することができました。
このユビキタス言語集を作成していく中で、システムに関する知識ができたのも非常に大きいと思います。
作成したユビキタス言語は以下のようにNotion上にまとめてあります。
ユビキタス言語を作成していくのと並行して、会社のビジネスモデルを理解するために部署間のリーダー同士のMTGにも参加して、ビジネスについても理解することができたので会社で使われている用語について理解することができました。
ユビキタス言語の更新をしたことで、エンジニア間で話すときでもユビキタス言語で話をして、ビジネスサイドが話していることもそのまま理解できたのがシステム開発をするときにすごい役に立っています。
何をするのかが名前からでは理解できないFormクラスのリファクタリング
おこなった理由
Formクラスを使用しているが、すでにメリットがほぼない状態
Formクラスはフロントエンドも含めてRails内で完結しているときは、すごい有用なパターンだと思います。
しかし、私がチームに参加したときはすでにReactを使用してる状況でした。
ReactのフォームとRailsのフォームクラスでは乖離が発生している状態でした。
そのため、Formクラスを使用していることのメリットが薄かったです。
Formクラスの名前から何をしているのかが理解することはできない
Formクラスの命名パターンはActiveRecordを継承しているクラスのインナークラスとして定義してCRUDでの名前をつけるということが習慣化されていました。
例えば、配達業務を開始するときに実行するのが、DeliveryEntry::CreateForm です。
そのため、Formクラスがクラス名から何をしているのかがわからない状態でした。
ミノ駆動さんがおっしゃっている目的駆動名前設計からは程遠い状態でした。
Railsに依存していたのを修正
ドメインモデリングをしていることもあり、コードもドメインモデリングに適した形のDDDの戦術パターンのドメインサービスクラスにできるだけ近づけていこうという話をチームで決定しました。
しかし、FormクラスはRailsに依存していたのでリファクタリングをしました。
解決策
配達業務を開始するFormクラスのリファクタリング前のクラス
DeliveryEntoryは配達パートナーが配達している状態を表しているテーブルです。
保存する処理は代わりのコードで記述させていただきます。
Formクラス
class DeliveryEntry
class CreateForm
extend ActiveModel::Callbacks
attr_reader :delivery_entry
def initialize(delivery_partner)
@delivery_partner = delivery_partner
end
define_model_callbacks :save
after_save AfterSaveCallbacks
def save!
run_callbacks :save do
ActiveRecord::Base.transaction do
save_hoge
save_fuga
save_piyo
end
delivery_entry
end
end
AfterSaveCallbacks
class DeliveryEntry
class CreateForm
class AfterSaveCallbacks
class << self
def after_save(create_form)
new(create_form).after_save
end
end
private_class_method :new
def initialize(create_form)
@create_form = create_form
end
def after_save
メールなどに通知するジョブをスケジューリングする処理
end
end
end
end
上記のコードは以下の問題点があると思います。
- DeliveryEntry::CreateFormだと単純にDeliveryEntryを作成しているだけのようにに見える
- しかし実際は集荷処理をするための準備をするという処理
- AfterSaveCallbacksが何をしているのかが一切わからない
そのため、以下のリファクタリングをしました。
- 配達ドメインの中の集荷(Pickup)の準備をするというユビキタス言語やドメインモデルに沿った名前にした
- RailsのCallbacksクラスの依存をやめて、after_saveが実行する順番を明示的にした
module DeliveryDomain
class GoingToPickupPreparer
attr_reader :delivery_entry
class << self
def execute!(delivery_entry)
new(create_form).after_save
end
end
private_class_method :new
def initialize(delivery_entry)
@delivery_entry = delivery_entry
end
def execute!
ActiveRecord::Base.transaction do
save_hoge
save_fuga
save_piyo
end
NotificationJobsScheduler.execute(self)
delivery_entry
end
このようなFormクラスが、多くあるため優先度の高い順からひとつずつリファクタリングをしていきました。これにより、クラスとユビキタス言語、ドメインモデリングが関連することになったので、クラス名をみることで、ドメインがなにをするのかがわかりやすくなったと思います。
(まだ、道半ばですが
システムの全体を理解できるようにするためにコンテキストマップを作成した
おこなった理由
開発しているシステムの機能は多いため、システムに存在するドメインがどれぐらいあるのかもチームに参加したてのころはわからなかったです。
また、それぞれどのような関係があるのかを理解した状態での優先づけしてリファクタリングすることができていなかったです。
解決策
実践ドメイン駆動設計の勉強会でコンテキストマップを作成しました
弊社では、ソフトウェア設計に関する読書会をしていて、ちょうど改善したいと思ったタイミングで、実践ドメイン駆動設計の読書会をおこないながらDDDの理解を深めておりました。
実践ドメイン駆動設計の章末の問題で自分たちが開発しているシステムのドメインを考えたり、コンテキストマップを作成するという問題がありチームで挑戦することをしました。
コンテキストマップを作成していく中で思ったこと
図を作成していく事自体も有益ですが、チームでディスカッションをしながらお互いの認識を揃ることができたので、ドメインについて同じ認識を持っていることが設計について話すときもコードレビューをするときに役立っています。
コンテキストマップに従ってドメインモジュールを作成しています
ビジネスに沿ったドメインモデルに従うためには作成したコンテキストマップに従ってモジュールを作っていったほうがいいと判断したため、現在はドメインクラスを使用したドメインモジュールに移行するリファクタリングに挑戦しております。
Domainクラスは、DomainのしたにContextを定義して、そのクラスにコンテキスト内にドメイン知識の実装をしています。
ActiveRecordはドメインの知識をもたないRDB上のテーブルについての知識や全ドメインに共通することだけの知識を持つようにしています
ディレクトリ構成は以下のようになります
domains
delivery_domain
delivery_context POROとして構成されているドメインクラス
match_context (PORO)
models
delivery_partner (ActiveRecordを継承したクラス)
delivery_entry (ActiveRecordを継承したクラス)
最後に
今のチームに私が参加してから、おこなった設計の改善点は次の3つです。
- 用語を理解するのが難しかったのでユビキタス言語を定着させた
- 何をするのかが名前からでは理解できないFormクラスのリファクタリング
- システムの全体を理解できるようにするためにコンテキストマップを作成した
これのことをしたことにより、要件定義や設計と実際のコードでユビキタス言語を意識した開発をすることができています。
システムの特性上より多くのクライアントのニーズに答えていく必要がありますが、ビジネスモデリングをしたうえで設計、実装をすることで後々大きな問題になる負債を抱えずに開発していくことができると思っています。
CTOが書いた記事も読んでいただけると改善してきたことについてより知ることができますので、弊社での開発についてもっと知りたい方はよんでいただけると嬉しいです。
記事を最後までみていただきありがとうございました!