ロコガイド テックブログ

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの社員がいろいろな記事を書いています。

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの社員がいろいろな記事を書いています。

技術的負債に立ち向かう ③ロコガイドの現在地編

f:id:ar_tama:20210422112342p:plain

こんにちは!@ar_tamaです。「技術的負債に立ち向かう」シリーズは①実装編②ドキュメント編で終わりの予定だったのですが、小出しにしていたロコガイドでの実践についてもまとめておこうと思い、再度筆を執りました。
今回は ③ロコガイドの現在地編 と称しまして、現在私たちがどのような課題を抱え、それをどのように乗り越えていこうとしているかをご紹介します。

※ あくまでも2021年5月時点でのスナップショットであり、今後取られるであろう様々な舵によって着地点が大きく変わる可能性があります

課題とアプローチ

ロコガイドが運営するトクバイのコードが抱える負債や課題についてはCTO前田の記事「トクバイにおけるレガシーシステム改善への取り組み」にて概説されているとおりで、意図しない密結合によるエンバグや、歴史的経緯の紐解きによりデリバリーコストがかさむといった問題がだんだんと無視できない状況となってきました。
黎明期とは取り巻く環境も開発チームの規模も徐々に変化してきた/しているということもあり、コードベースを状況に適応させ、メンバーそれぞれの力を更に引き出すべく、リアーキテクチャへの取り組みを開始しました。

キーフレーズには「トクバイにおけるレガシーシステム改善への取り組み」でも言及されている「モジュラーモノリス」を、ゴールには「Developer Experienceが向上し、サービス改善のサイクルが早まること」を設定しました。ゴールは言い換えるならば「拡張に開き、変更に閉じた状態」が作り出されるような環境を作るということですが、その実現を阻害している原因は実装編で例に挙げたような可読性の低下が主たるものでした。現状の「カオスモノリス」なコードベースからビジネスロジックを分離し、機能同士の絡まりを解きほぐすために、以下のように進めていきました。

登り方とチェックポイントを決める

まずは、「依存・変更影響の少ない既存実装箇所」と「新規実装箇所」の2点に絞ってアプローチすることにしました。トクバイはメンバーのほぼ全員が触るモノリスアプリケーションなため、いきなり本丸のドメインに切り込むのはリスクが高いというのが理由です。
分離に際してやりたいこと・できることはたくさんあるものの、あくまでもチーム全体の生産性を上げることが目的のため、対応を取捨選択せねばなりません。そのため週に1度、CTO・VPoEを含むメンバーとのチェックポイントを設け、立てた方針に従って同じ方向を向いて走れるかという観点でレビューしてもらいました。

配置方法を決める

次に相対したのは切り出したコードの置き場所です。「どこからでも何でも呼べる」Railsアプリケーションにほどよい制約を設けることを目的に、まずはRails Engineを使った境界の構築を検討しました。しかしどの粒度での切り分けが最適か分からない状態でのスタートなこと、導入に際し全体の可視性が大きく低下することなどから、他社事例も参考にしつつ、はじめはnamespaceベースでの分割を決定しました。そのためJavaのモジュール分割のような拘束力はありません*1が、馴染みやすさと機動力を優先した選択です。

「全部入り」から間引く

昨年からチームで「クリーンアーキテクチャ」「実践DDD」を輪読していたこともあり、これらのエッセンスは可能な範囲で取り入れようと計画していました。具体化の過程では「モノリスからマイクロサービスへ」「ドメイン駆動設計モデリング/実装ガイド」の思想も取り入れつつ、戦略をブラッシュアップしていきました。「モノリスからマイクロサービスへ」には

ドメインモデルからは、どこから分解に着手すべきか合理的に判断するのに必要十分な情報を得られれば良い、ということを理解するのが重要だ。(中略)最初から正確に行う必要はないからだ。必要なのは、次のステップに進むのに十分な情報だけだ。

