SOLIDとは
2000年代初頭にロバート・C・マーチンによって整理された、オブジェクト指向設計において、変更に強く、理解しやすく、再利用性の高いソフトウェアを作るための5つの原則です。
以下5つの原則が定義されています。
- S:単一責任の原則 (Single Responsibility Principle)
- O:開放閉鎖の原則 (Open-Closed Principle)
- L:リスコフの置換原則 (Liskov Substitution Principle)
- I:インターフェース分離の原則 (Interface Segregation Principle)
- D:依存性逆転の原則 (Dependency Inversion Principle)
S:単一責任原則(SRP)
クラスを変更する理由は、たったひとつであるべきという考え方です。 1つのクラスに「データの保存」「ログの出力」「計算処理」など、複数の役割を持たせすぎないようにします。
→ 特定の機能を修正した際に、無関係なはずの場所でバグが発生する「予期せぬ副作用」と、コードの複雑化を防ぎます。
❌ 一つのクラスに複数の機能:
class UserService {
saveUser() {}
sendEmail() {}
}
⭕ 一つのクラスに単一の機能:
class UserRepository {
save() {}
}
class EmailService {
send() {}
}
O:開放閉鎖原則(OCP)
拡張に対しては開いており、修正に対しては閉じているべきという考え方です。
- 拡張に対して開いている(Open): 新しい機能や振る舞いを自由に追加できること。
- 修正に対して閉じている(Closed): 新しい機能を追加するときに、すでに動いている(テスト済みの)ソースコードを書き換えなくてよいこと。
→ 新機能を追加するたびに既存のテスト済みコードを書き換えることで発生する、バグの混入(デグレード)と修正作業の連鎖を防ぎます。
❌ if を追加し続ける:
if (type === "A") {}
else if (type === "B") {}
⭕ 抽象的なクラス(インターフェース)の利用:
// ポリモーフィズムを使う
interface Strategy {
execute(): void
}
// 処理ごとにクラスを分ける
class StrategyA implements Strategy {
execute() {
console.log("Aの処理")
}
}
class StrategyB implements Strategy {
execute() {
console.log("Bの処理")
}
}
// 呼び出し側
function run(strategy: Strategy) {
strategy.execute()
}
run(new StrategyA())
run(new StrategyB())
L:リスコフの置換原則(LSP)
プログラムの中の、ある型(親クラス)のオブジェクトを、その派生型(子クラス)で置き換えても、正しく動作しなければならないという考え方です。簡単にいうと、「親クラスができることは、子クラスも100%同じようにできなきゃダメ」ということです。
→ 壊れた継承(開発者が「共通点があるから」という理由だけで安易に継承を使うこと)による、プログラムが予期せぬ動きをするバグを防ぎます。
❌ 子クラスで親クラスの操作が動かない:
// 親クラス
class Animal {
move() {
console.log("動く")
}
}
// 子クラス
class Snake extends Animal {
move() {
throw new Error("動けません")
}
}
// 使う側
function run(animal: Animal) {
animal.move()
}
run(new Snake()) // 実行時エラー
⭕ 親クラスの操作が子クラスで動く:
abstract class Animal {
abstract move(): void
}
class Dog extends Animal {
move() {
console.log("走る")
}
}
class Bird extends Animal {
move() {
console.log("飛ぶ")
}
}
run(new Dog())
run(new Bird())
I:インターフェース分離原則(ISP)
利用しないメソッドを、利用者に強制的に実装させてはいけないという考え方です。巨大で汎用的なインターフェースを1つ作るのではなく、クライアントが必要とする機能だけに絞った小さなインターフェースに分割します。
→「自分には関係ない機能」まで実装(あるいは依存)せざるを得なくなるという問題を解決します。
❌ 太いインターフェース:
interface Machine {
print()
scan()
fax()
}
⭕ 小さなインターフェース:
interface Printer { print() }
interface Scanner { scan() }
D:依存性逆転原則(DIP)
上位モジュールは下位モジュールに依存してはならない。両者は抽象に依存すべきであるという原則です。 具体的なクラス(実装)に直接依存するのではなく、インターフェース(抽象)を介してやり取りするようにします。
❌ 直接依存する:
class Service {
printer = new InkPrinter()
}
⭕ インターフェースが仲介する:
interface Printer {
print(): void
}
// 実体
class InkPrinter implements Printer {
print() {
console.log("インクで印刷")
}
}
// Service
class Service {
constructor(private printer: Printer) {}
run() {
this.printer.print()
}
}