オブジェクト指向不要論を徹底検証!過剰な抽象化を避けてコードの可読性を劇的に高める設計手法

過剰な抽象化から脱却し、シンプルで読みやすいコード設計を目指すエンジニアの姿 アーキテクチャ

オブジェクト指向プログラミングは、ここ数十年にわたってソフトウェア設計の金字塔とされてきました。
しかし昨今、いわゆる「オブジェクト指向不要論」がエンジニアリングコミュニティで散見されるようになりました。
私はこの動向を、単なるアンチテーゼや流行として片付けるべきではないと考えています。
そこには、継承の濫用デザインパターンの過剰適用によってもたらされる複雑性の増大に対する、真摯な警鐘が込められているからです。

実際に私たちが日常的にメンテナンスするコードベースを振り返ってみてください。
インターフェースがインターフェースを継承し、抽象クラスがさらに抽象クラスを派生させる階層の深い構造。
その末端にある具象クラスの挙動を追跡するには、IDEのジャンプ機能を駆使してもなお、5つ以上のファイルを行き来しなければならないことは珍しくありません。
このような状況では、可読性は著しく損なわれ、バグの温床となり、新たな機能追加に対する心理的コストは指数関数的に上昇します。
オブジェクト指向の本来の目的である「現実世界のモデル化」が、いつの間にか「コードの複雑さの隠蔽」にすり替わっているのです。

では、私たちは何を目指すべきか。
それは過剰な抽象化を排し、データとその変換処理を中心に据えた設計です。
以下の表は、従来のオブジェクト指向設計と、本記事で提案するシンプルな設計手法の比較です。

比較観点 従来の過剰抽象化設計 提案するデータ中心設計
関心事の分離 役割と責務をクラス単位で細分化 データ構造と純粋関数で処理を分離
コード追跡性 継承ツリーとポリモーフィズムで間接的 関数呼び出しが直接的で追跡容易
変更への強さ 基底クラス変更が全体に波及しやすい データ構造が安定していれば局所変更で済む
テスト容易性 モック生成に多大なセットアップが必要 入出力が明確な関数は単体テストが容易

この比較からも明らかなように、私はオブジェクト指向そのものを否定しているわけではありません。
適切な局面でポリモーフィズムを活用することは有効です。
しかし、多くのケースで、私たちは「将来の拡張性」という名目の下に、まだ存在しない要件に対して過剰な準備をしすぎています。
例えば、たった一つの振る舞いを追加するために、抽象ファクトリとビルダーを組み合わせた巨大な構造を構築するよりも、以下のようにシンプルな関数として定義し、必要に応じて構造体やレコードに処理を関連付けるほうが、はるかに読みやすく、保守しやすいコードになります。

// 過剰なクラス階層を用いる代わりに
function calculateDiscount(price, userType) {
  // 条件分岐で明示的に処理を記述する
  if (userType === 'premium') return price * 0.8;
  if (userType === 'regular') return price * 0.95;
  return price;
}

このアプローチの核心は、「何を実現するか」を「どのように抽象化するか」よりも優先させるという、プリミティブな原則に立ち返ることです。
データを不変に保ち、そのデータを入力として受け取り、新しいデータを出力として返す純粋な関数群を組織化することで、副作用が局所化され、結果としてコードの予測可能性が飛躍的に高まります。

本記事では、この思想に基づき、具体的なリファクタリング手法や、ドメインモデルの貧血化を避けながらも過剰な抽象化に陥らないためのバランスの取り方を、実践的なコード例を交えながら徹底的に検証していきます。
継承より合成、そして合成より単純な関数呼び出しを選ぶことで、チーム全体の生産性をどう向上させるか。
その具体的な道筋を、一緒に考えていきましょう。

  1. はじめに:なぜ今、オブジェクト指向不要論が話題なのか
    1. 複雑化するエンタープライズコードの現実
    2. アンチパターンとしての過剰設計が生む負のスパイラル
  2. 継承の濫用が招くデバッグ困難と保守コスト
    1. 深い継承階層がもたらすコード追跡性の喪失
    2. 基底クラス変更による想定外の副作用をどう防ぐか
  3. デザインパターンの過剰適用が生産性を阻害する理由
    1. ファクトリパターンの乱用で増えるボイラープレートコード
    2. オブザーバーやデコレータで見失うビジネスロジックの本質
  4. データ中心設計の基本原則――関数型思考への接近
    1. データと振る舞いの分離がもたらす明確性と予測可能性
    2. 不変データ構造を活用した安全で副作用のない状態管理
  5. 純粋関数によるリファクタリングで可読性を向上させる手法
    1. 副作用を局所化するための実践的テクニック
    2. 入出力が明確な関数はテスト自動化と品質担保に最適
  6. 過剰抽象化を避けるための判断基準とYAGNIの現実的適用
    1. 将来の拡張性よりも現在のシンプルさを優先する定量的指標
    2. チームの認知負荷を下げる設計判断の実践ルール集
  7. 実例で比較:オブジェクト指向過剰設計 vs シンプル設計
    1. 典型的な過剰クラス構造のコードスニペットとその問題点
    2. リファクタリング後の関数ベースコードとパフォーマンス比較
  8. まとめ:オブジェクト指向を否定せず、適材適所で使いこなす設計の心得

はじめに:なぜ今、オブジェクト指向不要論が話題なのか

プログラミングコードが散乱するデスクトップ画面と複雑なクラス図に悩むエンジニア

オブジェクト指向プログラミングに対する不要論は、決して新しいものではありません。
しかしここ数年で、この議論が再び活発化している背景には、エンタープライズシステムの肥大化開発生産性に対する要求の変化が大きく関係しています。
かつては「設計の銀の弾丸」と称えられたオブジェクト指向も、昨今のマイクロサービスアーキテクチャやクラウドネイティブな開発スタイルが浸透する中で、その重厚な抽象化レイヤーがむしろ足かせとなる場面が顕著になってきました。

特に気になるのは、多くのプロジェクトで「オブジェクト指向であること」自体が目的化し、本来のビジネス価値の実現よりも、クラス図の美しさデザインパターンの網羅性が優先されるケースです。
さらに、GitHub Copilot や Claude Code などのAIコーディングアシスタントが日常的に使われる現代では、人間が読むための可読性はもちろん、AIが正しく文脈を理解できるコード構造という新たな観点も求められています。
過剰に間接化されたコードは、人間だけでなく機械にも理解が難しく、生成AIによる支援効果を著しく減衰させることが実証されつつあります。

このような潮流を踏まえ、本記事ではオブジェクト指向そのものを否定するのではなく、過剰な抽象化がもたらす実際のコストを定量的に評価し、よりシンプルで持続可能な設計手法を提案していきます。

