メンテナンスコストの低いソフトウェア開発

目標

  • 技術的負債が存在しないこと。
  • ソフトウェアの設計が理解しやすく、修正が必要な際に対応するコードを容易に見つけられること。
  • 保守性を最優先事項とし、開発者がその存在を数年間忘れていた後でも、問題なく改善を継続できること。
  • 環境の変化によって陳腐化するか、新たな要件が発生するまで、ソフトウェアが実質的な完成状態を維持し、満足のいく動作を続けること。
  • メジャーバージョンが同一の依存関係を、自信を持ってアップグレードできること。
  • ソフトウェアが廃止される際、既存のシステムから円滑に削除できること。

原則

「ただより高いものはない(無料の昼食はない)」という言葉を常に心に留めておくべきです。何もしないまま、メンテナンスコストの低いソフトウェアプロジェクトを手に入れることは不可能です。低メンテナンスコストなソフトウェア開発を実現する鍵は、ミスを減らすことではなく、あらゆる小さな事柄を可能な限り正しく行うことにあります。これは間違いなく、極めて難易度が高いことです。
以下に、低メンテナンスコストな開発を実現するための一般的な原則をまとめます。
  • すべての非必須機能を削ぎ落とすこと。
  • エコシステムが不健全な言語やツールチェーンの使用を止めること。
  • 表現力の高い言語を採用し、コーディングにおいてその言語の一般的なイディオムから逸脱したり、不自然に捻じ曲げたりしないこと。
  • コードの悪臭(コードスメル)を生み出す可能性のある目的でマクロを使用しないこと。
  • 型定義または型アノテーションを持つ言語のみを使用すること。
  • 時間の試練に耐えてきた技術を可能な限り使用すること。それらの技術は、今後も長く生き残り続ける可能性が高いためです。
  • 大局的に問題を捉え、場当たり的な「戦術的プログラミング」を避けること。
  • ソフトウェアのエントロピーを下げるために、大幅な行数変更を許容し、積極的にリファクタリングを行うこと。
  • 複雑さを利用者に委譲することで、過剰な設計を削ぎ落とすこと。人間味のある(過度に親切な)設計ほど、最も保守が困難になりがちだからです。現実には、これに類する例がすでに多く存在します。MarkdownエディタとWYSIWYGエディタを比較してみましょう。前者はユーザーに(ごくわずかな)学習コストを要求しますが、後者には学習コストがほぼありません。しかし、保守性の観点では、後者は前者よりもはるかに劣っています。
    • Markdownエディタは、構造が明確で人間が読めるプレーンテキストファイルを容易に作成できます。一方、WYSIWYGエディタが生成するものは、移行が困難な独自の内部ドキュメントモデルか、完全に混沌としたHTMLのどちらかです。
    • Markdownドキュメントは2050年になっても利用可能であり、Markdownエディタは多くの場合ミニマルで代替可能です。WYSIWYGエディタは陳腐化しやすく、下位互換性の維持が困難で、開発者にとっても保守が極めて難しくなります。また、そのベンダーは2050年にはすでに存在していない可能性が高いでしょう。
  • ベンダーロックインにつながる大規模なソフトウェアフレームワークの使用を止めること。ソフトウェアフレームワークがあらゆる環境に適応しようとしたり、ワンストップソリューションを提供しようとし始めたとき、それはもはやフレームワークではなく、時限爆弾です。
  • 低品質なパッケージへの依存を極力減らすこと。これは、依存関係のコード品質をレビューすることを意味します。幸いなことに、粗悪なコードスタイルは見分けるのが容易であり、パッケージの内部動作を完全に理解せずとも判断できる場合が多いです。
  • 下位互換性のないプラットフォーム向けのソフトウェアは作成しないこと。
  • 単一の機能に特化したソフトウェアでない限り、安易なクロスプラットフォーム化は避けること。
  • クラウドプラットフォームが提供するプロプライエタリなサービスの使用を禁止すること。
  • 間接的な依存関係(子依存関係)のバージョンをロックできないパッケージマネージャの使用を禁止すること。また、Gitリポジトリをパッケージのソースとして直接指定することを禁止すること。
  • 必要不可欠でない限り、既存の標準規格やプロトコルに無理に迎合しないこと。
    • 標準やプロトコルの大半は粗悪なものであり、その多くは参考にする価値すらありません。それらに迎合することは、ソフトウェアの発展の足を引っ張るだけです。例えば、それらの標準やプロトコルでは実現できない要件に直面し、身動きが取れなくなるような不運に見舞われる可能性があります。
    • 無視できないほど普及している標準やプロトコルは、その人気の大部分が先行者利益と下位互換性の保証に由来しています。その結果、本番環境における実装は奇妙なエッジケースで溢れかえっており、それらを収集し対処する作業は開発者を疲弊させます。
  • ツールチェーンの抽象化レベルに着目し、移行やアップグレードが困難なツールチェーンの使用を止めること。
  • 依存関係のアップグレードに対する安全性を高めるため、大量の自動統合テストを作成すること。
  • ソフトウェアの実行環境は、他のプロジェクトと共有するのではなく、専用かつ隔離されたものであるべきです。
  • 技術の内製化・自立性を重視し、外部要因に左右される依存関係を可能な限り切り離すこと。
  • コア開発者以外の参加人数を可能な限り抑え、共同作業に伴う摩擦がプロジェクトの開発効率を損なうのを防ぐこと。これは交通渋滞と同じであり、最善の解決策は道路を広げることではなく、同時に走行する車の数を減らすことです。

