Web Engineer の @wozaki です。 スタディサプリの合格特訓コースの機能開発・保守が主な業務です。
今年3月に開催された Rails Developer Meetup 2018 で、弊社の @kyanny が Quipper における「関心の分離」の歴史 をプレゼンしました。 プレゼンでは、モノリシックなコードベースを「関心の分離」により保守性を高めた事例をご紹介しました。「分断されたモノリス」に共感された方も多かったのではないでしょうか。
「分断されたモノリス」がちょっと流行ってうれしい😄
— Kensuke Nagae (@kyanny) 2018年3月27日
本記事では「関心の分離」事例を、ドメイン駆動設計(以下、DDD)の観点からご紹介します。
対象読者
- モノリシックで大規模なアプリケーションを保守している方
- DDD の具体例を探している方
※ 注意点
- DDDの詳しい説明はしません。補足は入れますが、予備知識があるとより分かりやすいです。
- DDDをガッツリ適用したコードは出てきません。
本記事における用語の定義
関心の分離
@kyannyのプレゼン(Wikipedia)から引用します。
https://ja.wikipedia.org/wiki/%E9%96%A2%E5%BF%83%E3%81%AE%E5%88%86%E9%9B%A2
DDD
ドメイン駆動設計の道標 - sandbox から引用します。
この「DDD とは何なのか?」という説明から明確に認識すべきは、DDD = 設計手法ではないという事です。 勿論、設計手法の話も含まれますが、DDD の根幹は、「現実の複雑な問題解決領域をどの様にソフトウェアに落としこむか」というテーマの、組織、開発プロセス、設計論に及ぶソフトウェア開発哲学であり、その探求の過程で生まれたモデリングの戦略、戦術のパターン、思想の集まりです。
本記事では、設計以外についても言及します。
モデル
ドメインオブジェクト。ドメインにおける解決方法を表現したもの。 RailsのActiveRecordを継承したクラスそのものではないが、合致することもある。
事例紹介
「スタディサプリ合格特訓コースの資料請求ドメイン」を元に、事例を2点ご紹介します。 ユースケースは以下の通りです。
- ユーザとして
- 資料請求を申込む
- スタディサプリとして
- 資料を配送する
- 資料を配送後、フォローの電話を行う
- フォローの電話を行った後n日以内に合格特訓コースに入会した場合は、契約書を配送する
1.名前空間の分離 => 境界づけられたコンテキスト
@kyanny のプレゼンでは、スタディサプリ専用のモデルを Aya:: ネームスペースで分離する事例 をご紹介しました。資料請求ドメインでは、更に Aya::Coaching::Brochure::
で分離しています。
背景・解決したかった課題
開発当初、合格特訓コースドメインを表現するAya::Coaching::
ネームスペースが既に存在しており、その中に DocumentRequest
モデルが一つある状態でした。
しかし、資料請求ドメインの理解が進むにつれて、一つのモデルだけでは開発が難しいことが分かりました。そのためモデルを追加していきましたが、以下の課題がありました。
- 名前が冗長になる
- 他のモデル名と被らないように冗長になる (
DocumentRequestShippedResult
等)
- 他のモデル名と被らないように冗長になる (
- シンプルな名前にすると責務が曖昧になる
- モデルの責務に適さない改修が入る可能性がある
- 資料請求ドメインを構成する知識が散らばる
また、これらの課題は、モデル以外 (API エンドポイント、Jenkinsのview、Slack channel 等) でも発生しました。
やったこと
Aya::Coaching::Brochure::
を導入し他のモデルから隔離した
- 資料請求ドメインに関わる全てに
brochure
の名前を付けた
※スタディサプリのJenkinsなので aya
のprefixは省略している。
得られた結果
認知的エントリポイント
名前から資料請求ドメインを連想できるようになりました。 資料請求ドメインに関連するもの全てに一貫した名前を含めることが重要です。 名前変更時の追従コストは大きいですが、引き継ぐ際に効果を発揮すると思います。
コンテキストの確定
資料請求ドメインの何々を表現しやすくなりました。
ステークホルダーと会話する際、意識/無意識的にコンテキストを確定することで認識齟齬を防いでいます。
それはソフトウェアにも有効です。資料請求者を表すモデルをApplicant
と表現していますが、Applicant
モデルは資料請求ドメイン外にも存在しています。
そのため、Aya::Coaching::Brochure::
があることで「資料請求ドメインのApplicant」だと確定することができます。
また、資料請求ドメイン専用のSlack channelでの会話は、コンテキストを確立する手間が省けて楽になりました。
「境界づけられたコンテキスト」の観点から補足
定義を、実践ドメイン駆動設計 | ヴァーン・ヴァーノン, 高木正弘 から引用します
全部入りの巨大なモデルをひとつ作ってしまうという誘惑に駆られるプロジェクトもある。あらゆる名称が唯一の意味しかもたないように、組織全体で合意を形成しようというのが、その狙いだ。このようなモデリング手法では、落とし穴にはまってしまう。まず、あらゆる概念に対してステークホルダー全員が納得するような共通の意味付けをすることなど、事実上不可能だ。
さらに具体的な説明は、 境界づけられたコンテキスト 概念編 - ドメイン駆動設計用語解説 [DDD] に載っているので、参考になると思います。
2.アプリケーションの分離 => レイヤードアーキテクチャ
プレゼンでは、用途ごとにアプリケーションを分離する事例 をご紹介しました。資料請求ドメインでも同様にアプリケーションを分離・モデル層の共有ライブラリ化しています。
背景・解決したかった課題
モデルロジックの重複・流出
資料請求ドメインには様々なユースケースがあり、それぞれに適したアプリケーションが既にありました。 各アプリケーションにモデルロジックを書くとDRY原則に反します。
さらにドメインの知識が流出するためドメインの理解が難しくなります。 ref ドメインモデル貧血症 - Martin Fowler's Bliki
利用技術や運用方針の変更
資料の配送
や資料請求者へのフォローの電話
業務は、外部企業へ委託しています。
委託先が変わると利用技術や運用方針が変わる可能性があり、その際の改修範囲を少なくしたい要望がありました。
やったこと
モデルの分離とライブラリ化
各アプリケーションから、ライブラリ化したモデルを利用しています。
利用技術(インフラレイヤ)の分離
ユースケース「スタディサプリとして、資料を配送後、フォローの電話を行う」のコードで詳細をご紹介します。
システムの概要は以下の通りです。
コードは、多少改変していますが雰囲気をお伝えできればと思います。
# インフラレイヤ class S3Uploader def initialize(bucket_name:) @bucket_name = bucket_name end def upload(key: key, body: body) base_path.files.create(key: key, body: body) end private # https://github.com/fog/fog-aws に密結合しているが、fog-aws以外に変わる可能性は低いので許容 def base_path @base_path ||= FogHelper.establish_storage_connection.directories.get(@bucket_name) end end # アプリケーションレイヤ # フォローの電話業務を委託する企業へ連携するファイル。 module Aya module Coaching module Brochure module Telemarketing class UploadFile DIR_NAME = 'xxx' FILE_NAME_FORMAT = 'xxx' CSV_HEADERS = %w(xxx yyy zzz) # UploadFileは「何の技術」でアップロードするのか知らない # uploaderが upload(key:, name:) メソッドを持っていれば交換可能 def initialize(uploader:) @rows = [] @uploader = uploader end def append(target_record) @rows << shape_row(target_record) end def upload @uploader.upload(key: name, body: generate_body) end private # 各行を整形する。この例ではCSVの各field値 def shape_row(applicant) [ applicant.xxx, applicant.xxx, applicant.xxx, ] end def generate_body CSV.generate(headers: CSV_HEADERS, write_headers: true, col_sep: CSV_COL_SEP) do |csv| # xxx end end def name # xxx end end end end end end # アプリケーションレイヤ # 現在、Jenkins・rakeタスク経由で実行する運用だが、それが変わっても影響は受けない # ドメインに関係ない依存関係は、このレイヤで解決する module Aya module Coaching module Brochure module Telemarketing class RequestRunner def run s3_uploader = S3Uploader.new(bucket_name: "aya-coaching-brochure-telemarketing") upload_content = Telemarketing::UploadFile.new(uploader: s3_uploader) ActiveRecord::Base.transaction do Applicant.bulk_request_to_telemarket!(upload_content: upload_content) end end end end end end end # ドメインレイヤ # 資料請求者を表すクラス module Aya module Coaching module Brochure class Applicant key :first_name key :last_name key :phone_number key :xxxxx # 他にも状態(資料請求済み、フォローの電話前、契約書配送 等)を持っている # 状態の遷移知識はApplicantにカプセル化する state_machine :state, initial: :need_to_ship_brochure do transition need_to_telemarket: :requested_to_telemarket, on: :request_to_telemarket end scope :need_to_telemarket, -> { where(state: :need_to_telemarket) } class << self # Applicantは「何の技術」で「何を」アップロードするのか知らない # upload_contentが、appendとuploadメソッドを持っていれば交換可能 def bulk_request_to_telemarket!(upload_content:) need_to_telemarket.find_each do |applicant| upload_content.append(applicant) applicant.request_to_telemarket! end upload_content.upload end end end end end end
Rubyのダックタイピングで、依存関係逆転の原則(DIP) を適用できます。 つまり、上位レイヤから渡ってきたオブジェクトは、同一レイヤのインタフェースを実装していることを期待しています。 おそらく他の静的型付け言語であれば、インタフェースのみ同一レイヤーで定義する実装になるかもしれません。
ダックタイピングは暗黙的なので、Unitテストで明示・ドキュメント化すると、より保守性が上がりそうです。 テストの例や、ダックタイピングを探す方法は以下の書籍が参考になります。
オブジェクト指向設計実践ガイド ~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方 | Sandi Metz, 髙山 泰基
得られた結果
- ドメインロジックの隔離
- 交換可能性
- 各レイヤの具体ロジックを変更しても改修範囲が少ない
- 安定した方向への依存
- ドメインロジックはユーザインタフェースやアプリケーションロジックより変更頻度が低い可能性が高い(安定している)
- 安定している方向に依存することで改修時の影響を受けづらくなる
「レイヤードアーキテクチャ」の観点から補足
定義を、実践ドメイン駆動設計 | ヴァーン・ヴァーノン, 高木正弘 から引用します
インフラストラクチャやUIへの依存も排除して、さらには業務ロジック以外のアプリケーションロジックも分離する。複雑なプログラムはレイヤに分割すること。各レイヤ内で設計を進め、凝集度を高めて下位層だけに依存するようにすること。
書籍には他にもアーキテクチャが紹介されていますが、一貫して大事なことは、「ドメインレイヤを隔離すること」と「安定したレイヤへ単一方向に依存する」ことだと思います。
まとめ
DDDの視点から「関心の分離」を再考しました。 DDDは抽象的な説明が多いですが、既知のソフトウェア開発プラクティスと照らし合わせると理解が進みやすいかもしれません。
また、「分断されたモノリス」にも課題はあるので、保守性を高めるプロジェクトが進行中です。
Quipperでは、コードの設計・保守性・DDDなどについて議論するのが好きな Web Engineer を募集しています。