mtx2s’s blog

エンジニアリングをエンジニアリングする。

マルチバリューストリームというアンチパターン

「保守や運用を考慮した開発」とは言うが、それが意図するところは思うより広い。ソフトウェアエンジニアとしてのその理解は、あくまでもソフトウェアエンジニアリングに閉じたものとなってしまいがちだ。

変更しやすいコードや、システムの安定した稼働は、無論、大切だが、バリューストリーム(value stream)という観点が抜け落ちることがある。そのペナルティとしてチーム間が密結合となってコミュニケーションコストが増大し、テスト容易性デプロイ容易性を著しく損なう結果を招いたプロジェクトもある。

私が聞いた過去のあるケースでは、複数のアプリケーション間で一部の機能の共通化を進めたことでこのペナルティを支払った。何が問題だったのか。どうすべきだったのだろうか。

そうなったいきさつ

通化された機能はもともと複数のアプリケーションそれぞれで実装されていた。その機能は、ユーザインタフェースを含め、いずれのアプリケーションもほぼ同じものだった。「ほぼ同じ」なので、わずかな差はある。それがユーザー体験に差を生じさせることが社内で問題視され、互いにユーザインタフェースを揃えることが急務とされていた。

問題視されたのはユーザー体験の差だけではない。ほぼ同じ機能の開発・改善を続けるために、それぞれでエンジニアが稼働するという重複も避けたかった。エンジニアの人数は限られている。競争が苛烈なマーケットで勝ち抜くには、注力すべき領域にエンジニアリングパワーを集中させなければならない。それを削ぐようなムダは少しでも排除する。それが、経営リーダーの意志だった。

こういった背景から方針として打ち出されたのが共通化だった。件の機能を共有コンポーネントとして切り出して各アプリケーションに組み込む、というものだ。

通化という方式は、実行可能で現実的な判断だった。アプリケーションは、それぞれが異なるビジネス向けに、各々専任のチームによって開発が続けられているが、ウェブアプリケーションという点で共通している。同じ会社の中なので、そこで採用されている技術要素も大差はない。また、各アプリケーションチームがそれぞれで開発していたことからも分かるように、技術的な専門性を要する類のものでもない。

こうして、経営リーダーから号令が発せられ、共通化はビジネス、チームを横断して取り組むプロジェクトとなった。

そしてマルチバリューストリームが発生した

完成した共有コンポーネントによって、ユーザインタフェースは統一された。このコンポーネントひとつに変更を加えれば、全てのアプリケーションに反映できる。

一見すると大成功したかに見えるプロジェクトではあるが、新たに大きな問題を抱えることとなった。共有コンポーネントに対して日々発生する追加開発を通し、チームが互いに密結合になったのである。当然、リードタイムは著しく悪化した。

理由は明らかだった。共有コンポーネントチームが、アプリケーションごとに流れるバリューストリームそれぞれの対応に追われることとなったのだ。「マルチバリューストリーム」とでも言えば良いだろうか。

マルチバリューストリーム
マルチバリューストリーム

アプリケーションチームは、いわゆるストリームアラインドチーム(stream-aligned team)に位置づけされる存在だ。アプリケーションに対するユーザーのニーズや利用目的は、アプリケーションごとに異なる。それぞれにバリューストリームがあり、それを受けて仮説を立て、アプリケーションに変更を加えていく。

ここで問題になったのは、その過程で共有コンポーネントへの変更の必要性が生じることだ。それは即ち、共有コンポーネントに対する変更理由やタイミングが、アプリケーション側によってもたらされることを意味する。この状況が、閉鎖性共通の原則(CCP, the Common Closure Principle)に反することは明らかだ。コンポーネントを変更する理由は、複数あるべきではない。

共有コンポーネントも、それを利用する各アプリケーションも、それぞれが専任のチームによって開発が続けられている。CCPが守られなかったことで、共有コンポーネントに何か変更を加えようとする度に、これらのチームが集まって、仕様や開発、テスト期間、リリース日などについて話し合い、調整しなければならなくなった。

まるで玉突きのようだった。あるアプリケーションへの変更要求の範囲に、共有コンポーネントに対する機能追加・改善が含まれる。すると、共有コンポーネントを変更する影響が、他のアプリケーションにも波及していく。影響は限定的とは言え、チームが互いに密結合になり、共有コンポーネントの存在がビジネスの足かせになっていった。

更に根深い問題は、この状況にあっても、チームが互いに密結合にあることに気付くものがほぼいなかったことだ。むしろ、チーム間の連携をより強くすべきだと、コミュニケーションの回数・時間を増やし出したぐらいだ。共有コンポーネントの変更による影響で、仕様に齟齬の生じたアプリケーションが障害を起こすことが度々発生したからだ。