とあり、その例として集約と境界づけられたコンテキストの適度な援用が勧められています。しかしこれは機能過剰となったモノリスアプリケーションをマイクロサービスへ分割するための手引なので、垂直方向の境界*2を設計する際には役立ちましたが、水平方向の境界設計*3についての言及はありません。そのため「実践DDD」「ドメイン駆動設計モデリング/実装ガイド」を参照しながら、まずは「全部入り」の理想状態を作ってみて、そこから運用コストもしくはパフォーマンスに影響を与えそうな部分*4を削っていく、というアプローチを採りました。この塩梅はチームの人数やコードベースの状態、またゴールをどこに置くかでも変わってくる部分だと思いますので、どこを妥協点とするかをディスカッションしながら進めていくのがよいでしょう。

余談ですが、まさにこれらの対応を行っているさなかにShopifyのUpgrowが公開されたため、これまでのアプローチを全て捨ててUpgrowを使用することも検討しました。しかしながら、あちらは水平分割におけるベストプラクティスといった具合で、ビジネスロジックの切り出しや垂直分割については言及がなかったこと、値の受け渡し(Action/Result)を採用すると現時点では機動力を大きく損なってしまいそうなことなどから採用を見送りました。現在はなんらかの理由で公開が取り下げられているようですが、docの「Railsべからず集・べし集」やライブラリの思想には完全に同意しており、Railsによる新規プロジェクトの立ち上げ時にはぜひ導入したいと考えています。

啓蒙する

せっかくルールやガイドラインを定めても、それがチームに浸透しなければ意味がありません。そのため前述の方向性レビューのほかに、以下のような取り組みを行っています*5

  • 資料化して勉強会を開く
  • 試行錯誤の過程をWikiなどにアウトプットし、読んでもらう
  • プロトタイプ実装のレビュアーとして巻き込む
  • 小さな変更(一時限りのバッチ処理など)で実践し、レビューを受ける(またはその逆)
  • リアーキテクチャが実践された新規実装部分に関わるメンバーを徐々に増やす

やはり、この手のものは説明やレビューだけでは「読めるけど書けない」状態を脱せないですし、(できればプロダクションで)実践してもらわないことには改善点も見えてこないため、根気強く取り組まなければなりません。逆に言えば実践までたどり着ければ改善のサイクルが自然に回るということでもあるので、この役割を担う人には諦めずに啓蒙し続ける・周りを巻き込む胆力が必要とされるように感じています。こういった活動は一人で頑張ってうまくいく類のものではないため、「ビジョンと戦略を決めて巻き込む」「偉い人の口から言わせる」などのアプローチも採りながら、徐々に浸透させていきましょう。

成果物

そんなこんなで出来上がったのがこちら。

f:id:ar_tama:20210427072925p:plain

垂直分割については、app以下に新しいディレクトリ(components)を切り、配置することとしました。このように「見かけ上の境界」を作ることで、書き手の心理的ハードルを上げ、コンテキスト同士の結合を疎に保つ狙いです。コンテキストの分け方はCQRSに大きく影響を受けており、いわゆる「機能」単位(この場合は"チラシ")より責務をはっきりと分けられるため、拡張しやすく変更時の影響を最小限に留められる実感を得ています。

f:id:ar_tama:20210427080114p:plain