複雑化するエンタープライズコードの現実

まず、多くのエンタープライズシステムが抱えるコードベースの実態を見てみましょう。
例えば、JavaのSpring FrameworkやC#の.NETを用いた典型的な業務システムでは、コントローラ、サービス、リポジトリ、DTO、バリデータ、コンバータといった多層構造が当たり前のように採用されています。
一見すると関心の分離がなされているように見えますが、実際にビジネスロジックを追跡しようとすると、6〜7層のメソッドコールを経由してようやく本体に到達するという状況は珍しくありません。

ここで問題となるのは、各層がインターフェースを介して疎結合に設計されているがゆえに、実行時の実際の実装クラスがDIコンテナによって動的に決定される点です。
IDEの「定義へ移動」機能を使っても、インターフェースの宣言に飛ばされるだけで、具象クラスは実行時設定に依存します。
この間接性は、デバッグに要する認知負荷を著しく増大させ、新たなメンバーのオンボーディングコストを押し上げる主要因となっています。

加えて、データベースアクセスをORMに委譲するケースでは、発行されるSQLが暗黙的に生成されるため、パフォーマンスチューニングの際に予想外のN+1問題が発生することも頻繁です。
これらの問題は、オブジェクト指向のカプセル化が「内部の複雑さを隠す」ことに成功する反面、「何が起きているのかを把握するための手がかり」まで隠してしまっているという逆説的な状況を生み出しています。
実際の現場では、このような複雑性に対処するために、デバッグログの増設プロファイリングツールの導入に多大な工数を割くことになり、それがさらにコードベースを膨張させる要因ともなっています。

アンチパターンとしての過剰設計が生む負のスパイラル

過剰設計がもたらす最も深刻な悪影響は、保守作業そのものがコードの複雑性を増幅させる負のスパイラルに陥ることです。
具体的には、以下のような悪循環が観察されます。

  • 将来的な拡張に備えて抽象基底クラスを導入するが、実際にはその拡張が一度も発生しない
  • 既存の抽象クラスに新しいメソッドを追加する際、全サブクラスへの影響を考慮して、さらに新しい中間抽象クラスを挿入する
  • その結果、継承ツリーが深くなり、オーバーライドの優先順位が複雑化して、予期せぬ動作を引き起こす
  • このバグを修正するために、さらに条件分岐やフラグ変数が追加され、コードの本質的複雑度が上がる

このスパイラルの根底にあるのは、「変更に強い設計」という美名の下に、実際の変更頻度や変更パターンを分析せずに抽象化を先行させるという判断ミスです。
例えば、ある振る舞いが今後3種類のバリエーションを持つかもしれないという想定で、ストラテジーパターンを導入したとします。
しかし現実には、そのバリエーションは2種類で固定され、しかもその差は単なる条件分岐で十分に吸収できるものでした。
この場合、パターン導入によってクラス数が倍増し、設定ファイルも増え、ビルド時間も延びたにもかかわらず、得られるメリットはほとんどありませんでした。

さらに悪いことに、このような過剰設計はコードレビューの質をも低下させます。
レビュアーは膨大な抽象化レイヤーを理解するのに疲弊し、本質的なロジックの誤りよりも「デザインパターンが正しく適用されているか」という形式的な観点に注力せざるを得なくなります。
結果として、ビジネスルールの欠陥が見逃され、納品後に重大な仕様変更が発生するという事態を招きます。

過剰設計の症状 引き起こされる具体的な問題 累積的なコスト
不要なインターフェースの分離 実装追跡に余分なジャンプが発生 毎日の開発時間が10〜15%増加
継承階層の深掘り オーバーライドの挙動予測が困難に バグ修正に要する平均時間が2倍に
デザインパターンの乱用 コード量が実際のロジックの3倍以上に 新規メンバーの戦力化までに3ヶ月余分
過剰なジェネリクス/テンプレート コンパイルエラーメッセージが不可解に ビルド失敗からの復旧に工数浪費

このような負のスパイラルから脱出するには、まず「抽象化はコストである」という認識を持つことが不可欠です。
各抽象化レイヤーには、それに見合ったリターンが必ず存在するべきであり、そのリターンとは「変更時の影響範囲の局所化」や「テストの容易性」など、具体的かつ計測可能なメリットでなければなりません。
次の章では、この判断基準を明確にするための定量的アプローチと、過剰抽象化を回避する具体的なリファクタリング手順について掘り下げていきます。

継承の濫用が招くデバッグ困難と保守コスト

深く積み重なった継承ツリーをたどるデバッガのスクリーンショット

オブジェクト指向の三大要素の一つである継承は、コードの再利用とポリモーフィズムを実現する強力な仕組みです。
しかし、その力を過信し、継承をデフォルトの設計手段として採用し続けると、システムは予想以上に脆く、デバッグが極めて困難なものへと変貌します。
特にエンタープライズアプリケーションでは、基底クラスから5階層、6階層と深くなる継承ツリーは珍しくなく、そうしたコードベースに対する修正作業は、まるで迷宮を歩くような複雑さを伴います。

ここで肝心なのは、継承が静的な結合関係を生むという点です。
コンパイル時に決定されるこの結合は、実行時の動的振る舞いを追跡する際に、ソースコードだけでは完結しない情報を強いることになります。
IDEの「呼び出し階層」機能を駆使しても、どのオーバーライドが実際に選択されるかは、実行時のオブジェクト型に依存するため、静的分析では限界があります。
この不確実性が、デバッガをステップ実行しながら一つ一つのレイヤーを確認するという、非効率な作業を強いるのです。

深い継承階層がもたらすコード追跡性の喪失

具体例として、ある業務システムにおける「承認処理」をモデル化した継承構造を考えてみましょう。
基底クラスとしてApprovalProcessがあり、それをDepartmentApprovalが継承し、さらにManagerApprovalExecutiveApproval、そしてSpecialExecutiveApprovalといった具合に階層が深まっていきます。
各クラスはapproveメソッドをオーバーライドし、それぞれでsuper.approve()を呼び出した上で独自のロジックを追加しています。

このような構造で、実際にSpecialExecutiveApprovalのインスタンスに対してapproveが呼び出されたとき、実行されるコードの順序は以下のようになります。

SpecialExecutiveApproval.approve()
  -> ExecutiveApproval.approve()
    -> ManagerApproval.approve()
      -> DepartmentApproval.approve()
        -> ApprovalProcess.approve()

