Skip to content

SOLID原則への批判と限界

SOLID原則は広く受け入れられている設計指針ですが、万能ではありません。
このページでは、SOLID原則に対する主要な批判、適用の限界、そしてよくある誤解を整理し、より実践的な設計判断ができるようになることを目指します。

なぜ批判を学ぶべきか?

SOLID原則を「絶対的なルール」として盲目的に適用すると、かえって問題を引き起こすことがあります。 批判的な視点を持つことで、以下のメリットが得られます。

  • 適切な場面で適切な原則を選択できる
  • 過剰設計(オーバーエンジニアリング)を避けられる
  • チームでの設計議論がより建設的になる
  • トレードオフを意識した現実的な判断ができる

各原則への主要な批判

S: 単一責任の原則(SRP)への批判

批判1: 「アクター」の特定が難しい

SRPの正確な定義は「クラスはたったひとりのアクター(利用者・責任者) に対して責任を持つべき」です。 しかし、実務ではアクターの特定自体が困難なケースがあります。

問題点:

  • 組織構造が明確でない場合、誰が「アクター」か判断しにくい
  • 同じ機能を複数の部署が利用する場合、アクターの境界が曖昧になる
  • スタートアップなど少人数チームでは、アクターが実質的に重複する

例: レポート生成クラス

typescript
class ReportGenerator {
	calculateMetrics(): Metrics {
		/* ... */
	}
	formatAsHtml(): string {
		/* ... */
	}
	sendToStakeholders(): void {
		/* ... */
	}
}
  • calculateMetrics() → 経理部門がアクター
  • formatAsHtml() → マーケティング部門がアクター
  • sendToStakeholders() → 経営層がアクター

SRPに従えば3つのクラスに分割すべきですが、小規模プロジェクトで経理もマーケも同じ人が担当している場合、分割のメリットは薄れます。アクターが組織的に分離していない段階で分割すると、かえって複雑さが増すことがあります。

批判2: 過度な分割がコードを複雑にする

アクターを厳密に分離しようとすると、小さなクラスが大量に生まれ、かえって理解しにくくなることがあります。

typescript
// 過度に分割された例
class UserValidator {
	/* ... */
}
class UserRepository {
	/* ... */
}
class UserEmailSender {
	/* ... */
}
class UserFactory {
	/* ... */
}
class UserMapper {
	/* ... */
}
class UserDTOConverter {
	/* ... */
}
// ... 処理を追うために6つのファイルを行き来する必要がある

現実的なアプローチ:

  • 最初から完璧に分割しようとしない
  • 実際に変更が発生した時点でリファクタリングする
  • コードの理解しやすさとのバランスを取る

O: オープン・クローズド原則(OCP)への批判

批判1: 「修正に閉じている」は現実的ではない

OCPは特に多くの批判を受けています。C# in Depthの著者 Jon Skeet 氏も疑問を呈しています。

「完全にこの原則に従える拡張ばかりではなく、多くの場合、既存コードの修正は少なからず発生してしまう」

問題点:

  • あらゆる拡張パターンを事前に予測するのは不可能
  • 将来の変更を予測して抽象化を作ると、使われない複雑さが残る
  • 「修正に閉じる」ために導入した抽象化が、別の修正を必要とすることもある

批判2: YAGNI原則との矛盾