こうして各所でチーム間のコミュニケーションパスが増えていき、共有や調整を目的とするミーティングが頻繁に開かれ、コミュニケーションコストが組織の処理能力を減衰させる事態に陥っていった。

なにが起こったのだろうか

通化という判断は、間違いだったのだろうか。アーキテクチャの視点で見てみると、依存方向は、各アプリケーションから共有コンポーネントへの一方向だ。共有コンポーネントに対してアプリケーションが従属している。この関係においては、共有コンポーネントに対する変更が外部要因となって、従属アプリケーションを変更することはあり得る。

それだけに、責務を負うこととなった共有コンポーネントに対する変更は慎重にならざるを得ないのであるが、従属アプリケーションに対するバリューストリームが、共有コンポーネントを頻繁に変更するモチベーションになっている。つまり変更理由という視点では、アーキテクチャとは関係性が逆転し、各アプリケーションに対して共有コンポーネントが従属しているということになる。アーキテクチャだけでは読み取れない相互依存関係が形成されているようだ。

従属アプリケーション側の事情による変更
従属アプリケーション側の事情による変更

共有コンポーネントチームから見れば、アプリケーションチームは直接のユーザーだ。アプリケーションチームへの提供価値の拡大は、共有コンポーネントの変更理由となるため、この依存関係は正しく見えなくもない。

しかし本ケースでは、共有コンポーネントチームに対し、各アプリケーションチームからプッシュ型でダイレクトに変更要求が押し寄せるようになっている。それがマルチバリューストリームの正体だ。共有コンポーネントチームは、そうやって多方面から流れ込んでくる多くの変更要求の間で生じるコンフリクトやリリースタイミングの調整に追われ続けていた。

社外サービスやOSSライブラリに対して自社アプリケーションを依存させる場合は、こうならない。自社アプリケーションに対するバリューストリームが、社外サービスやOSSライブラリに流れ込まないからだ。会社の内と外という境界に立ちはだかる高い壁が流れをせき止める。

社内開発する共有コンポーネントにも、組織やチームの内と外という壁はあるが、その高さは社外とのそれに比べてあまりに低い。強引に流れを止めようとしても、社内のコンセンサスを得ることは困難だろう。不特定多数を利用者とするOSSや社外サービスと違い、特定少数を相手にするという点も、社内事情の影響を受けやすい理由だと考えられる。

内外を隔てる壁に頼らず、アーキテクチャをより明確・厳密に分離する必要があるのではないか。それが、共有コンポーネントに流れるマルチバリューストリームを分離し、単一のバリューストリームによる独立したプル型のソフトウェアデリバリを実現する道に繋がるのではないだろうか。

どうやって抜け出そうか

まずは問題の整理が必要だ。共有コンポーネントが範囲とするコンテキスト(bounded context)と、従属アプリケーションから流れ込むバリューストリームが扱う範囲の関係をベン図で描いてみると見えてくるものがあった。

共有コンポーネントのコンテキストとマルチバリューストリームの関係
共有コンポーネントのコンテキストとマルチバリューストリームの関係

共有コンポーネントのコンテキストの中で、いずれのバリューストリームとも重ならない領域(1)が、共有コンポーネント固有の領域と言える。逆に、バリューストリームが共有コンポーネントのコンテキストと重ならない領域(2, 3)は、それぞれのアプリケーションが扱うべき領域だ。残った2通りの重なり合う領域(4, 5)が、問題の発生源だろう。

まず、共有コンポーネントのコンテキストと、単一のバリューストリームが重なる領域(4)は、それぞれアプリケーション側のコンテキストであるべき可能性が高い。共有コンポーネントがそれを侵犯してしまったために、アプリケーションのバリューストリームが流れ込んでしまっていると考えられる。コンテキストの境界を最初から正確に見極めることなどできないので、これは仕方がない。共有コンポーネントのコンテキストから徐々に切り離していけば良さそうだ。

次に、共有コンポーネントのコンテキストと、複数のバリューストリームが重なる領域(5)は、どう考えるべきだろうか。ここは、共有コンポーネントに対する要求がコンフリクトしやすい箇所とも言える。アプリケーションごとにその使われ方、つまりユースケースやユーザーストーリーが異なるからだ。共有コンポーネントチームがその仲裁に入っているために、いつまで経ってもチーム間が密接に協力し合うコラボレーションモードのインタラクションが続いているのだろう。従属アプリケーションの数がさらに増えれば、コミュニケーションコストが増大することは目に見えている。これはまずい。

