READYFOR Tech Blog

READYFOR のエンジニアブログ

DDDの腐敗防止層を用いた変更容易性向上

こんにちは、リファクタリング大好きなミノ駆動です。 リファクタリングを主任務とするアプリケーションアーキテクトとして、弊社READYFORのエンジニアリングを推進しています。

ドメイン駆動設計に登場する 腐敗防止層 を用いたリファクタリングで、システムの変更容易性を向上したお話を解説します。

本記事の概要

背景

弊社READYFORのシステムは、モノリシックなRuby on Railsのサービスとして実装されています。 システムが解決したいドメイン(業務活動)にはさまざまなセグメントがあり、その中に審査オペレーションがあります。

f:id:minodriven:20220411092702p:plain

審査オペレーションとは、クラウドファンディング実行者さんが申し込みを提出してからプロジェクト公開に至るまでの、申し込み内容を審査する業務フローです。 弊社サービス拡大に伴い、より多くのプロジェクトが公開できるよう、審査オペレーションの業務は日々改善が繰り返されています。

f:id:minodriven:20220411092754p:plain

審査オペレーションをより効率化するため、審査関係のロジックにもさまざまな改良が加えられてきました。

ところが度重なるロジック変更により、機能改善が難しくなっていきました。 技術的負債が蓄積し、変更困難なロジックになっていたのです。

正確なリファクタリングには設計が必要

技術的負債の解消に有効な手法が、リファクタリングです。 リファクタリングとは外から見た挙動を変えずに、内部のロジックを整理することです。

if文のネストを浅くしたり、ロジックの一部分をメソッドで抽出する程度であれば、ロジックの意図が分かっていなくても、ある程度の複雑さは解消可能です。 しかし複雑怪奇なロジックでは、十分に負債を解消できません。リファクタリングのゴールに辿り着けません。実際、審査関連のロジックはかなり混乱していて、ぱっと見で何を意味するものなのか読み取れませんでした。

そもそも技術的負債は、あるべき構造と現状の構造とのギャップです。あるべき構造がリファクタリングのゴールとなります。 あるべき構造が定義できていないと、どこが負債になっているのか全て明確化できません。そしてリファクタリングのゴールにも辿り着けません。

従って、まずはあるべき構造を設計することから始めました。

DDDベースの設計

あるべき構造の設計は、ドメイン駆動設計(DDD)の考え方をベースにしました。 DDDは、機能性と変更容易性を長期的に向上させ、利益体質にする設計手法です。

DDDではドメイン概念をドメインモデルとして設計します。 ドメイン概念とは、たとえば「実行者」や「審査」、「申し込み」といった、システムで取り扱う業務活動や社会的活動の概念です。 ドメインモデルとして表現することで、要求から実装まで一貫性を持たせ、機能性と変更容易性の向上を狙うのが特徴です。

審査オペレーションでやりたいことや解決したい事柄をシステム上で的確に表現するため、リファクタリングのゴールとなる、あるべき構造は、ドメインモデルとして設計しました。

分析のための情報収集

ドメインモデリングには、ドメイン概念や、その背景・目的を洗い出すことが必須です。

DDDでは、ドメイン(業務活動)に詳しい人のことをドメインエキスパートと呼びます。ドメインエキスパートと協力してドメインモデルを作っていくことの重要性をDDDでは説いています。

まずは審査オペレーションに詳しい人たちに、インタビューして回ることにしました。そして下記のような質問や情報収集をしました。

  • 審査をどう進めているのか
  • その目的は
  • 審査を実施する上で特に注意を向けている事柄
  • 特殊なケースの審査 etc...

インタビューと分析を進めていったところ、審査には一定の作業フローがあることがわかりました。そしてその作業フローは、状態遷移図で表現可能なものであることもわかりました。

判明するイビツな構造

システム設計において、状態遷移はステートマシンとして設計します。 そして、状態ひとつひとつを状態クラスやenumなどで表現するのがシンプルで扱いやすい構造となります。

