SOLID原則への批判と限界
SOLID原則は広く受け入れられている設計指針ですが、万能ではありません。
このページでは、SOLID原則に対する主要な批判、適用の限界、そしてよくある誤解を整理し、より実践的な設計判断ができるようになることを目指します。
なぜ批判を学ぶべきか?
SOLID原則を「絶対的なルール」として盲目的に適用すると、かえって問題を引き起こすことがあります。 批判的な視点を持つことで、以下のメリットが得られます。
- 適切な場面で適切な原則を選択できる
- 過剰設計(オーバーエンジニアリング)を避けられる
- チームでの設計議論がより建設的になる
- トレードオフを意識した現実的な判断ができる
各原則への主要な批判
S: 単一責任の原則(SRP)への批判
批判1: 「アクター」の特定が難しい
SRPの正確な定義は「クラスはたったひとりのアクター(利用者・責任者) に対して責任を持つべき」です。 しかし、実務ではアクターの特定自体が困難なケースがあります。
問題点:
- 組織構造が明確でない場合、誰が「アクター」か判断しにくい
- 同じ機能を複数の部署が利用する場合、アクターの境界が曖昧になる
- スタートアップなど少人数チームでは、アクターが実質的に重複する
例: レポート生成クラス
class ReportGenerator {
calculateMetrics(): Metrics {
/* ... */
}
formatAsHtml(): string {
/* ... */
}
sendToStakeholders(): void {
/* ... */
}
}calculateMetrics()→ 経理部門がアクターformatAsHtml()→ マーケティング部門がアクターsendToStakeholders()→ 経営層がアクター
SRPに従えば3つのクラスに分割すべきですが、小規模プロジェクトで経理もマーケも同じ人が担当している場合、分割のメリットは薄れます。アクターが組織的に分離していない段階で分割すると、かえって複雑さが増すことがあります。
批判2: 過度な分割がコードを複雑にする
アクターを厳密に分離しようとすると、小さなクラスが大量に生まれ、かえって理解しにくくなることがあります。
// 過度に分割された例
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) に違反しがちです。
// 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を厳格に適用すると、インターフェースが細分化されすぎて管理が困難になります。
// 過度に分離された例
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: 不必要な抽象化の増加
すべての依存関係を抽象に向けると、シンプルなコードが複雑になります。
// 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原則は有用なガイドラインですが、批判や限界を理解することで、より実践的な設計ができるようになります。
覚えておくべきこと:
- 原則は「ルール」ではなく「ガイドライン」
- 過剰適用は過剰設計を招く
- コンテキストに応じて判断する
- 読みやすさ・シンプルさとのバランスを取る
- リファクタリングを通じて段階的に改善する
「トレードオフを理解し、SOLID原則にこだわらずに、自分のニーズと価値観に基づいて選択すること」