車輪の再発明

車輪の再発明はアンチパターンとみなされます。なぜなら、再利用できる「すでに発明された車輪」が、常に1つあるいは複数存在しているからです。しかし、既存の車輪を再利用することは、しばしば依存関係管理の問題を引き起こします。最初は順調に進んでも、すぐに次から次へと問題が発生するのです。最も安易な道は、往々にして技術的負債を最大化する道です。そのような道を進んでいると、頻繁に挫折感を味わうことになります。長い時間を経るうちに、どれほど自信や情熱を持った人間であっても学習性無力感に陥り、ソフトウェア開発を「終わりが見えない過酷な苦役」だと捉えるようになってしまいます。
依存関係を導入する価値があるかどうかを見極めることは、見落とされがちですが極めて重要な能力であり、その裏付けには言語化しがたい大量の経験が必要です。この能力が欠けていると、簡単に騙され、将来的な被害者になるだけでなく、加害者の片棒を担ぐことにもなりかねません。大多数の開発者がこの能力を欠いているため、業界内では悪貨が良貨を駆逐する現象がごく一般的に起きています。
一定の規模に達したオープンソースソフトウェアプロジェクトは、容易に「クソコードの山(ゴミ屋敷)」と化します。
  • 誤った抽象化、劣悪なアーキテクチャ、肥大化した恐ろしいAPIサーフェス。
  • 信頼に足るテストの欠如。
  • 積極的に保守されておらず、実態と乖離していたり、有用な詳細を覆い隠したり無視したりしているドキュメント。
  • 多すぎる戦術的コーディングと、少なすぎる戦略的コーディング。削減されるコード量よりも、常に追加されるコード量のほうが多い状態。
  • 存在しない要件に過剰な時間を投じた結果としての、ソフトウェアの肥大化と腐敗。
  • ……
コミュニティ内に、戦略的な視点から品質を管理し、厳格なコードレビューを実施できる権威あるメンテナが存在しない限り、貢献者のスキルがどれほど高かろうと、オープンソースの参加モデルそのものが本質的にこのようなクソコードの山を生み出してしまいます。プロプライエタリなソフトウェアも同様の問題を抱えていますが、非公開であるがゆえに問題がさらに隠蔽されているに過ぎません。問題を絨毯の下に掃き込んでも、問題が消えたわけではないのです。
技術的負債から解放されるために、時にはあえてNIH(Not Invented Here)症候群の帽子をかぶる必要があります。車輪を再発明することで、劣悪なサードパーティ製依存関係から脱却できるだけでなく、技術的負債にまみれた開発者たちが決して知ることのないメリットが得られることに気づくでしょう。
  • より優れた車輪を発明できる可能性があること。
  • 解決策を自ら生み出す能力がますます向上すること。
  • 依存関係の良し悪しを識別する能力がさらに磨かれること。
