2024/07/01よりVP of CTO Officeとなったよこちゃん(@jikun)です。
hacomonoでは開発組織全体の横断課題に取り組んでましたが、今後はより経営課題を意識して行きたいと思っています。
技術的負債解消のための専門チームを作ろう!
hacomonoでは、元々イネーブルメントチームがあり、バックエンドのスペシャリストのiwazerさん(@iwazer)とフロントエンドのスペシャリストのみゅーとん(@_mew_ton)の2名体制でした。
しかし二人とも日々多くの差し込み依頼に忙殺され、また開発チームが忙しすぎるなどイネーブルメントで何とかしていくことの限界を感じていました。
- Rails や Vueのバージョンアップが進まない
- パフォーマンス改善すべき箇所が多い
- リファクタしたいがテストコードが不足している
- 自動テストの実行に時間がかかり過ぎる
- Platform Engineeringチームが提供した新しいPlatformへの移行が大変
しかもこれらの課題はお互いに依存関係にあることが多く、何かを解決するためには先に別の何かを対応しなければいけないなど開発チームがスプリントの合間にやるのは困難でした。
そんな時、CTOのまこさん(@macococo)からANDPAD様の事例を紹介いただき「これだ!」と思い、
自分たちでも技術的負債の解消に向き合うためにリアーキテクティングまで踏み込む専門チームを作ることを決め2024/04/01に正式に発足しました。
組織名にイネーブリングを残したいだと?
イネーブリング部を改名し、他部署から技術的負債解消に強いメンバーの異動をお願いしてリアーキテクチャ部として準備をしていたところ、フロントエンドスペシャリストのみゅーとん(@_mew_ton)から
「リアーキテクチャとイネーブルメントは違う。両方やる必要があるから組織名にも残してほしい」
と強い要望を受けました。
「え?リアーキテクチャにイネーブルメントも含んでいることで良くないですか?」
って思ったのですが、みゅーとんの強い気持ちに押され「リアーキテクチャ&イネーブルメント部」になりました。
数ヶ月たって正解だったなと思っています。
- 自分たちでやるべきところは手を動かす→リアーキテクチャとして
- 自分たちだけでは広がらないので開発チームが主体的に技術的負債を解消できるための仕組みやドキュメンテーションや広報を頑張る→イネーブルメントとして
の2つの側面を両方意識できるのが良いと感じてます。
またhacomonoは7/1〜6/30を事業年度としており、組織発足から3ヶ月しかたってない6月末に各組織の振り返りをしたのですが、大変すごいシナジーにより超短期的に成果を出せました!
以降は、フロントエンド、バックエンド両方に深く食い込んでくれている野崎サイモンさんから直近の取り組みについて紹介してもらいます。
重要なドメインロジックに向き合い、改善する(リファクタリングの取り組み紹介)
ここからは野崎(@serum_vision)がバトンタッチして担当します。
リアーキテクチャの機運の高まりとともに本格的に取り組んだ、バックエンドリファクタリングの事例を紹介します。決済・金額計算の関係する重要(そして複雑)なビジネスロジックに向き合い、改善する取り組みを行いました。
なお、本稿はFindy社主催「開発生産性Conference 2024」でのセッションをベースに、ブログ用に少し加筆したものとなっています。
hacomonoにおける決済
リファクタリングの取り組みを説明する前に、hacomonoが提供している機能や決済の位置づけ、特徴をプレビューしておくこととします。
hacomonoは実店舗運営で発生する業務をデジタルにできますよ、ということをやっていますので、すなわちジムやスポーツクラブ、スクールで発生する決済業務を広くカバーしています。
したがって、主な登場人物は主にジムに通う会員と、店舗運営にあたるスタッフの2つということになります(厳密にはもう少し幅があるけどここでは単純化)。
クラブの会員であればメンバーサイトから自身で手続きと決済を完結できますし、スタッフの方であれば管理画面やPOSレジから同じように売上の登録・物販の決済ができます。
決済を一元管理でき、どのインターフェースで決済を行っても、「いつ」「どこで」「だれが」「なにを」購入したか、管理画面にて確認できるようになっています。
課題認識 ― ドメインと実装の難しさ
hacomonoが提供するユースケースはやることが概ね似ています。したがって共通処理の実装になっているのですが、裏を返すと「似ているが微妙に違う」ロジックもぐちゃっと一つになっています。
そのような経緯から、この共通処理はドメインおよび実装双方の観点で難解なロジックに育ってしまっていました。
ユーザストーリーから見た決済のドメイン、そして実装それぞれから当時の状況を整理すると次のようになります:
ドメインの目線
- (一般的に)お金の計算はトラブルになりやすい。
- 変更頻度の高い、重要なビジネスロジックである。
- 金額の算出に使う概念が多く、選択的に組み合わされる。
実装の目線
- ユニットテストはない(すべてインテグレーションテスト)
- すべてがミュータブル。変数のライフサイクルが長い。
- 複数のユーザストーリーから呼ばれ、凝集度が低い。
- Ruby on Railsベースの独自レイヤードアーキテクチャ。下図のように、Controller → ドメイン層 → インフラ層の構造になっており、階層間がギチギチに結合している。
その結果、普段読んでいる人でさえ難しいと感じるコードとなってしまっています。
- 前提知識なしにはほとんど読めない
- 潜在的な不具合を抱えている
- 変更コストが高い(開発者だけでは安心して修正できない)
特に致命的なのは変更コストの高さで、お金を取り扱うがゆえに失敗できないプレッシャーがとても高いです。
いっぽうで、このプロジェクトを始めた頃に全社的な品質改善の機運も高まっていました。
そのような状況が重なり、腰を据えてしっかり改善に着手することとしました。
コードを改善する
派手なリアーキテクチャは志向せず、テストの実装・リファクタリングを愚直に進めました。
まずユニットテストよりはじめよ
リファクタリングしたいと考えてもテストがなければなりません。まずはテストを1から書き起こしていきます…
実装はもとより、仕様もドキュメントはないケースが非常に多く、今回対象の共通処理もコードがドキュメントでした。
したがって、知りうる限りすべて、共通処理を通る画面を触って動作を確認、確認結果からユニットテストを書き起こし動作とコードの一致を調べていきました。
IDEで見たテストの全体像です(実際はもう少し量があります)。ここではコツなどなくて、「がんばる」メソッドで乗り切りました… ただし、ここでがんばったことはあとあと副作用の形で効いてきます。
計算のモデルを発見する
ユニットテストが書ければあとはやりたいようにやっていくだけです!
ユニットテストの書き起こしと並行して、共通処理に詳しい方に解説いただきながらクラスの特徴を考える時間を持ってみたところ、次の特徴を有することがわかってきました:
- 任意の計算を担う処理単位が複数いる
- ある処理の出力が次の処理の入力になっている
ここで注目すべきは2つめの特徴です。例えば、POSレジで「サプリメントをクーポン割引付きで購入する」ケースを考えてみると
- サプリメントの定価を導出する
- クーポンで任意割合の値引きを計算する
- 合計金額に対して税率および税額を計算する
3つのステップに分解でき、しかも前の処理のアウトプットを引き継いで次のステップにそのまま引き渡していることが見えてきました。
責務の分割: Chain of Responsibilityによる解釈
一緒にプロジェクトに取り組んでくださった先輩が、そのような特徴からChain of Responsibility(CoR)パターンによる抽象化と実装が改善に有効そうと考えられ、改善の素案が生まれるに至りました。
私も当時その抽象化を聞いて膝を打った記憶があり、ClojureやF#にあるような「パイプライン演算」のイメージで解釈できました。(試みに、Clojureのスレッドマクロを解釈例として載せてみます)
(defn calculate [purchase] -> purchase (init member) (discount) (calculate-tax)))
ひるがえって、Rubyは普通に書くとミュータブルで変数のスコープも無秩序に広く長くなってしまいます。
我々のアーキテクチャはRailsベースとはいえ、独自のレイヤードアーキテクチャですからそれにフィットするライブラリを作り、既存のアーキテクチャを無視しないようにしながらCoRの実装を目指しました。
(ここからは私の成果ではありませんが代理でご紹介です)
はじめに、ライブラリ実装の全体像を示してみます。さきに示したように、決済処理に登場する計算は、「割引」や「税計算」など、一つひとつを独立した計算単位とみなせます(以下、ハンドラと呼びます)。
このライブラリでは呼び元で登録されたキューを順番に処理しつつ、呼び元の要求にしたがって次の処理に渡すときの値の可変性や参照できる属性の管理をコンテキストが担う構造です。
使い方も例示します。
まずはハンドラの定義からです。例のように、ProcessorChain
をincludeすることでハンドラの基底モジュールが自動的にincludeされてマクロを使える仕組みになっています。
process!
メソッドを実装することで、あとでハンドラをキューに積んで捌く時に呼ばれるようになります。
class CalculatePurchaseDetailsService include ProcessorChain input :purchase_details output :purchase_details def process! output.purchase_details = input.purchase_details.map! { |price| price * 10 } end end
ここで入力(input) / 出力(output)をDSLで宣言することでコードの理解を助長します。
チーム開発においては、他人の書いたコードを読む時間の方が多いケースもあります。そのコストを最小限にする工夫を心がけています。
RubyのDSLはそう言った意味でも活躍する場が多いと思います。
ハンドラを定義できたので、1連の処理の仕方や属性フィールドを初期化しておきます。この例は単純なもので purchase_details
しか触らないようになっていますが、同じ要領で初期値と属性の定義を記述することもできます。
original_purchase_details = [100, 200] context = ProcessorChain.context_builder. attribute!(:purchase_details, copy: :once). build do |c| c.purchase_details = original_purchase_details end
最後にハンドラをコンテキストに登録して process!
をコールします。コンテキストは登録順にハンドラを呼び出してくれ、最後にすべての処理を通ってきた結果を得られます。
context.chain << PurchaseDetailsCalculator.new
context.chain.process!
Chain of Responsibilityの実装例は、簡単ですがここまでです。
このライブラリを骨格に据えた新しい実装は、入出力のミュータビリティをランタイムで制御できるようになりました。そのおかげで、処理の単位で変数のスコープが独立し、変数の状態やスコープを安心して取り扱えます。
共通処理の呼び出し文脈を考慮した抽象化: Builderパターンによる任意性の表現
hacomonoの決済は、先述の通り複数の経路から行われるのに加え、特に割引は組み合わせのパターンが多いです。経路によってはプランの割引だけ、他の経路ではPOSレジ特有の割引のみ考慮して計算… などなど。
なお、少しだけ私の解釈を補足すると、製品全体で金額の計算ルールは(すなわちドメインのルールであるから)統一されているほうが良いと考えています。したがって、このような共通性・任意性はドメインに起因する複雑性と理解して受け入れています。
明細の初期化や税額の計算のように、必ず行うものと呼び元で選択的に選んで実行されるものの2つが存在します。
この「状況によって処理を組み合わせる」モデルの表現にはBuilderパターンの適用が有効でした。
少しだけ実際のコードを引用しながらイメージを共有してみます。クラスの定義は:
class CalculateService def with_plan_discount!(member_plan, member_plan_discounts) if member_plan.present? @plan_discount_service = CalculatePlanDiscountService.new(user, @member, member_plan, member_plan_discounts) end self end # ... snip ... def invoke! chainable_services = [ CalculatePurchaseDetailsService.new(user), # 売上明細計算 CalculateCouponDiscountService.new(user, @member), # クーポン割引計算 @plan_discount_service, # プラン割引計算 # ... snip ... ].compact @chain.push(*chainable_services) @chain.process! @chain.store_result! state end end
割引ごとに適用する条件をクリアしたらインスタンス変数にハンドラが追加される仕組みです。
呼ぶときには:
state = Purchase::Purchases::V2::CalculateService.new(user, member, purchase, purchase_details). with_plan_discount!(member_plan, member_plan_discounts). invoke!
割引をつけずに税額の計算だけ行う場合には with_plan_discount!
を付けずに呼び出せばよく、使われる文脈によってどの処理を通したいかを明示できるようになりました。
処理の単位でメソッドが実装されたため、IDEの支援も受けながら利用箇所の特定が容易になった点も見逃せません。
なお、割引の種類は他にも4種類ほどあり、使われる場面によって組み合わせのケースがいくつかあります。
リリースまで丁寧にやり切る
このプロジェクトを通じて改善に向き合い、良くなったコードには自信を持っています。ですが、一度も油を差していないものをいきなり実践投入するのはさすがに気が引けてしまいます。
そのような、なんとなく抱えている不安を払拭すべく、実戦投入に際してはフィーチャーフラグも組み合わせて利用することとしました。
フィーチャーフラグはhacomonoのプラットフォームチームが開発、メンテしている基盤コードですがシンプルかつ使い勝手の良いものでした。フラグが有効ならonを返してくれるので、フラグが有効になっているかどうかだけを見ます。
if FeatureFlag.on?('calculate-service-2') # 新クラスを使う else # 旧クラスを使う end
我々はウェルネス産業にフォーカスしてビジネスをしており、お客様の数もバリエーションも年々増えています。フィーチャーフラグは範囲を絞ってリリースする助けとなってくれ、リファクタリングの正しさを示してくれました。
改善効果
私が同じクラスをずっと読んでいることによる慣れを考慮しても、「責務分割」「テスト」「可読性」の観点で改善を感じられています。
責務が分割されたことで、変数のスコープやライフサイクルが限定されるメリットはもちろん、計算手続き全体の見通しがとてもよくなりました。今後、手順を増やしたりハンドラの中を修正する際にも、変更範囲の極小化が期待できます。
また、テストがなければリファクタリングができないので、前工程としてテストの書き起こしに取り組みました。これは副次的に潜在的な不具合の改善にも寄与してくれています。
最後に… 修正したクラスを触った方に使ってみた感触を聞いてみたところ、良い反応をもらえたので概ね良い改善だったのではないでしょうか?!
改善を経た気付き
最後に、リファクタリングに取り組んだ中で得た気づきをいくつかあげて、本稿を締めたいと思います。
1.テストの記述はメンタルモデルの獲得に寄与する
リファクタリングをやろうとなったとき、はじめどのように改善されるべきなのか、想像がついていませんでした。実際、型情報もないし配列だったり変数だったりが散在していてめちゃくちゃ難解でした。
ですが、テストを書き起こしていくうちに、今まで開発者が捉えていたクラスやその使われ方の視座を得ていく実感がありました。
ユニットテストはリファクタリングのための必要条件ですが、ドキュメントがなかったり変更頻度の高い口伝の領域では、視座を映す装置にもなることを実感したのでした。
テスト、本当に大事。
2.コードのクリーンアップや分割は地道にやるしかないし、それが一番良い
私個人の意見ですが、デザインパターンやアーキテクチャパターンは今も有用なケースが多いと考えています。
デザインパターンのうちいくつかのものは現在でも教養のようなもので、キャッチアップしておくといいと思います。
しかし、ドメインの深淵は一つずつ読んで直していくしかないです。読みながら、改善しながら見えてきたドメインの暗黙知に対して得たツールボックスから使えそうなツールで掘り進める。そのような態度で臨むとバランスよく進捗していく実感を得られました。
現実のユースケースを理解しながら改善を進めることで、現実に即した解を出せることをこのプロジェクトで再認識できたと思います。
3.リファクタリングにも積極的に張ってくれる環境が原動力になった
我々はウェルネス産業にフォーカスして、産業を良くするぞと猛烈な勢いでビジネスを進めています。
ですから、まさに「猛攻」を仕掛けている感覚ではある一方で、そろそろ「兵站」がやばいという機運が高まっている状況でもあります。つまり、コード自体の質だったり性能だったり、非機能における改善も腰を据えて取り組まねばならないという課題認識でもあります。
そのような状況とプロジェクト進捗の歩調が揃ったこともまた、推進の原動力になりました。この場を借りてお礼申し上げたいと思います。
おわりに
オムニバス形式で執筆したこともあり、少し長い文章となってしまいました。ここまでお読みいただきありがとうございます。
この四半期で一気に進んだテーマもあり、感慨深い一方でアプローチできているのは氷山の一角だと思っています。
リファクタリング・リアーキテクチャ・設計は泥臭く手を動かしながらも中長期的な絵をイメージしてソフトウェアを良くしていける、興味深い・意義深い領域です。
本記事をきっかけに、hacomonoのリアーキテクチャ・イネーブルメント部の取り組みに少しでも興味を持っていただけましたら幸いです。
(2024/8/19追記)
この記事の元となったFindy社主催「開発生産性Conference 2024」でのセッションがYoutubeで公開されました。こちらもぜひご覧ください。
www.youtube.com
株式会社hacomonoでは一緒に働く仲間を募集しています。
採用情報や採用ウィッシュリストもぜひご覧ください!