つまり、一つのメソッド呼び出しが5つのクラスの実装を連鎖的に実行することになります。
この連鎖のどこで状態が変更され、どの条件分岐が評価されるのかを把握するには、少なくとも5つのファイルを開き、それぞれの実装を頭の中で合成しなければなりません。
さらに、各クラスがインスタンス変数を持ち、それらがスーパーのメソッドで参照される場合、その変数の初期化順序や変更タイミングも考慮に入れる必要が生じます。

こうした追跡性の喪失は、バグ発生時の根本原因特定に通常の3倍から5倍の時間を要するというデータも存在します。
また、新たにチームに参加したエンジニアがこの階層を理解するには、膨大なドキュメント読解とデバッグセッションの反復が必須となり、結果として戦力化までの期間が著しく延びるのです。
この状況は、コードの可読性という観点からは明らかに失敗であり、継承の濫用がもたらす最も顕著な負債と言えます。

基底クラス変更による想定外の副作用をどう防ぐか

継承のもう一つの重大なリスクは、基底クラスに加えた変更が、意図せず全てのサブクラスに波及するという副作用の問題です。
例えば、基底クラスApprovalProcessに新しいメソッドisApprovalRequiredを追加し、デフォルトでtrueを返すように実装したとします。
この変更自体は一見無害に見えますが、特定のサブクラスがこのメソッドをオーバーライドしていなかった場合、そのサブクラスの振る舞いが暗黙のうちに変わってしまう可能性があります。

さらに危険なのは、基底クラスの内部で使用しているプライベートメソッドやフィールドを変更するケースです。
サブクラスはそれらを直接参照できなくても、スーパーのパブリックメソッドを通じて間接的に影響を受けます。
たとえば、基底クラスのコンストラクタで行う初期化処理を変更した場合、すべてのサブクラスのインスタンス生成時に新たな前提条件が課されることになり、既存のサブクラスで予期せぬ例外が発生することがあります。

このような副作用を防ぐための実践的な対策を、以下に列挙します。

  • 継承よりもコンポジションを優先する:振る舞いを委譲オブジェクトとして切り出し、フィールドとして持つことで、依存関係を明示的かつ疎結合に保てます
  • 基底クラスは「封印(sealed/final)」可能な箇所で積極的に封印する:特に変更頻度の高い基底クラスは、継承を許可しないか、継承用に設計された明確な拡張ポイントのみを公開する
  • オーバーライド可能なメソッドには@Overrideや明示的なアノテーションを強制し、基底クラスの変更履歴をサブクラスに伝搬させる仕組みをCIに組み込む
  • 基底クラスの変更に対しては、全サブクラスをビルドする統合テストを必ず実行し、回帰を検出する
予防策 適用タイミング 期待される効果
コンポジションへの置き換え 新規設計時およびリファクタリング時 継承階層の深さを1〜2層に抑制
封印クラスの積極的利用 基底クラスが安定した機能を提供する場合 無制限な派生を防止し影響範囲を限定
オーバーライド検証の自動化 CIパイプラインの静的解析ステップ 基底変更時にサブクラスの破壊を早期発見
統合テストスイートの拡充 基底クラス変更の都度実行 実行時エラーを開発環境で即座にキャッチ

最終的に、基底クラス変更の副作用を完全にゼロにすることは現実的ではありません。
しかし、継承の使用を設計上の決断として厳格に管理し、変更の影響範囲を可視化するツールとプロセスを整備することで、リスクを許容可能な水準まで低減することは十分に可能です。
次の章では、このような継承依存から脱却するための具体的な代替アーキテクチャとして、データ中心設計と関数型のプラクティスを詳しく見ていきます。

デザインパターンの過剰適用が生産性を阻害する理由

抽象ファクトリやビルダーなど多数のデザインパターン図が乱雑に並ぶホワイトボード

デザインパターンは、GoF(Gang of Four)の名著によって広く知られるようになって以来、ソフトウェア設計の「共通語彙」として重要な役割を果たしてきました。
しかし、この共通語彙が誤って設計の目的そのものにすり替わってしまうと、生産性は著しく低下します。
私が現場で何度も目撃してきたのは、「このシチュエーションにはこのパターン」という機械的な適用が、むしろコードベースを複雑にし、変更のたびに不要なクラス群を増殖させている光景です。

パターンはあくまで問題に対する解決策のテンプレートであり、すべての問題に当てはまる万能薬ではありません。
特に、ビジネスロジックが比較的シンプルで、将来の拡張が明確に見通せない段階で、戦略パターンや抽象ファクトリを先行導入すると、そのパターンの維持に費やすコストが、パターンがもたらすメリットを上回るという逆転現象が起きます。
本節では、特に乱用されがちなファクトリパターンと、オブザーバー/デコレータパターンに焦点を当て、彼らが生産性を阻害するメカニズムを解剖していきます。

ファクトリパターンの乱用で増えるボイラープレートコード

ファクトリパターンは、オブジェクト生成のロジックをクライアントから分離するという正当な目的を持ちます。
しかし、多くのプロジェクトでは、単一の具象クラスしか生成しないファクトリが無数に作られています。
例えば、UserServiceというクラスがあるとき、そのインスタンスを生成するためのUserServiceFactoryを用意し、さらにそのファクトリに対するインターフェースIUserServiceFactoryまで定義する。
これだけでも既に3つの構成要素が増え、依存性注入コンテナへの登録作業も追加で必要になります。

このような過剰なファクトリ導入がもたらす直接的コストは、ボイラープレートコードの氾濫です。
ファクトリクラスは通常、コンストラクタ引数の受け渡しと、new演算子の呼び出しをラップするだけの単純なコードであり、そこには本質的なビジネスロジックは一切含まれません。
それにもかかわらず、これらのクラスはメンテナンス対象となり、リファクタリング時にコンストラクタのシグネチャが変われば、ファクトリも追随して修正を強いられます。

さらに深刻なのは、テストコードにおいてもこの影響が及ぶ点です。
モックフレームワークを用いる場合、インターフェース経由でモック化すること自体は可能ですが、ファクトリそのものをモックする必要が生じ、セットアップコードが冗長化します。
また、DIコンテナを使う場合、ファクトリの生成自体をコンテナに委譲するための追加設定が求められ、設定ファイルやアノテーションが増殖する原因にもなります。

過剰ファクトリが引き起こす問題 開発への影響度 回避策の例
クラス数の異常な増加 高(プロジェクト全体で20〜30%のクラスがファクトリ) 単純な生成はnewで直接行い、複雑な生成だけをファクトリ化する
ビルド時間の延伸 中(各ファクトリのコンパイルが積み重なる) ファクトリメソッドを既存クラスの静的メソッドとして持つ
DI設定の複雑化 中(コンテナへの登録項目が倍増) コンストラクタインジェクションを原則とし、ファクトリを極力使わない
コードリーディングの中断 高(生成箇所を追うために余計なジャンプが発生) シンプルな生成は読者の予測に任せる