f:id:minodriven:20220411093435p:plain

ところが、分析で分かった作業フローと審査関連のロジックがどう対応付いているかソースコードを調査したところ……なんと複数のモデル(ActiveRecord)の、複数のカラムを複雑に組み合わせて審査状態を表現していたのです。

審査関連のモデル(ActiveRecord、すなわちDBテーブル)は、はじめからイビツな構造だったわけではなく、様々な歴史的経緯を背景に拡張が繰り返されてきたものだったそうです。 結果として、経緯を知る一部のエンジニアのみがかろうじて触れるような、大変取り扱いにくいモデルになってしまった、とのことなのです。

イビツな構造を隔離する腐敗防止層

DBテーブルの構造に課題がある場合、DBをリファクタリングする選択肢があります。しかし、審査オペレーション以外の様々なロジックが審査関連モデルに依存していたり、DBのリファクタリングは高コスト、高リスクです。

そこで今回利用したのがDDDの腐敗防止層です。

腐敗防止層とは、変更困難でイビツなロジックをカプセル化する設計パターンです。 イビツな構造のロジックを腐敗防止層の中に閉じ込めます。 そして腐敗防止層には変換機構が設けられており、層からデータを入出力する際に相互に解釈変換します。

  • 腐敗防止層→他の層:防止層内のイビツなデータ形式を、キレイな構造のオブジェクトに変換して渡す
  • 他の層→腐敗防止層:キレイな構造のオブジェクトを、防止層内のデータ形式に変換して渡す

このようにイビツな構造を他と隔離することで、以下の利点が生まれます。

  • イビツな構造を無理に変更せずに済む。
    • 変更に伴うコストやリスクを低く抑えられる。
  • イビツな構造の影響が他に伝搬しなくなる。
    • 腐敗防止層とは別の層に、より機能性と変更容易性の高いキレイな構造を設計しやすくなる。

なお、DBなどデータソースの制御ロジックをカプセル化するRepositoryパターンも腐敗防止層の一種です。

設計

腐敗防止層を取り入れて設計した構造が次の図です。 (※模式的に表現した構造です。実際の審査ステップやプロダクト構造を表したものではありません。)

f:id:minodriven:20220411092937p:plain

ドメイン層 審査ステップ

ドメイン層はDDDに登場するアーキテクチャレイヤーです。 ドメインモデルを実装形態にしたドメインオブジェクトを置く層です。 Rails非依存の、pureなRubyクラスとして実装します。

状態遷移図で表現した、審査の作業フローおける各審査ステップを個別の状態クラスとして設計します。各審査ステップはただ状態を示すだけでなく、次の遷移先や遷移権限などの知識もカプセル化しています。

各審査ステップは、ステップの追加や変更を容易にするため、審査ステップinterfaceにより抽象化して扱います。なお、Rubyはダックタイピング仕様であり、interfaceは心の中にしかありません。しかし何で抽象化しているのか判断できるよう module をinterfaceの代わりにしています。

アプリケーションでは、この審査ステップを中心に審査の作業フローを制御する形になるので、とてもシンプルなロジックになります。複数のActiveRecordの、複数のカラムを複雑に制御する労苦から解放されます。 また、変更容易性が高い、キレイな構造がベースになるので、機能改善しやすくなります。

インフラ層 gateway

インフラ層はDBへのアクセスや、別システムとの通信を実装するアーキテクチャレイヤーです。

gatewayが腐敗防止層です。 各審査ステップにそれぞれ個別に対応するgatewayを設けます。 各gatewayには、各審査ステップに対応するActiveRecordとカラムの値に解釈変換する知識をカプセル化します。 イビツな構造がgatewayの中に閉じ込められたおかげで、他のアプリケーションロジックがDBテーブルのイビツな構造に振り回される労苦から解放されます。

なお、gatewayは上で図示していないRepositoryからコールして使う形をとっています。

ふたつの橋作戦(ストラングラーパターン)

