アーキテクチャの限界を漸進的に押し上げる取り組み

10X在籍8年目、取締役CTOのishkawaです。

この記事は10Xアドベントカレンダー2024の3日目の記事です。


メイン事業であるStailerは現在5年目です。

自分はプロダクト開発を統括する立場として、プロダクト戦略立てたり、開発プロセスの整備の旗振りをしたり、開発体制を調整したり、採用活動に奔走したりと、色々な取り組みをしてきました。そして今、何に注力しているかというと、コードの立て直しです。

なぜコードの立て直し?

プロダクトを5年くらい開発していると、開発速度は当初と比べて落ち着いてきます。その要因は色々あると思いますが、自分は「事業やプロダクトのフェーズの変化による規模や複雑度の変化」と「規模や複雑度に対する技術的な適応度」の2点に着目しました。

前者はプロダクトマネジメントのトピックです。Stailerでは、自分たちが持つ強み、組織の規模、得られる事業成果、運用にかかるコストなどを考えて、自分たちは何をやって何をやらないのかという指針を策定してきました。すべてが想定通りになった訳ではないですが、パートナー企業のネットスーパー/ネットドラッグ事業の成長を支えつつ、10Xとしても十分に開発と運用ができる形にできたと思います。

後者はエンジニアリングのトピックです。事業やプロダクトのフェーズの変化に伴って、システムの規模や複雑度が変化します。10Xの開発組織でもこの変化に対処する取り組みは続けており、この1-2年は保守性を最重要なものとして掲げ続けていますが、十分に追いついているとは言い切れない状況です。規模や複雑度に追いついていないというのは、技術的な要因によって開発を速くできていない、運用を楽にできていないといった状況を見て、そのように捉えています。

翻って経営の帽子で考えると、ネットスーパー/ネットドラッグ事業はもっと早く成長させたいし、新規領域への投資も拡げたいという状況です。その実現に向けて最初に対処しなければならないのが技術的課題なので、それに取り組み始めたという訳です。

後になってから壁に当たるのは何故か

最初は速くて後から遅くなったという状況ですが、これは後から入ったコードが特に悪いという訳ではなく、ある程度の規模や複雑度に達すると遅くならざるを得ない構造に最初からなっていたと分かりました。つまり、アーキテクチャが劣化したのではなく、アーキテクチャの限界に達したということです。

最初からもっと上手くやれたら良かったと思いますし、限界の到達を遅らせる余地はあったと思い当たる節はいくつもあるのですが、今のプロダクトは今の実装で社会に価値を生み出しています。悔やむだけではアーキテクチャが改善されることもないので、これからの規模や複雑度を見据えて、どうやってなりたい状態に近づけるかを今は考えています。

アーキテクチャの限界を漸進的に押し上げる

以下の手順を繰り返して、やっていこうとしています。

  • 欲しい特性を狙ってアーキテクチャ決定を積み重ねる: 現状のStailerの開発には規定が少なく、自由度が高い状態にある。組織にとって望ましい特性がこのまま自然発生する訳もないので、今の組織に必要な特性は何か特定し、その特性を得られそうなアーキテクチャ決定を積み重ねる。
  • ADRでアーキテクチャ決定の意図を明確にする: アーキテクチャ決定は意図を持ってなされるが、その意図が正しく認識されないと、決定自体が無視されたり、不健全な形でバイパスされることがある。とはいえ、当該のアーキテクチャ決定を理解しなければならないタイミングは人によって異なるので、誰もがその決定を追えるようにするためにADR(Architecture Decision Record)を残す。
  • linterでADRに辿り着けるようにする: ADRがあったとしても、メンバーがその存在に気付けなければ意味がない。適切なタイミングでメンバーが気付けるようにするため、ADRの決定に反するコードを検知するlinterをADRとセットで実装する。
  • ADRへの違反状況を可視化して漸進的に適応度を上げる: ADR + linterの作成時に、すべての既存コードをアーキテクチャ決定に沿わせることは難しいので、当初は違反箇所をlintの対象から除外することを許容する。lintの除外数はADRへの適応度を表すので、それを可視化して漸進的に適応度を上げていって、最終的に得たかった特性が広く得られるようにする。

linterによる検知はエディタ上では、以下のスクリーンショットのようになります。

「データレイヤーをUIレイヤーに依存させない」という決定への違反を検知してエディタ上のエラーとし、具体的に何が違反なのか、違反をどのように解消して欲しいかを表示しています。ルール名のところをクリックするとADRが表示され、どういう背景で「データレイヤーをUIレイヤーに依存させない」という決定をしたのか知ることができます。

この仕組みはDartのcustom_lintを使っていて、以下のように簡単に実現できます。

class NoUiLayerImportFromDataLayer extends DartLintRule {
  static const _code = LintCode(
      name: 'no_ui_layer_import_from_data_layer',
      problemMessage: 'データレイヤーからUIレイヤーのファイルはインポートできません。',
      errorSeverity: ErrorSeverity.ERROR,
      url: 'https://example.com/path/to/adr');

  @override
  List<String> get filesToAnalyze => const ['/**/data/**.dart'];

  @override
  void run(
    CustomLintResolver resolver,
    ErrorReporter reporter,
    CustomLintContext context,
  ) {
    context.registry.addImportDirective((node) {
      final uri = node.uri.stringValue;
      if (uri != null && uri.contains('/ui/')) {
        reporter.reportErrorForNode(code, node);
      }
    });
  }
}

平凡な決定も浸透させて価値を高める

アーキテクチャ決定という大層な雰囲気ですが、実際には平凡なものです。例えば、「レイヤーAはレイヤーBに依存させない」とか、「Xという責務はレイヤーAに位置付ける」とか、「レイヤーBのオブジェクトはimmutableでなければならない」とか、そういうものです。

1つ1つが平凡な決定であっても、ある程度以上の規模のコードベースではそれが浸透していることが大きな価値になります。自分自身、これを体感し始めたという段階ではありますが、浸透が進めばもっと上手く開発やテストができるようになりそうだという期待感もあります。

意気込み

今あらためてエンジニアリングにコミットすることで、会社がネットスーパー/ネットドラッグ事業をより早く成長させ、新規領域への投資も拡げられる状態を作りたいと意気込んでいます。

10Xではソフトウェアエンジニアも募集しているので、面白そうだなと思った方は、ぜひ採用ページもご覧ください。カジュアル面談のURLも用意したので、まずは話を聞いてみたいという方はそちらもどうぞ。


明日はビジネス本部の鈴木さんが記事を公開する予定です。お楽しみに!