私の経験則として、ファクトリを導入すべきかどうかの判断基準は「生成時に複数の条件分岐や外部リソースの取得が必要かどうか」に集約されます。
それ以外のケースでは、コンストラクタを直接呼び出す方が、読みやすく、修正コストも低いのです。

オブザーバーやデコレータで見失うビジネスロジックの本質

オブザーバーパターンはイベント駆動アーキテクチャの基盤として有用であり、デコレータパターンは機能の追加を柔軟に行うための定番手段です。
しかし、これらのパターンを過剰に適用すると、ビジネスロジックの流れが断片化され、システム全体の振る舞いを把握することが極端に難しくなります。

例えば、ある決済処理システムを考えます。
注文確定後に、在庫減算、メール送信、ポイント付与、ログ出力といった複数の処理をオブザーバーとして登録する設計は一見美しく見えます。
しかし、実際のビジネス要件では「在庫減算が失敗したらメールは送信しない」といった順序依存やトランザクション境界が存在することがほとんどです。
オブザーバーを非同期にばらまいてしまうと、このような依存関係を実装するために、さらに複雑な調整用の仕組み(イベントバスやトランザクション同期)が必要になり、コードの本質が隠れてしまいます。

同様に、デコレータで認証、ロギング、キャッシュ、リトライなどを積み重ねるケースでは、実際のビジネス処理がラッパーの連鎖の一番奥に埋もれ、デバッガでスタックトレースを確認しても数十行のデコレータチェーンが表示されるだけという事態が起こります。
このようなコードでは、新しい開発者が「どのデコレータがどの順番で実行されるか」を理解するのに数日を要し、しかもその順序を誤ると想定外の副作用が発生します。

  • オブザーバー乱用の弊害:イベント発行順序やエラー伝播が暗黙的になり、障害発生時の原因特定が極めて困難になる
  • デコレータ乱用の弊害:本来1つの関数で完結する処理が複数クラスに分散し、リファクタリング時にすべてのデコレータを同時に修正せねばならない
  • 共通する根本問題:パターンが「どのように」実装するかに焦点を当てすぎて、「何を」実現するかというビジネス要件が見えにくくなる

これらの問題に対処するには、パターン導入の前にプレーンな手続き型コードで書き出すというアプローチが有効です。
最初からパターンで囲い込むのではなく、まずはシンプルな関数やメソッドでビジネスロジックを表現し、その後に本当に必要なパターンのみを選別的に適用するのです。
この「後付けパターン」戦略により、過剰な抽象化を防ぎつつ、真に価値のある部分だけを洗練させることができます。
次の章では、この考え方をさらに推し進めたデータ中心設計の具体的手法を紹介します。

データ中心設計の基本原則――関数型思考への接近

データ構造と純粋関数が整然と分離されたシンプルな設計図

ここまでの議論で、過剰な継承やデザインパターンの濫用がもたらす複雑性のコストについて見てきました。
では、それらを排した先にあるシンプルで持続可能な設計とは、具体的にどのようなものでしょうか。
私が推奨するのは、データと振る舞いを明確に分離するデータ中心設計、すなわち関数型の思考様式を積極的に取り入れたアプローチです。
これはオブジェクト指向の完全な否定ではなく、オブジェクト指向が提供する「状態のカプセル化」を、より厳密で検証可能な形で再解釈するものだと考えてください。

伝統的なオブジェクト指向では、データ(フィールド)と振る舞い(メソッド)を同一クラス内に同居させることが原則です。
しかしこの原則は、振る舞いがデータの内部構造に強く依存することを暗黙に許し、結果としてクラス同士が密結合しやすい構造を生みます。
一方、データ中心設計では、データを純粋な値オブジェクト(レコードや構造体)として定義し、それに対する操作はすべて外部の純粋関数として実装します。
この単純な分離が、コードの明確性と予測可能性を劇的に向上させる原動力となるのです。

データと振る舞いの分離がもたらす明確性と予測可能性

データと振る舞いを分離する最大のメリットは、関数の入出力が自己文書化されるという点にあります。
データは不変の入力として扱われ、関数はそのデータを受け取り、新しいデータを返す。
この入出力の関係がシグネチャに完全に現れるため、関数内部の実装を読まなくても、その関数が何を行うのか、どのような副作用を持つのかを推測できます。
例えば、以下のコードをご覧ください。

// データ定義(構造体/レコード)
record Order(
    int Id,
    List<OrderItem> Items,
    string CustomerType,
    decimal BasePrice
);

record OrderItem(string Name, int Quantity, decimal UnitPrice);

// 振る舞いは純粋関数として分離
decimal CalculateDiscountedTotal(Order order, decimal premiumDiscountRate) {
    var subtotal = order.Items.Sum(item => item.Quantity * item.UnitPrice);
    var discount = order.CustomerType == "Premium" ? premiumDiscountRate : 0.0m;
    return subtotal * (1.0m - discount);
}

この例では、Orderは単なるデータの入れ物であり、計算ロジックは完全にCalculateDiscountedTotalという関数に委譲されています。
この関数は、Orderと割引率を受け取り、合計金額を返すだけです。
データベースへのアクセスも、ファイルの書き込みも、グローバル変数の参照もありません。
したがって、この関数のテストは、入力データを用意して出力を検証するという極めて直交的な作業になります。
オブジェクト指向のようにモックを準備したり、インスタンス化のための複雑なセットアップをする必要はありません。

比較観点 オブジェクト指向(クラス内包型) データ中心(分離型)
振る舞いの所在 データと同じクラス内のメソッド 独立した関数またはモジュール内の関数
テストの容易性 インスタンス生成とモック作成に工数を要す データを入力し、戻り値を検証するだけ
コード読解時の認知負荷 メソッドが内部状態を変更する可能性を考慮 関数シグネチャだけで処理内容を推測可能
変更の影響範囲 クラス修正が他クラスに波及しやすい 関数の置き換えや追加が局所的で安全

このアプローチを徹底すると、システム全体が「データの流れ」として表現されるようになります。
各関数はパイプラインの部品として機能し、データがそれらの部品を通過するごとに変換されていきます。
この流れは非常に直線的で、コードの実行経路が実際のコードの構造と一致するため、デバッグ時にスタックトレースを追う手間が劇的に減少します。

不変データ構造を活用した安全で副作用のない状態管理

