10xな開発を支えるCustom Lint Rule - スケーラブルで効率的な開発を目指して

はじめに

こんにちは! モジュール開発部の yamakazu (@yamarkz) です。

チームのサイドプロジェクトでLint Ruleの整備を進めてきており、メインプロジェクトを進める傍ら、隙間時間を捻出してはLint Ruleを作っています。

興味本位で始めたLint Rule作りだったのですが、これが思っていた以上に面白く、面白さにハマった勢いで1ヶ月で20以上のLint Ruleを作っていました。

今回はそんな勢いを持って整備を進めていたLint Rule整備の話を取り上げて、そもそもなぜLint Rule整備を始めているのか?Lint Ruleで何を解決したいのか?どういったLint Ruleを作っているのか?を紹介します。

目次

話の全体整理

早速内容に進む前に、話の全体像を捉えると以下のような構成です。

話の全体像

なぜLintを活用していくのか

Lintを活用する理由は、開発にかかる認知負荷を下げる というイシューを解決するためです。

その背景には、2つの状況と顕在化した3つの状態がありました。

状況1. プロダクトと組織の拡大 (未知の増加) :

Stailerは開発着手から丸3年が経過し、非常に大きなプロダクトに成長しました。

初期のネイティブアプリだけを提供する形から、小売のEC事業立ち上げに必要な全ての機能を備えたプロダクトに進化し、組織も10名から100名へと10xしています。

組織の変化

この規模にまで成長すると顕在化する負の現象があります。それは未知の増加です。

組織拡大に合わせて多くの人が関係を持ち、多くの物事が並列で進むようになり、未知な事象が爆発的に増えていきます。組織を大きくする限り、この現象はどうやっても避けられないことで、組織に関わる全ての人が向き合わなければいけないことでしょう。

ことプロダクト開発においては、日々コミットされる成果の絶対量が増えていきます。知らぬ間に大きな変更が加わっていくようになり、変更内容の確認や表現意図を推論しながら理解し、自身の開発に関係することを汲み取りながら開発を推進することが求められてきます。

コードコミット量の推移

状況2. 多様な表現での実装 (注意の増加 / 曖昧さの増加) :

Stailerの開発は自由と裁量を持って進められてきました。

これは強いオーナーシップと推進力を持つメンバーが初期から揃っていたが故に選択できた開発スタイルです。上手く活用できていたと思いますし、活用していたからこそ今のプロダクト規模に3年という短い期間で進んでこれたとも思っています。このスタイルでなければ発明できない機能、生み出せない特性がありました。

初期はポジティブな面が大きかった一方で、プロダクトが成長するにつれてネガティブな面も大きくなってきました。その1つが実装表現の多様性が高くなることです。

表現選択の自由を許容することは実装者目線で短期的に最適化しやすくなり、結果として新たな発明や開発速度が生まれやすくなります。

このスタイルによるアプローチの結果を、読み解く側の目線に立ち、中長期に運用し続けるという前提で捉え直すと、多様な表現を正しく読み解くために多くの注意が必要になることや、表現選択基準が曖昧な中で正しい判断を下すために多くの確認が必要になるといった、ネガティブな影響が強くなります。

例えば、以下2つのAssertionは表現が異なりますが、どちらもテストコードとしては成立しています。

// predicateを使ったケース
completion(predicate<ReminderTask>(
 (task) => task.status == ReminderTaskStatus.failed),
)

// TypeMatcherを使ったケース
completion(isA<ReminderTask>().having(
  (e) => e.status,
  'status',
  equals(ReminderTaskStatus.failed),
)),

上記の表現の違いにはメッセージや意図はありません。どちらを選択してもテストとしては問題ないです。強いて言えばTypeMatcherを使う方がMatcherの使い方としては正しいので、TypeMatcherを使ったケースを選択するのが望ましいでしょう。

小さな例ですがこういった表現の違いが複数ファイルに入り乱れて存在していた場合、新たに変更加える人の目線では読み解く際に混乱が生まれ、統率が取れていれば不要であった確認と推論が必要になってきます。


2つの状況 (1.プロダクトと組織の拡大 / 2.多様な表現での実装) に共通して言えることは、物事を理解するために確認と推論を必要とする頻度と量が増えているということです。これは社内の業務全てに共通していることですが、プロダクト開発においても定性的な感覚として表れていると感じます。

課題感を定量的に裏付けるのならFour Keysのようなフレームで測る必要がありますが、自分たちはまだそういったプラクティスを導入していない(できていない)のと、特殊な開発ライフサイクルの構造上、現時点で妥当性ある包括的なパフォーマンスの計測が難しいため、今回は定性的な課題感を頼りにイシューを見極めていきました。

イシューの見極め :