OCPを厳格に守ろうとすると、YAGNI(You Ain't Gonna Need It) に違反しがちです。

typescript
// OCPを意識しすぎた過剰設計
interface PaymentProcessor {
	process(amount: number): void;
}
interface PaymentProcessorFactory {
	create(type: string): PaymentProcessor;
}
interface PaymentStrategy {
	execute(): void;
}
// ... 実際には1種類の決済しか使わないのに

// シンプルで十分な場合も多い
class Payment {
	processCard(amount: number): void {
		/* ... */
	}
}

Robert Ashton氏の指摘:

「OCPは多くの設計過剰なソフトウェア部品の原因である。ただし、繰り返し変更される領域では意味がある」

L: リスコフの置換原則(LSP)への批判

批判1: 継承自体が問題視される現代

現代のソフトウェア開発では 「継承より合成(Composition over Inheritance)」 が推奨されています。

背景:

  • 継承は強い結合を生む
  • 深い継承階層は理解しにくい
  • TypeScriptやGoなど、インターフェース指向の言語が主流に

LSPは継承を前提とした原則であるため、継承をあまり使わない現代の設計では適用場面が限られます。

批判2: 数学的な厳密さが実務と乖離

LSPの元となる Barbara Liskov の定義は数学的に厳密ですが、実務では以下の問題があります。

  • 「振る舞いが変わらない」の境界が不明確
  • パフォーマンス特性の違いは「置換可能」に含まれるか?
  • 例外の種類が変わる場合は?

I: インターフェース分離の原則(ISP)への批判

批判1: 細かすぎるインターフェースの乱立

ISPを厳格に適用すると、インターフェースが細分化されすぎて管理が困難になります。

typescript
// 過度に分離された例
interface Readable {
	read(): string;
}
interface Writable {
	write(data: string): void;
}
interface Closable {
	close(): void;
}
interface Seekable {
	seek(position: number): void;
}
interface Flushable {
	flush(): void;
}

// 実装クラスは複数のインターフェースを実装
class FileHandler implements Readable, Writable, Closable, Seekable, Flushable {
	// 5つのインターフェースを意識しながら実装
}

問題点:

  • どのインターフェースの組み合わせを使うべきか判断が難しい
  • インターフェース間の関係性が見えにくい
  • ファイル数・型定義が増えて管理コストが上がる

D: 依存性逆転の原則(DIP)への批判

批判1: 不必要な抽象化の増加

すべての依存関係を抽象に向けると、シンプルなコードが複雑になります。

typescript
// DIPを過剰適用した例
interface ILogger {
	log(message: string): void;
}
interface IConfig {
	get(key: string): string;
}
interface IDateProvider {
	now(): Date;
}

class UserService {
	constructor(
		private logger: ILogger,
		private config: IConfig,
		private dateProvider: IDateProvider,
	) {}
}

// 実際には console.log, process.env, new Date() で十分な場合も多い

批判2: テスタビリティのための過剰設計

「テスト可能にするためにクラスを柔軟にする必要があるなら、ただそう言ってほしい」

DIPは「テストのため」に正当化されることが多いですが、それなら最初からそう明言すべきという批判があります。 テストのためだけに抽象化を導入するかどうかは、プロジェクトの規模や要件によって判断すべきです。

よくある誤解

誤解1: 「SOLID原則は常に守るべきルール」

現実: SOLID原則は ガイドライン であり、絶対的なルール ではありません。

状況に応じて、あえて原則に従わない選択も正当化されます。

状況推奨アプローチ
プロトタイプ開発スピード重視、後からリファクタリング
小規模スクリプトシンプルさ重視
パフォーマンスクリティカル最適化のために原則を緩める
変更が見込まれない領域過剰な抽象化を避ける

誤解2: 「原則を守れば良いコードになる」

原則を守ることと、良いコードを書くことはイコールではありません。

本当に重要なこと:

  • コードが 読みやすい
  • 意図が明確
  • 変更しやすい か(必要な箇所が)
  • チームが理解できる

誤解3: 「5つの原則は等しく重要」

実務での重要度には差があるという意見もあります。

原則適用頻度適用場面
SRP★★★★★ほぼ常に意識すべき
DIP★★★★☆依存関係の管理に重要
ISP★★★☆☆インターフェース設計時
OCP★★☆☆☆変更パターンが明確な時
LSP★★☆☆☆継承を使う時のみ

批判を踏まえた現実的なアプローチ

1. YAGNI を忘れない

「必要になったときに柔軟性を追加するのが、必要になるかもしれないからと柔軟性を追加するよりも常に良い」

2. コンテキストを考慮する

  • チームのスキルレベル: 複雑な抽象化を全員が理解できるか
  • プロジェクトの寿命: 短期プロジェクトなら過剰設計を避ける
  • 変更の頻度: 実際に変更が多い箇所に注力する
  • ドメインの特性: ビジネスロジックの複雑さに応じて判断

3. リファクタリングを前提とする

完璧な設計を最初から目指すのではなく、以下のサイクルを回します。

実装 → 変更要求 → 痛みを感じる → リファクタリング → 原則を適用

4. トレードオフを明確にする

設計の決定には必ずトレードオフがあります。

原則を適用メリットデメリット
SRP変更の影響範囲が小さいクラス数が増える
OCP既存コードを触らない抽象化のコストがかかる
DIPテストしやすいインターフェースが増える

まとめ

SOLID原則は有用なガイドラインですが、批判や限界を理解することで、より実践的な設計ができるようになります。

覚えておくべきこと:

  1. 原則は「ルール」ではなく「ガイドライン」
  2. 過剰適用は過剰設計を招く
  3. コンテキストに応じて判断する
  4. 読みやすさ・シンプルさとのバランスを取る
  5. リファクタリングを通じて段階的に改善する

「トレードオフを理解し、SOLID原則にこだわらずに、自分のニーズと価値観に基づいて選択すること」

参考文献

Released under the CC-BY-4.0 license.