データ中心設計のもう一つの柱は、不変性(イミュータビリティ)の徹底です。
データ構造を一度作成したら変更できないという制約を設けることで、状態変更に伴う無数のバグを根本から排除できます。
オブジェクト指向では、setterメソッドを通じてオブジェクトの内部状態をいつでも書き換えるのが一般的ですが、この自由度がかえって危険を招きます。
特にマルチスレッド環境では、一つのオブジェクトを複数のスレッドが同時に変更しようとしてデータ競合が発生するのは周知の事実です。

不変データ構造では、状態を変更したい場合、新しいインスタンスを生成して返すという操作が基本となります。
例えば、ユーザーの住所を更新する処理は、以下のように実装されます。

record User(int Id, string Name, string Address);

User UpdateAddress(User user, string newAddress) {
    // 元のUserは変更せず、新しいインスタンスを返す
    return user with { Address = newAddress };
}

このコードにおけるwith式は、既存のUserレコードのAddressフィールドだけを新しい値に置き換えた新たなオブジェクトを生成します。
元のuserオブジェクトはそのままメモリ上に残り、他のどの部分からも参照され続けている場合でも、その参照先のデータが突然変わることはありません。
この性質により、以下のような決定的なメリットが得られます。

  • 時間的追跡性の向上:各時点の状態が個別のオブジェクトとして保存されるため、変更履歴をオブジェクトのリストとして保持できます
  • 並行処理の安全性:ロック機構が不要になり、スレッドセーフなコードが自動的に実現されます
  • 参照透過性の担保:同じ入力に対して常に同じ出力が返されるため、等式推論(equational reasoning)が可能になります

また、不変データ構造はキャッシュやメモ化(memoization)との相性も抜群です。
入力が変わらない限り、関数の戻り値も変わらないことが保証されるため、計算結果を安全に再利用できます。
このような特性は、関数型プログラミング言語であるHaskellやScala、さらにはモダンなTypeScriptやPythonのdataclass(frozen=True)などでも広く採用されており、オブジェクト指向言語でも十分に導入可能なプラクティスである点が重要です。

次の章では、これらの原則を実際のリファクタリングにどう適用するか、具体的な判断基準と併せて解説していきます。

純粋関数によるリファクタリングで可読性を向上させる手法

入出力が明示された純粋関数のシグネチャと、対応する単体テストコード

ここまでデータ中心設計の理念と、不変データ構造のメリットを説明してきました。
しかし、現実のプロジェクトで既存のオブジェクト指向コードをいきなりすべて書き換えることは非現実的です。
そこで重要になるのが、純粋関数を段階的に導入するリファクタリング手法です。
このアプローチでは、システム全体を一度に変えるのではなく、変更頻度の高いモジュールや、バグが多発している箇所から優先的に関数ベースの設計へと移行します。
純粋関数とは、同じ引数に対して常に同じ結果を返し、かつ外部状態を一切変更しない関数のことです。
この定義を満たす関数は、読み手にとって予測が極めて容易であり、コードレビューやデバッグの工数を大幅に削減します。

リファクタリングの第一歩は、副作用を伴う処理とそうでない処理を明確に区別することです。
多くのクラスメソッドは、データベースアクセス、ファイルI/O、ログ出力、グローバル変数の更新など、複数の副作用を内包しています。
これらの副作用を関数の外側に追い出し、ビジネスロジックの核心部分を純粋関数として切り出すことで、可読性は飛躍的に向上します。
例えば、ユーザー登録処理を考えた場合、バリデーションやポイント計算などのロジックは純粋関数に抽出し、データベースへの保存処理だけを副作用として残すのです。
この分離により、テスト時には純粋関数部分のみを高速に検証でき、副作用部分はモックやスタブで置き換えることが容易になります。

副作用を局所化するための実践的テクニック

副作用を局所化するための具体的なテクニックとして、私は「境界関数」パターンを推奨しています。
これは、システムの入出力ポイント(HTTPエンドポイント、メッセージキュー、データベース接続など)だけに副作用を集中させ、それより内側の層ではすべて純粋関数で構成するという設計です。
実践的には、以下のような手順で進めます。

  • ステップ1:既存のメソッドから、外部リソースへのアクセス部分を抽出し、専用のアダプタ関数に委譲する。このアダプタ関数は副作用を持つことを明示的に命名(例:saveToDatabasefetchFromApi)する
  • ステップ2:残ったロジックを純粋関数として再実装し、入力パラメータと戻り値のみで完結させる。このとき、データ変換やバリデーション、計算処理はすべてこの純粋関数に含める
  • ステップ3:アダプタ関数と純粋関数を組み合わせる「コンポジションルート」を用意し、全体のフローを構築する。このルートだけが副作用を扱い、他のモジュールは純粋性を保つ

このアーキテクチャを採用すると、コードの構造が「副作用レイヤー」と「ロジックレイヤー」の2層に明確に分離されます。
副作用レイヤーはシステムの端っこに集中し、ロジックレイヤーはすべて純粋となるため、ビジネスルールの変更はロジックレイヤーだけを修正すれば済みます。
また、新しい外部サービスを追加する場合も、アダプタ関数を新設するだけでよく、既存の純粋関数には一切影響を与えません。
この分離は、単体テストの実行速度にも大きなメリットをもたらします。
純粋関数のテストはデータベースやネットワークを介さないため、ミリ秒単位で完了し、開発者のフィードバックループが格段に短縮されるのです。

さらに、副作用を局所化することで、デバッグ時の原因特定が劇的に簡単になります。
エラーが発生した場合、まず副作用レイヤー(ネットワークタイムアウトやDB接続エラーなど)か、ロジックレイヤー(計算ミスや条件漏れなど)かを切り分けられるため、調査範囲を半減できます。
このように、純粋関数の導入は単なる「きれいなコード」にとどまらず、運用フェーズにおける障害対応の効率も向上させる実践的な価値を持つのです。

入出力が明確な関数はテスト自動化と品質担保に最適

純粋関数の最大の強みは、テストが極めて容易かつ網羅的になるという点にあります。
入出力が明確に定義されているため、テストケースは入力値の組み合わせを網羅するだけで事足ります。
例えば、割引計算関数をテストする場合、通常のケース、境界値(ゼロ、負数、最大値)、エッジケース(割引率が100%を超えるなど)をパラメタライズドテストとして記述するだけで、高いカバレッジを達成できます。
このアプローチは、オブジェクト指向のモック地獄から完全に解放されます。

テスト観点 オブジェクト指向(モック主体) 純粋関数(入力主体)
テスト準備 依存オブジェクトのモック生成が必須 入力データ構造を構築するだけ
テスト実行速度 モックフレームワークのオーバーヘッドあり 関数呼び出しのみで極めて高速
カバレッジ測定 メソッド内部の分岐が複数クラスに分散 関数単位で全分岐を一覧可能
リファクタリング耐性 モックの振る舞い変更がテストに波及 関数シグネチャが変わらなければテストは不変