低メンテナンスコストを目標に据えている場合、車輪を再発明することの恩恵は、将来さらに拡大していく可能性があります。負債を捨て去れば、あとは利益(利息)だけが残る――極めてシンプルな道理です。時が経つにつれ、こうした正のフィードバックが積み重なり、車輪の再発明という選択肢はあなたの中でより重要な位置を占めるようになるでしょう。ただし、車輪を再発明する前には、常にその経済性を慎重に見極める必要があります。すべてを一から再発明することは依然として推奨されません。再発明の費用対効果が極めて低く、開発失敗のリスクが高い車輪も明らかに存在するため、そうしたものには注意を払う必要があります。

バッテリー型依存関係の構築

一部のプログラミング言語やフレームワークは、「バッテリー同梱(batteries included)」を謳っています。これは、すぐに使える高度な機能が組み込まれていることを意味します。これらの「バッテリー」の本質は、低レベルな詳細の露出を徹底的に避け、比較的安定した高水準APIを提供することにあります。適切な抽象化とテストにより、破壊的変更が発生することは稀です。
この「バッテリー」の概念は、依存関係の管理にもそのまま適用できます。いわば「バッテリー型依存関係」を構築するのです。この依存関係は、実際に依存関係を使用するプロジェクトと、外部依存関係本体との間の「中間層」として機能します。これは外部依存関係をベースにしつつ、整然としたAPIセットを提供するものです。これにより、巨大な引数や大量のパラメータを持つ、野蛮人が作ったかのような関数やクラスを、ビジネスロジックが直接使用することを回避できます。
利用するのはその依存関係のうち必要な一部分だけであるため、この中間層に独自のテストコードを記述しておくことで、依存関係のバージョンが更新された際にも期待通りの動作が維持されているかを検証できます。将来的にその依存関係が破綻した場合には、古いバージョンを使い続けるか、別の依存関係に差し替えるか、あるいは自前で実装するかを選択できます。事前に作成したテストは、リファクタリング時の正当性を保証するために機能します。これこそが、自作したバッテリーと他人が作ったバッテリーの決定的な違いです。自作のバッテリーであればいつでも充電や容量拡張ができますが、他人のバッテリーは、徐々に電力が切れていくのをただ眺めることしかできないのが普通です。
形式的な類似性から、バッテリー型依存関係とカプセル化されたビジネスロジックが混同されることがあります。両者の主な違いは、依存関係からの「距離」にあります。バッテリー型依存関係は通常、依存関係のすぐ近くに位置しますが、ビジネスロジックは依存関係から遠く離れた場所に位置します。ビジネスロジックが関連する依存関係の上に直接構築されているようなごく稀なケースを除き、両者が同一になることはありません。
バッテリー型依存関係は、依存関係管理の問題を解決する聖杯(特効薬)のように見えますが、依然としていくつかの課題が存在します。
  • あくまで余分な中間層であるため、新機能が必要になった場合や、破壊的変更を行う際には、依存関係を直接使用するよりも手間がかかります。
  • 正しい抽象化を構築することは困難です。適切な抽象化を模索する過程において、時にはゼロからの作り直しが避けられないこともあります。また、人間は将来の要件に対する予見性に欠けがちであるため、一時的には正しかった抽象化が、将来的には不適切になることもあります。誤った抽象化によってプロジェクトが修正の泥沼に陥ることを恐れるあまり、人々はより大きな混乱の渦に自らを巻き込むことを選びがちです。少なくとも、問題が起きた際に他人のせいにするほうが常に楽だからであり、この心理を否定することは困難です。

小さなパッケージの使用

小さなパッケージは、Unix哲学とセマンティックバージョニング(SemVer)が共鳴した体現です。パッケージの粒度が小さければ小さいほど、単一のパッケージが持つ機能は少なくなります。機能が少なければ安定性は高まり、コードを変更した際に壊れる箇所も少なくなります。
しかし、小さなパッケージは時として新たな問題を引き起こすことがあります。例えば、パッケージ数の急増に伴う管理コストの肥大化です。こうした問題は、日々の蓄積とともに力技での解決が困難になっていくため、非常に厄介です。最終的には、この傾向に対処するためにFacebookのcodemodのような専用の解決策が生み出されることになりますが、それらを使用すること自体も依然として煩雑です。