要求のコンフリクトが発生する箇所は、それぞれの要求に対する柔軟性が必要な箇所であるとも言える。設定によるカスタマイズ性や、イベントのハンドリング手段、プラグイン機構などを提供すれば、柔軟性を持たせることができる。こういった仕組みを提供することは、チーム間をX-as-a-Serviceモードのインタラクションに移行させ、コミュニケーションコストを下げることにもつながる。

いや、要求のコンフリクトが発生しないこともあり得る。むしろその方が多いのかもしれない。しかしこれを共有コンポーネントチームが一手に引き受けて開発する方式は、ボトルネックになりやすい。

それならば、要求を持つ従属アプリケーションチームが、共有コンポーネントに変更を加えても良いのではないか。コントリビューターとして実装し、コミッターたる共有コンポーネントチームにプルリクを送るという、OSS開発のようなチーム間インタラクションが実現できれば、負荷を分散できる。コントリビューター/コミッターモードでのインタラクションとでも言えば良いだろうか。

もちろん、要求はイシュー管理し、仕様や設計方針は、イシューやプルリクに対するコメントで議論する。共有コンポーネントに関するドキュメントを整備し、テストもデプロイも自動化しておくことが必須になるだろう。

マルチバリューストリームから解放されるために共有コンポーネントチームがやるべきことが見えてきた。

  • 共有コンポーネント独自のコンテキスト領域(1)の開発を進めていく
  • 共有コンポーネントのコンテキストと、単一のバリューストリームが重なる領域(4)を切り離していく
  • 共有コンポーネントのコンテキストと、複数のバリューストリームが重なる領域(5)を次のいずれかで対応する
    • 要求のコンフリクトが発生する箇所は、カスタマイズ可能にする
    • 要求のコンフリクトが発生しない箇所は、コミッターとして、アプリケーションチームからのプルリクを受けつける

これらの対応を進めるにあたり、アプリケーションチームも対となる対応が求められる。共有コンポーネントから切り離されたコンテキストをアプリケーションに組み込むことや、共有コンポーネントのカスタマイズを進めることなどだ。共有コンポーネントはバージョニングされているので、これらの対応の多くは、共有コンポーネントチームとアプリケーションチームの間で非同期に進められる。

そうは言っても、共有コンポーネントと従属アプリケーションの境界にこれだけの変更を加えるとなると、意図しないトラブルが発生し得る。共有コンポーネントチームはそれを不安に感じ、コンポーネントの変更にためらいを感じるかもしれない。

その不安を軽減するために、アプリケーションチームがテストコードを共有コンポーネントチームに提供しておくと良いだろう。共有コンポーネントに対する担当アプリケーションの要件を、テストコードに書いて引き渡し、共有コンポーネントのビルドパイプライン内でテストが実行されるようにしておく。そうすれば、トラブルを未然に防ぐことができる。マイクロサービスアーキテクチャで言うところのconsumer-driven contrat testingだ。

アプリケーションチームには他にもやるべきことがある。共有コンポーネントの変更に対する担当アプリケーション側での変更コストを最小限にとどめることだ。依存先の変更に影響を受けること自体は仕方のないことだが、それがアプリケーションの広範囲にわたるならアーキテクチャに問題がある。影響範囲を特定することが困難になり、修正箇所に抜け漏れが発生しかねない。

この問題は、依存性逆転の原則(DIP, Dependency Inversion Principle)に従うことで軽減できる。「抽象に依存せよ」というやつだ。それぞれのアプリケーション側で、自身が必要とする機能を抽象(インタフェース)として定義し、その詳細(実装)の中で共有コンポーネントに依存させる。こうすることで、アプリケーションの下位レベルが依存する共有コンポーネントの仕様が、上位レベルのコードを汚染することを防ぐ。共有コンポーネントに対する変更は、この詳細の中で制御可能になる。また、抽象があればテストダブルを導入できる。テストにかかるコストも軽減できるだろう。

依存性逆転の原則
依存性逆転の原則

結論は無い

やるべきことは見えたが、これはあくまでも机上の論だ。マルチバリューストリームで苦労していた他社事例を題材に(多少、肉付けしたが)、どう対処できるかを本エントリを書きながら考えてみたものだ。この事例がその後、現実世界でどうなったのかは聞いていない。

ここに書いたことを実際に実行しようとすると、より難解な問題が多くたちはだかると思う。現実問題として、そもそもこれらを進めていく間も、マルチバリューストリームは流れ続ける。共有コンポーネントチームがその対応に追われていては、状況はいつまで経っても変わらない。崩すべきはまずここからだろう。やり方はいくつか考えられるが、これ以上は論を重ねても実行性がなくなりそうだ。考えるのは、この辺りでやめておこう。