実際のプロジェクトでは、純粋関数に対するプロパティベーステスト(例:QuickCheckスタイル)を導入することで、より強力な品質担保が可能になります。
例えば、「割引後の金額は元の金額を超えない」という不変条件をテストプロパティとして定義し、ランダムな入力を大量に生成して検証するのです。
このようなテストは、手動で境界値を列挙するよりもはるかに多くのバグを発見できることが知られています。

また、純粋関数はCI/CDパイプラインとの親和性も高いです。
各関数が独立しているため、変更があった関数だけをビルドしてテストする「差分テスト」が実装しやすく、全体のビルド時間を短縮できます。
さらに、関数の入出力がJSONや構造体としてシリアライズ可能であれば、APIモックやフィクスチャデータの生成にもそのまま流用できるため、エンドツーエンドテストの準備コストも下がります。

最後に、品質担保の観点で忘れてはならないのは、コードレビューの効率化です。
純粋関数はその関数単体で完結しているため、レビュアーは周辺のクラスやグローバル状態を一切意識せず、関数内部のロジックだけに集中できます。
この集中力の向上は、レビュー指摘の質を高め、見逃しバグを大幅に減少させる効果があります。
次の章では、これらの手法を適用する際の判断基準と、チーム全体で合意するためのプラクティスを整理していきます。

過剰抽象化を避けるための判断基準とYAGNIの現実的適用

天秤の一方に抽象化コスト、他方に可読性メリットを載せたバランスシート

これまでに、継承やデザインパターンの過剰な適用がどのようにコードの可読性と保守性を損なうかを論じてきました。
しかし、では具体的に「どこまでが適切で、どこからが過剰なのか」という境界線をどう引けばよいのでしょうか。
この問いに答えるために、私はYAGNI(You Aren’t Gonna Need It)の原則を単なる精神論ではなく、定量的な指標とチーム合意に基づく実践ルールとして再定義する必要があると考えます。
YAGNIは「将来のために書くな」というシンプルな教訓ですが、実際の開発現場では「将来の拡張性を考慮した設計」という美名の下に、容易に逸脱されてしまいます。
その逸脱を防ぐには、抽象化のコストを可視化する指標と、判断を標準化するルールセットが不可欠です。

本節では、まず抽象化の妥当性を評価するための定量的なメトリクスを提示し、次にチーム全員が共通して適用できる実践的な判断ルールを整理します。
これらの基準を導入することで、設計判断が個人の経験や好みに依存せず、客観的で再現性のあるプロセスへと変わります。
結果として、コードベースは必要最小限の抽象化にとどまり、結果的に可読性と開発速度の両方を高い水準で維持できるようになるでしょう。

将来の拡張性よりも現在のシンプルさを優先する定量的指標

抽象化を導入するか否かの判断において、私が最初に推奨するのは「変更頻度」と「変更コスト」のトレードオフを数値化することです。
具体的には、以下の3つのメトリクスを計測し、それらを基に意思決定を行います。

  • サイクロマティック複雑度(CC):関数やメソッド内の独立した実行経路の数を示します。CCが10を超える場合はリファクタリングを検討すべきですが、その際に抽象化ではなく、単純な関数分割で対応できないかをまず評価します
  • 結合度(Coupling):クラスやモジュール間の依存関係の数を計測します。結合度が高い場合、抽象化(インターフェースなど)を導入して依存を緩和する価値がありますが、その前に依存の方向を整理するだけで改善しないかを検討します
  • 変更インパクトサイズ:ある要件変更が発生した際に、修正が必要なファイル数を過去の履歴から推定します。この数が小さければ、抽象化の導入は後回しにできます

これらの指標を組み合わせた判断フレームワークを、以下の表にまとめました。

メトリクス 閾値(目安) 抽象化を検討すべきケース シンプルなまま維持すべきケース
サイクロマティック複雑度 CC > 15 条件分岐やループが多く、テストが困難 分割や抽出でCCを下げられる場合は抽象化不要
結合度(扇出/扇入) 依存先が10以上、または依存元が20以上 変更の波及が広範囲に及び、影響予測が困難 依存関係が安定しており、今後も変わらない見込み
変更インパクトサイズ(過去3ヶ月) 1回の変更あたり平均5ファイル以上 頻繁に同じ箇所が修正される 修正頻度が低く、変更の都度個別対応で十分

さらに、将来拡張の「確度」を評価することも重要です。
製品ロードマップに明記され、かつ優先度が高い機能であれば、それに備えた抽象化は投資として正当化されます。
しかし、「いつか来るかもしれない」という不確実な要求に対しては、抽象化の導入を遅延させるのが最適です。
この「遅延判断」こそがYAGNIの現実的な適用であり、実際にその機能が実装される段階で、その時点の情報に基づいて最適な抽象化を設計すればよいのです。
過去のプロジェクトを振り返ると、事前に導入した抽象化のうち実際に活用されたのは半分にも満たないというデータもあり、この統計は「後回し戦略」の有効性を強く支持しています。

チームの認知負荷を下げる設計判断の実践ルール集

定量的指標だけでは全ての判断をカバーできません。
そこで、チームとして合意すべき実践的なルールをいくつか提案します。
これらのルールは、個々のエンジニアの裁量に委ねるのではなく、コードレビューや設計レビューのチェックリストとして組み込むことで、チーム全体の認知負荷を均一に下げる効果を発揮します。

  • ルール1:継承は「is-a」関係が明確で、かつ階層が3層を超えたら設計見直しのシグナルとする。3層を超える場合は、コンポジションやインターフェースの分離で代替できないか必ず議論します
  • ルール2:デザインパターンを導入する前に、まずプレーンな関数で実装してみる。その実装で十分に動作し、かつ読みやすいとチームが判断した場合、パターンは不要とみなします
  • ルール3:インターフェースは少なくとも2つ以上の具象実装が存在するまでは定義しない。単一の実装しかないインターフェースは、将来の交換可能性を過大評価している可能性が高いです
  • ルール4:新しい抽象化(基底クラス、インターフェース、ファクトリなど)を追加する際は、その導入によって削減できるコード量(重複排除や条件分岐の削減)が、追加されるコード量を上回ることを確認する
  • ルール5:「これがないとテストが書けない」という理由だけで抽象化しない。テスト容易性は重要ですが、モックフレームワークやテスト用のスタブ実装を直接コードに埋め込むなど、別の手段で解決できる場合が多いです