状況を認識し、特徴を分析すると解くべきイシューが見えてきます。それが先に述べた 開発にかかる認知負荷を下げる というイシューです。

イシューの解決対象である「認知負荷」は3つの負の状態変数で構成されます。

認知負荷が高い状態

  1. (注意) 多くの注意を向けなければ、実装意図を把握できない
  2. (未知) 多くの未知な仕様と考慮の発生により、都度確認しなければ理解できない
  3. (曖昧) 多様な表現の共存により、解決手段の選択判断を下すのが難しい

認知負荷は注意, 未知, 曖昧という思考の抑制要素から成り立っており、それらの増加に比例して課題解決に必要な確認と推論の数も増加する特徴があると考えました。それらは結果として、開発全体のアジリティやスループットが低下することに繋がると整理します。

この整理を踏まえると「状況を打開するには、各抑制要素に対応した策を打つことで解決に至れる」という仮説がつくれます。

解決策は無数に考えられます。王道的なのは組織構造を再編し小規模で開発を進められるようにすることや、機能をモジュール単位に切り分けるなどでしょうか。リファクタリングも鉄板の案として挙がります。

色々と選択肢は考えられる中で、今回取り上げるのがLintです。

Lintを活用することが、先の認知負荷が高い状態を改善するのではないかという仮説を持ち、狙いと期待を持ってLint Ruleの整備を始めました。

Lintの効果とトレードオフ

Lintによって得られる最大の効果は「認知負荷を減らすこと」です。 そしてそれは3つの特性を獲得することで、結果的に得られるものと考えます。

Lintによる効果 : 認知負荷を減らす

  1. 表現を統一することで、読み手が理解に必要とする注意を減らす (一貫性 / 可読性)
  2. 表現を統一することで、読み手の理解を抑制する未知を減らす (理解容易性)
  3. 推奨表現を提示することで、書き手が下す実装判断の迷い (曖昧さ) を減らす (判断容易性)

整理すると先の3つの状態変数とうまくマッピングされていることに気づきませんか?

これは意図的にマッピングしていますが、論理整合の観点で捉えても一定納得感が持てる整理になっていると思います。それはつまり、定義した課題に対して効果的な手段であるという仮説の証明に、期待が持てそうだということです。もちろん全てがストレートに解決できるとは考えていません。”一定の効果が見込めそう ”という期待値です。

トレードオフとして失うものもあります。それは、柔軟性、整合性担保、時間です。

トレードオフとして失うもの

  1. 表現の自由 (柔軟性)
  2. 想定外なエッジケースへの対応 (整合性担保)
  3. 整備に必要となるリソース (人と時間)

先のイシュー整理の話でも触れたように、現在はトレードオフで失うもの以上に、認知負荷を減らすというイシューの方が重要であるため、失っても良いと判断できました。

見方を逆転させれば、ここで挙げたトレードオフで失うものを従来は前向きに得ていたが故に現在の状態に至ったとも捉えられます。

表現が自由だったから発明が生まれた。エッジケースに対応できたから成長してこれた。柔軟にリソースを調整していたから最適化できた。

つまりは、全てはトレードオフなのでしょう。

どのようにLint Rule対象を見極めていくのか

Lintの活用がイシューの解決に効果的なアプローチであるという仮説が作れました。

では具体的に整備するLint Ruleは何を考え、どのように定義していくと良いのでしょうか?

これまでLint Ruleを作ってきた経験を振り返ると、Lint Ruleは2つのケースで課題解決に向かうことに気づきました。

1. あるべき理想状態が明確に見えている

1つ目が、あるべき理想が明確に見えているケース。

例えば、”モジュール間の依存関係をシステマチックに制御したい” など。

これは他社の事例でもよく見聞きします。

Lint Ruleを考える前から向かいたいあるべき理想状態が明確に定まっているので、現状を理想にどう合わせにいくのか?という問いから考え、差分を埋めるのに必要なフィードバックをかけるLint Ruleを作ります。

10Xではちょっとした雑談から新しい開発が始まります

2. より良い表現に統一していく

2つ目は、より良い表現に統一していくケース。

例えば、類似課題の解決にAとBの2つの表現が考えられ、どちらも解決に至れるが、BがAの後に発明された表現で簡潔な記述になっているとします。自由な開発であれば、課題解決に至れているので細かな修正を要求せずにコミットを許すでしょう。

しかしそれをやめて統治していくならば、簡潔さのあるBの表現を選択するようにフィードバックをかけるLint Ruleをつくります。

細かな表現の推奨も決めていく

どういったLint Ruleを整備してきたのか

ここまでのイシュー整理や考え方の話を踏まえて、10X内で実際に整備したLint Ruleをいくつか実践例として紹介します。

optional positional parameterの使用を禁止するLint Rule