可読性とパフォーマンスの二者択一では、可読性を選択する

実際のコーディングにおいて、可読性とパフォーマンスのどちらを優先すべきか揺れ動く場面に遭遇することがあります。その際、開発者は一時の近視眼的な判断から、可読性ではなくパフォーマンスを選んでしまいがちです。パフォーマンスを選択することには、通常それほど強力な根拠はありません。可読性を犠牲にしてパフォーマンスを得るトレードオフは、後から振り返ると割に合わないことがほとんどです。場合によっては、開発者がパフォーマンスを選ぶ理由が、「ハイパフォーマンスなコードを書くことで自分が賢く見える」という幼稚な精神的報酬に過ぎないことすらあります。
可読性とパフォーマンスの間で判断に迷ったときは、常に可読性を選択すべきです。この点は、どれほど強調してもしすぎることはありません。そのコードが一度きりしか書かれず、生涯二度と変更されないと確信できる場合でも、可読性を選択してください。可読性の高いコードのほうが1000倍遅い場合でも、可読性を選択してください。もし実際に観測可能なレベルでパフォーマンスに悪影響が出た場合は、もう一つ別のコードを作成すればよいのです。そのコードは、可読性の高いコードを変換したハイパフォーマンス版とし、それがどのようにして生成されたのかを説明する非常に詳細なドキュメントをすぐ隣に添える必要があります。
例外となるのは、最初からハイパフォーマンスなプログラムを書くことが目的である場合です。最初からのゴールが高性能である以上、この場合は「可読性」という選択肢そのものが消滅します。しかし、ハイパフォーマンスなプログラムであっても、コードの可読性を向上させる余地は依然として存在します。ただ、そうした機会に気づくのが極めて難しいだけです。往々にして、二度目にコードを書くときに初めて気づくものです。そして、可読性を向上させる機会に一度気づいたならば、それらを無視し続ける理由はありません。パフォーマンスを維持したまま、可能な限り可読性を高めるべきです。

保守を必要としないソフトウェアを愛でる

ソフトウェアの「保守が不要であること(無保守性)」を、一つの美として愛でるべきです。ソフトウェアが「保守を必要としない時間」が長ければ長いほど、その無保守性は長期の試練に耐え抜き、蓄積される美もより深まります。
最も重要な点は、ソフトウェアが「保守できない」のでも「保守が困難」なのでもなく、「保守する必要がない」状態にあると明確に理解していることです。コードはそこに存在しますが、弄る必要はありません。なぜなら、要件を満たし続け、正常に機能し、極めて見事に動作し続けているからです。
仮に保守が必要になったとしても、その摩擦は障子紙のように、指先で軽く突くだけで取り除ける程度であるべきです。技術的負債がないため、利息を支払う必要もありません。強迫観念に駆られて依存関係をアップグレードしたくなったとしても、問題ありません。そのアップグレードは10分以内に完了しますし、あなたには浪費できる10分がいくらでもあるからです。しかし、それよりも素晴らしいのは、自制心を保ち、その衝動を踏みとどまることです。それは新たな要件がまだ発生していないからというだけでなく、ソフトウェアが時間をかけて積み上げてきた美を損ないたくないからでもあります。
こうした鑑賞眼を養うことは、極めて直感に反するため非常に困難です。人間は無意識のうちに、物事の新規性を「良し悪しを測る基準」として誤認してしまいがちです。流行に追いつくことが、時には何よりも重要であるかのように思えます。しかし、少し立ち止まって考えれば、それが非合理的であることはすぐに分かります。日常生活を見渡すだけでも、良質な素材で作られ、成熟した技術によって製造された機器は、予想をはるかに超えて長持ちすることが分かります。24時間稼働し続ける冷蔵庫はその最も典型的な例であり、毎年新しい冷蔵庫に買い替える人などほとんどいません。デジタルな産物であるソフトウェアが、ハードウェアよりも早く陳腐化する道理はありません。アップデートが容易であることは、アップデートを行う正当な理由には決してなり得ないのです。