これらのルールを徹底するために、私は「リファクタリングタイマー」というアプローチも実践しています。
すなわち、抽象化を導入する設計判断を行った時点で、その抽象化が実際に役立ったかどうかを3ヶ月後に振り返る仕組みです。
この振り返りで「使われなかった抽象化」が多数判明した場合は、その原因をチームで共有し、次回以降の判断にフィードバックします。
この継続的な改善サイクルにより、チーム全体の「抽象化センス」が徐々に洗練されていきます。

最後に、これらのルールは絶対的なものではなく、プロジェクトの性質やチームの成熟度に応じてカスタマイズすることが前提です。
例えば、ライブラリ開発やプラットフォームチームでは、ある程度の拡張性を先読みした抽象化が求められます。
しかし、ビジネスアプリケーションの大多数では、上記のルールを厳格に適用することで、過剰設計を効果的に抑制できると確信しています。
次の章では、これらの判断基準を実際のコードリファクタリング事例に適用し、具体的なビフォーアフターを比較してみましょう。

実例で比較:オブジェクト指向過剰設計 vs シンプル設計

左右に並べられた過剰クラス設計と関数ベース設計のコード差分を表示するスプリットビュー

ここまで、過剰な抽象化がもたらす問題と、それを回避するためのデータ中心設計の理論を体系的に説明してきました。
しかし、やはり実際のコードを見なければ、その差は実感しづらいものです。
そこで本節では、同じビジネス要件に対して、過剰なオブジェクト指向設計を施したコードと、データ中心かつ関数ベースのシンプルな設計を施したコードを具体的に比較してみましょう。
題材として、ECサイトにおける「顧客タイプに応じた割引計算」という、非常にありふれた処理を選びます。

この処理は、通常顧客とプレミアム顧客で割引率を変え、さらに特定のキャンペーン期間中は追加割引を適用するというものです。
シンプルな要件であるにもかかわらず、過剰設計のアプローチでは、これを実装するために実に5つのファイルと3つのインターフェースを用意することになります。
一方、シンプル設計では、たった1つのデータ定義と1つの関数で完結します。
この劇的な差を、以下で詳しく見ていきましょう。

典型的な過剰クラス構造のコードスニペットとその問題点

まずは、過剰設計の典型例として、戦略(Strategy)パターンと抽象ファクトリ(Abstract Factory)パターンを組み合わせた実装を考えます。
このコードは、教科書通りの「拡張性」を備えているように見えますが、実際の開発現場では多くの問題を引き起こします。

// ---- インターフェース定義(IDiscountStrategy) ----
public interface IDiscountStrategy {
    double applyDiscount(double amount);
}

// ---- 具象戦略クラス(RegularDiscount) ----
public class RegularDiscount implements IDiscountStrategy {
    @Override
    public double applyDiscount(double amount) {
        return amount * 0.95; // 5%割引
    }
}

// ---- 具象戦略クラス(PremiumDiscount) ----
public class PremiumDiscount implements IDiscountStrategy {
    @Override
    public double applyDiscount(double amount) {
        return amount * 0.80; // 20%割引
    }
}

// ---- ファクトリクラス(DiscountStrategyFactory) ----
public class DiscountStrategyFactory {
    public static IDiscountStrategy create(String customerType) {
        if ("Premium".equals(customerType)) {
            return new PremiumDiscount();
        } else {
            return new RegularDiscount();
        }
    }
}

// ---- これを利用する注文処理クラス(OrderService) ----
public class OrderService {
    public double calculateFinalPrice(double basePrice, String customerType) {
        IDiscountStrategy strategy = DiscountStrategyFactory.create(customerType);
        return strategy.applyDiscount(basePrice);
    }
}

一見すると、インターフェースを用いて戦略を切り替えることで、オープンクローズドの原則に従った美しい設計に見えます。
しかし、このコードが抱える問題点を列挙してみましょう。

  • ファイル数の爆発:このわずかなロジックのために、インターフェース、2つの具象クラス、ファクトリ、そして利用側の合計5つのファイルが必要です。実際のプロジェクトでは、これにさらに基底クラスや抽象クラスが加わるため、ファイル数は指数関数的に増加します
  • コード追跡性の著しい低下OrderServiceから実際の割引計算ロジックにたどり着くまでに、ファクトリとインターフェースという2段階の間接層を経由しなければなりません。IDEのジャンプ機能を使っても、実行時にどの具象クラスが選択されるかは、customerTypeの値を見るまで確定しません
  • 冗長なボイラープレート:各戦略クラスは単に数値を返すだけで、それ以上の責務を持ちません。それにもかかわらず、クラス定義、コンストラクタ、メソッドオーバーライドといった構文が必須となり、本質的なロジックよりも構文的なノイズが多くなっています
  • 拡張時のコスト:仮に新しい「ゴールド」顧客タイプを追加する場合、新しい具象クラスの作成とファクトリへの分岐追加が必要です。この「拡張容易性」が、実際には「変更箇所の分散」という保守コストを生んでいることに気づくべきです

このように、過剰設計は「将来のため」という名目で導入されるものの、現在の開発速度を確実に低下させ、バグの温床となるのです。

リファクタリング後の関数ベースコードとパフォーマンス比較

では、同じ要件をデータ中心かつ関数ベースで実装するとどうなるでしょうか。
以下のコードは、先ほどの5つのファイルをすべて排除し、たった1つのファイルにすべてのロジックを集約しています。

// ---- データ定義(イミュータブルなレコード) ----
public record Order(double basePrice, String customerType) {}

// ---- 純粋関数による割引計算 ----
public class DiscountCalculator {
    public static double calculate(Order order) {
        double rate = switch (order.customerType()) {
            case "Premium" -> 0.80;
            case "Regular" -> 0.95;
            default -> 1.00;
        };
        return order.basePrice() * rate;
    }
}

このシンプルな実装には、次のような決定的なメリットがあります。

  • 即時理解可能性:割引率がどのように決まるかが、switch式として一目で把握できます。他のファイルを開く必要は一切ありません
  • テストの容易性:この関数のテストは、Orderレコードのインスタンスを生成して渡すだけです。モックもスタブも、DIコンテナの設定も不要です
  • 変更の局所性:割引率の変更や新しい顧客タイプの追加は、この1つの関数内だけで完結します。影響範囲が明確であり、修正漏れが発生しません

ここで、両者の設計を定量的に比較してみましょう。
以下の表は、今回のサンプルコードをベースに、一般的なプロジェクト規模に換算した指標です。