// DON'T
Entry _toEntry(
  Stock stock,
  DateTime dateTime, [
  int? price, // このようなPositional宣言は禁止
]) {}

// DO
Entry _toEntry(
  Stock stock,
  DateTime dateTime, {
  int? price, // Namedな宣言を使用する
}) {}

Optional Positional Parameterの使用を禁止しました。

言語の表現として柔軟性を持たせる表現ですが、安全性の観点から、Named Parameterに寄せる判断をしています。

雑談からルールが決まるシーン

クラス, 変数, 関数の定義位置を指定するLint Rule

// DON'T

// void main() {} よりも前に宣言するのはNG
final shopId = '123';
final shop = Shop(id: shopId);

void setUpMock() {
}

class TestCase {}

void main() {
}

// DO

// トップレベルでは必ずmainを最上位で宣言する
void main() {
}

final shopId = '123';
final shop = Shop(id: shopId);

void setUpMock() {
}

class TestCase {}

テストファイルではクラス, 変数, 関数の定義位置を厳密にしました。

読み解き方がパターン化されるので、一定の可読性が保証されることや、定義位置が明確になり新しいメンバーが実装する際の迷いが減ることを期待しています。

Kotlinのスタイルガイドではクラスレイアウトの位置が決まっているようで、アナロジーとして参考になりました。

Matcherの使用を統一するLint Rule

// DON'T
await expectLater(
  stockService.registerNewStock(
    shopId: shopId,
    stock: stock,
  ),
  throwsA(isException), // エラーのスローを確認するだけのケース
);

// DO
await expectLater(
  stockService.registerNewStock(
    shopId: shopId,
    stock: stock,
  ),
  throwsException, // ビルトインで存在するMatcherを使うようにする
);

多様なMatcherの使用に制約を設けて、統一された表現でテストコードを記述することを促しています。

例として挙げているのは、例外エラーがスローされた場合を検証するコードです。

DON’Tの記述でもテストとして機能していますが、簡潔な表現を促すMatcherがビルトインで用意されているのなら、そちらを使いましょうというフィードバックをかけます。

groupの階層を2階層以下に限定するLint Rule

// DON'T
group('listDeliveryTimes', () {
  group('siteController', () {
    group('パートナーA', () {
      group('店舗の配達日時を取得する', () {});
      group('...', () {}); // 階層が深くなると、スクロールしなければ文脈を読み取れなくなる
      group('...', () {});
    });
  });
});

// DO
group('ShopService#listDeliveryTimes(配達日時の一覧を取得する)', () {
  group('パートナーA(SiteController)', () { // 2階層目までであれば許容範囲
    test('正常系 担当店舗が配達可能状態の場合、配達日時が取得できる', () async {
      // ...
    });
  });
});

テストコードをグルーピングして、論理的な境界を敷く表現がDartにはあります。

このgroup表現は便利ですが、乱用することもできるため、可読性を守るという意図で階層を2階層までしか定義できないようにしました。

あらかじめこれが守られていれば、2階層の表現でどうテストを記述していくかを考えていけるようになります。

まとめ : 手応えと推進タイミング

Lint Ruleを整備することで徐々に認知負荷が減らせていると感じており、先に立てた仮説は正しかったという手応えも生まれてきました。

取り組みの効果は即効性があるものでもないので、周囲に効果が浸透してフィードバックをもらえるまでには2ヶ月 ~ 3ヶ月ほど時間が必要になりそうな気がしています。もう少し時間が経ったら組織内でヒアリングを試みようと思っています。

自分たちは課題が顕在化したタイミングで整備を始めましたが、振り返るとベストなタイミングでした。

もう少し早くても良かったかもしれませんが、初期からやるべきことだったとは思いません。(やれたらやった方が良いものではある)

組織やプロダクトの存続可能性に価値を置く段階から、継続可能性に価値を置く段階に切り替わった後にLint Ruleを整備して、成果の標準化を図っていくのがベターなのではないかと、個人的には思います。

なので、中長期の運用を見据える段階にいる方でLint Ruleの整備を検討されていれば、「ぜひ取り組んでみてほしい」と背中を押すコメントを残しておきます。

最後に

10XでのLintとLint Ruleの活用例を紹介してきました。

勢いで開発を進めているLint Ruleですが、その背後では現状分析やWhy/Whatを考え、明確な狙いと期待を持って取り組んでいます。ここでの内容が、Lint Ruleの活用を前向きに検討する方の背中を押すきっかけになれれば嬉しいです。

10Xではリアルの課題解決はもちろん、中長期を見据えた開発課題の解決に取り組む機会も生まれてきています。

そういった機会で腕を振るいたいという方はぜひ10Xへ。

一緒に良いプロダクト開発を支える基盤を作りましょう。

open.talentio.com

参考