このリファクタリングのスコープは小さくありません。 審査の作業フロー全体にかかるスコープなので、規模はそれなりに大きめです。

安全にリファクタリングを進める主な手段のひとつに、テストコードがあります。 もちろん今回のリファクタリングでもテストコードでしっかり保護しましたが、それでも結合レベルの挙動はテストコードだけでは保護が困難な場合があります。 規模が大きなリファクタリングに失敗した場合、revertが大変になります。

この課題を解決するために用いたリファクタリング手法が、ふたつの橋作戦です。 これは、リファクタリング対象のコードをコピーし、コピーしたコードをリファクタリングする手法です。

手順1:リファクタリング対象をコピー

まず、リファクタリング対象のコードをコピーします。

f:id:minodriven:20220411093006p:plain

手順2:新しい橋のコードをリファクタリング

コピーした、新しい橋のコードをリファクタリングします。

f:id:minodriven:20220411093031p:plain

手順3:向き先を新しい橋へ切り替え

リファクタリングしたら、向き先を新しい橋へ切り替えます。 動作に問題がある場合、すぐに古い橋に切り替えられます。

f:id:minodriven:20220411093531p:plain

手順4:古い橋を落とす

リリース後、しばらく様子を見て問題なければ、古い橋を削除します。

f:id:minodriven:20220411093554p:plain

このリファクタリング手法は、書籍『モノリスからマイクロサービスへ』にて、ストラングラーパターンとして解説されています。ただ、個人的には新旧ふたつの橋に例えた方が分かりやすいので、ふたつの橋作戦と呼称しています。 (※ふたつの橋の元ネタはt_wada氏のこちらのスライド

リファクタリングの成果

以上の設計、および手法を用いてリファクタリングを実施しました。 以下の成果を得られました。

  • リファクタリング前と変わらず、滞りなく審査オペレーションを遂行できている。
  • 一部のエンジニアしか理解できなかったロジックが簡明になり、誰でも触れるようになった。
  • 審査ステップを容易に追加、変更したりなど、審査フローを容易にカスタマイズ可能になった。

設計ノウハウを本にして出版します

今回のような拡張性を向上させるリファクタリングは、実は初めてではありません。 以前勤めていた会社での開発も含め、4度目になります。

リファクタリング対象となったソフトウェアは、IT領域、使用言語、フレームワーク…どれも異なるものです。にもかかわらず、設計ノウハウは活用できています。 私の設計技術はかなり再現性が高いと自負します。

さて、今回記事として取り上げた腐敗防止層は、やや応用レベルの設計です。 リファクタリングの実施においては、記事には記載していない、基本かつ様々なオブジェクト指向設計のノウハウをふんだんに用いております。

そうしたオブジェクト指向設計の実践的ノウハウを書籍化しました。

『良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方』

様々な悪しきソースコードを例に、マズい構造の何が課題なのか、どう改善すれば変更しやすい構造を設計できるかを解説する、設計の入門書です。

  • 向上が期待できるスキル
    • 悪しき構造を見破るスキル
    • 変更に強い構造を設計するスキル
  • 本書の特徴
    • 初級~中級向けの設計入門書です。
    • 膨大なサンプルコードと事例で解説します(400ページ!)。
    • 私が普段活用している、オブジェクト指向設計の実践的ノウハウを解説します。
    • Javaで書かれています。
    • 陥りがちな罠を中心に解説します。特にバグを埋め込みやすい仕様変更時の注意点や、対策としての設計方法を解説します。

より詳しい紹介記事はこちら。 note.com

目次や概要はこちら。 gihyo.jp

AmazonなどECサイトで、すでに多くの予約が入っており、ヨドバシ.comでは一時期予約終了になったほどです。おかげさまで初版部数が2倍になりました。

2022年4月30日発売(ほぼ同日に電子書籍版も出ます)。

新人教育にピッタリなので、ぜひお手に取りご活用していただけると幸いです。