水平分割には、原則としてレイヤードアーキテクチャを採用しました。オニオン(ヘキサゴナル)やクリーンアーキテクチャ等々も検討しましたが、RailsではインタフェースやDIの表現が難しいこと、浸透までのハードルを極力下げたかったことなどから見送っています。
具体的にはかつての素朴なMVCから責務を分解し、アプリケーション層・ドメイン層・インフラ層の大分類とそれぞれに紐付く小分類とに定義しました。小分類のうち代表的なものを以下に示します。

  • Presentation
    • DDDの「アプリケーションサービス」のうち、主に「ビューのアダプター(腐敗防止層)」の役割
    • 既存のコントローラ層のうち、以下を配置
      • プラットフォーム都合の操作(リクエストパラメータのハンドリングやバリデーションなど)
      • アプリケーション固有の手続き
  • Usecase
    • DDDの「アプリケーションサービス」のうち、主に「ドメインモデルのクライアント」の役割
    • ビジネスロジック(ドメイン層)にアクセスするためのアプリケーション層の手続き
    • ★1 WebとJobなど、複数アプリケーション層からドメイン層へのアクセスを共通化できる場合にのみ、この層を設ける
  • Domain
    • 既存のモデル層のうち以下を配置
      • ビジネスロジックの実現に必要なデータ構造および手続き
      • コンテキスト内で共有されうるオブジェクト・エンティティ・サービス
  • Repository
    • 既存のモデル層から、データソースからデータを取得・挿入・加工する処理を配置
      • ★2 基本的にはドメインオブジェクト(エンティティ・値オブジェクト)に変換するが、 ActiveRecord Objectを返してもよい とする
    • ActiveRecordのアダプターとしても機能
  • Table
    • ActiveRecord::Base を継承したクラス群を配置
    • 基本的にはリレーションの定義・最低限のscopeのみが記述される
    • 複数コンテキストで同じtableを扱う場合はコンテキストごとに定義し直す

★のついている部分が前述の「全部入り」から引き戻したポイントです。★1は垂直分割によって処理の主体が明確に分けられる(ケースが増えた)ため、ドメイン層の呼び出し処理はひとまずControllerに寄せることにしました。
★2は、データ構造とエンティティの属性が同等になる場合のオブジェクト生成コストや取り回しにくさなどを考慮して、一定許容することにしました。拡張の過程でコンテキスト独自の属性を生やしたくなるタイミングが来るはずなので、そのとき初めてドメインオブジェクトを定義する、という方針にしています。もちろんこれは上位層でDB操作がされないこと(save など)を前提としているため、早めにGoodcheckなどのチェックツールを入れたいところです。

この調子で分割を進めていけば、縦横の境界が見え「拡張に開き、変更に閉じた状態」を作り出すことができるはずです。ただ、ある程度の目処が立ったタイミングでKotlinなどの言語に移植できればとも思っています。トクバイは今でも盛んに機能改善・機能追加が行われていますが、黎明期よりは成熟期であると言えるため、やはり静的型付けやパッケージング、インタフェースの恩恵を受けながら開発するのが最善だと考えています。一足飛びに式年遷宮……とはいかないのが歯がゆいところですが、フルスクラッチでリファクタを行うのはたいてい失敗するので、ぐっと我慢して上記を実践しています。

おわりに

「モノリスからマイクロサービスへ」の終盤に、「他人の事例から学ぶべき教訓があるのは事実だが、自分のコンテキストでうまく機能するアプローチを見つけるには、時間をかけなければならない」という言葉がありました。我々が現在目指しているのはマイクロサービスそのものではありませんが、この言葉の意味するところを痛感しています。世の中のベストプラクティスをしっかりと身につけながら自分たちの文脈に合わせて再実装することは、正解が見えない状態での模索も多く非常に大変ですが、それだけやりがいもありますし、間違いなく今のフェーズでしかできないチャレンジでもあります(あなたのご参画をお待ちしています!)。今はまだ走り始めの段階ですので、ここから更にブラッシュアップを重ね、生み出す価値の量と速さを非連続的に高めていけたらと考えています。
同じように悩み実践しているところも少なくないはずですので、こうした事例の公開がどんどん増えていくことを願いつつ、擱筆することとします。

*1:代わりにGoodcheckなどを入れたいと思っています

*2:機能ごとの分割

*3:レイヤの分割

*4:ActiveRecord Objectをどこまで隠蔽するかなど。着地点については後述

*5:言ってしまえば本稿もその一環です