評価指標 過剰設計(戦略+ファクトリ) シンプル設計(関数ベース)
関連ファイル数 5ファイル(I/F, 具象x2, Factory, Service) 1ファイル(Data + Function)
総コード行数(LOC) 約60行(ボイラープレート含む) 約15行(ロジックのみ)
単体テストのセットアップ工数 モック生成やスタブ注入で平均5分/ケース 入力オブジェクト生成のみで平均1分/ケース
新規顧客タイプ追加時の変更箇所 具象クラス作成 + Factory修正 + テスト追加(計3箇所) 関数内のswitch修正 + テスト追加(計1箇所)
デバッグ時のスタックトレース深さ 5〜6フレーム(Factory→I/F→具象) 1〜2フレーム(関数呼び出しのみ)

パフォーマンス面でも、関数ベースのコードは有利です。
過剰設計では、インターフェース経由の仮想関数呼び出し(vtableルックアップ)や、ファクトリ経由のオブジェクト生成が入るため、CPUサイクルを余分に消費します。
一方、シンプル設計では、静的メソッドの直接呼び出しと値型のデータ渡しが中心となるため、メモリアロケーションも少なく、キャッシュヒット率も向上します。
もちろん、この程度の処理ではミクロな差に過ぎませんが、これが数千回、数万回と呼び出される大規模システムでは、無視できない蓄積効果となります。

何より重要なのは、開発者の認知負荷です。
過剰設計のコードを理解するには、5つのファイルを行き来しながら、実行時のポリモーフィズムを頭の中でシミュレートする必要があります。
しかし、シンプル設計のコードは、関数のシグネチャと数行の分岐ロジックを読むだけで完了します。
この差は、コードレビューや障害対応のたびに顕著に現れます。
私はこの比較を通じて、「設計」とは「クラスを増やすこと」ではなく「複雑さを減らすこと」であると、改めて強く認識させられました。
次の最終章では、本記事の全体を総括し、オブジェクト指向とどう向き合うべきかをまとめます。

まとめ:オブジェクト指向を否定せず、適材適所で使いこなす設計の心得

オブジェクト指向、関数型、データ中心など複数のパラダイムが調和するパズルの完成図

ここまで、オブジェクト指向の過剰な抽象化がもたらすデバグ困難性、保守コストの増大、そしてチーム生産性の阻害について、具体的なコード例や定量的指標を交えながら論じてきました。
しかし、ここで強調しておきたいのは、私はオブジェクト指向プログラミングそのものを否定しているわけではないという点です。
継承、ポリモーフィズム、カプセル化は、適切な文脈においては依然として強力な武器であり、特に大規模なフレームワーク開発や、複数の実装を切り替える必要があるプラグインアーキテクチャなどでは、オブジェクト指向の利点が際立ちます。
問題は、その武器をすべての問題に対して無差別に振り回すことにあるのです。

本記事を通じて私が伝えたかったのは、設計判断における優先順位の再構築です。
私たちがまず考えるべきは、コードの可読性、テスト容易性、そしてチームの認知負荷です。
これらの価値は、しばしば「拡張性」や「将来性」という名の抽象化の影に隠れがちですが、実際には、現在のシンプルさが将来の変更に対する最大の耐性を生むという逆説を、多くのプロジェクトが証明しています。
変更が発生したときに初めて抽象化を導入する「ちょうどいいタイミング」を見極めることが、経験豊富なエンジニアの真価であり、そのタイミングを逃さないための指標として、先述したサイクロマティック複雑度や変更インパクトサイズなどのメトリクスを活用することをお勧めします。

では、実際に私たちはどう行動すべきか。
私は以下の3つの指針を、チーム開発の基本憲章として提案します。

  • 指針1:まずはデータ構造を定義し、そのデータを変換する純粋関数を書く。クラスやインターフェースの設計はその後に行い、本当に必要な場合だけに限定する
  • 指針2:継承階層を1つ追加するたびに、それが「is-a」関係として本当に妥当か、そして代替としてコンポジションや単純な条件分岐が使えないかを厳しく問う
  • 指針3:デザインパターンの導入は、少なくとも3回以上の同様のパターンがコード内に出現してからとする。初回はパターンなしで実装し、2回目で共通化を検討し、3回目でパターン導入を決定するという「3回ルール」は、過剰設計を防ぐ実践的なベンチマークになります

これらの指針は、決してオブジェクト指向を軽視するものではなく、オブジェクト指向の利点を最大限に引き出すための制約として機能します。
むしろ、抽象化を絞ることで、本当に必要な抽象化にはより多くの設計リソースを集中でき、結果として高品質なインターフェースや基底クラスを生み出すことができるのです。

最後に、チーム文化として忘れてはならないのは、設計判断をドグマではなく、継続的な対話の対象とすることです。
私が関わってきたプロジェクトで成功したチームは、必ず「この抽象化は本当に今必要か」という問いをレビューやリファクタリングのたびに繰り返していました。
そして、使われなくなった抽象化は躊躇なく削除する勇気を持っていました。
この削除の勇気こそが、コードベースを健康に保つ最大の秘訣です。
なぜなら、抽象化は一度導入すると心理的に手放しにくくなりますが、その未使用の抽象化が将来の変更の妨げになるという事実を認識する必要があるからです。

以下の表は、本記事で提案した各設計アプローチの適用推奨シーンをまとめたものです。

設計アプローチ 推奨される適用シーン 避けるべきシーン
継承(is-a階層) GUIコンポーネントや例外クラスなど、階層が安定していて深さが2〜3層に収まる場合 ビジネスロジックや頻繁に変更されるドメインモデル
インターフェース分離 複数の実装(例:異なるデータベース、外部API)が確実に存在する場合 実装が単一で、かつ今後も増える見込みが薄い場合
デザインパターン(戦略、ファクトリ等) 同じ処理パターンが3以上の異なる場所で繰り返され、かつそれらが独立して変更される場合 条件分岐で十分に吸収できる範囲のバリエーション
関数ベース+データ中心 バリデーション、変換、計算など、副作用を持たないビジネスロジック全般 状態管理やライフサイクル制御が主体のコンポーネント

オブジェクト指向不要論は、あくまで「過剰なオブジェクト指向は不要」という意味であり、オブジェクト指向のエッセンスを否定するものではありません
カプセル化による情報隠蔽や、ポリモーフィズムによる柔軟な振る舞いの切り替えは、今もなお重要な設計資産です。
しかし、それらを使うかどうかの判断は、理論ではなく、実際のコードベースとチームのコンテクストに基づいて行うべきです。
私は、読者の皆さんが本記事の考え方を足がかりに、自分たちのプロジェクトで「ちょうどいい抽象化」のバランスを見つけ出し、より読みやすく、より変更に強く、そして何よりも開発者が幸せになれるコードを育てていかれることを願っています。
設計に正解はありませんが、常にシンプルさを志向する姿勢は、決して裏切りません。

コメント

タイトルとURLをコピーしました