SOLID原則とは
SOLIDは、Robert C. Martin(Uncle Bob)が提唱したオブジェクト指向設計の5つの原則です。保守性、拡張性、テスト容易性の高いソフトウェアを設計するための指針となります。
S - Single Responsibility Principle(単一責任の原則)
O - Open/Closed Principle(開放閉鎖の原則)
L - Liskov Substitution Principle(リスコフの置換原則)
I - Interface Segregation Principle(インターフェース分離の原則)
D - Dependency Inversion Principle(依存性逆転の原則)
1. 単一責任の原則(SRP)
クラスは1つの責任のみを持つべきです。変更する理由は1つだけであるべきです。
違反例
// 悪い例: 複数の責任を持つクラス
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// 責任1: ユーザーデータの管理
getName() { return this.name; }
setName(name) { this.name = name; }
// 責任2: データベース操作
save() {
db.query('INSERT INTO users ...');
}
// 責任3: メール送信
sendWelcomeEmail() {
emailService.send(this.email, 'Welcome!');
}
// 責任4: JSONシリアライズ
toJSON() {
return JSON.stringify({ name: this.name, email: this.email });
}
}
改善例
// 良い例: 責任を分離
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getName() { return this.name; }
setName(name) { this.name = name; }
}
class UserRepository {
save(user) {
db.query('INSERT INTO users ...');
}
}
class UserNotificationService {
sendWelcomeEmail(user) {
emailService.send(user.email, 'Welcome!');
}
}
class UserSerializer {
toJSON(user) {
return JSON.stringify({ name: user.name, email: user.email });
}
}
2. 開放閉鎖の原則(OCP)
ソフトウェアは拡張に対して開いていて、修正に対して閉じているべきです。
違反例
// 悪い例: 新しい支払い方法を追加するたびに修正が必要
class PaymentProcessor {
processPayment(type, amount) {
if (type === 'credit') {
// クレジットカード処理
} else if (type === 'debit') {
// デビットカード処理
} else if (type === 'paypal') {
// PayPal処理
}
// 新しい支払い方法を追加するには、このメソッドを修正
}
}
改善例
// 良い例: 拡張に対して開いている
class PaymentProcessor {
constructor(paymentMethod) {
this.paymentMethod = paymentMethod;
}
processPayment(amount) {
return this.paymentMethod.process(amount);
}
}
// 各支払い方法は独立したクラス
class CreditCardPayment {
process(amount) { /* クレジットカード処理 */ }
}
class PayPalPayment {
process(amount) { /* PayPal処理 */ }
}
// 新しい支払い方法の追加は既存コードを変更せずに可能
class CryptoPayment {
process(amount) { /* 暗号通貨処理 */ }
}
// 使用
const processor = new PaymentProcessor(new CryptoPayment());
processor.processPayment(1000);
3. リスコフの置換原則(LSP)
派生クラスは、基底クラスと置換可能でなければなりません。
違反例
// 悪い例: 正方形は長方形の一種だが、振る舞いが異なる
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) { this.width = width; }
setHeight(height) { this.height = height; }
getArea() { return this.width * this.height; }
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width; // 正方形なので高さも変更
}
setHeight(height) {
this.width = height; // 正方形なので幅も変更
this.height = height;
}
}
// 問題のあるコード
function increaseRectangleWidth(rect) {
rect.setWidth(rect.width + 1);
// Rectangleなら: area = (width + 1) * height
// Squareなら: area = (width + 1) * (width + 1) ← 予期しない結果
}
改善例
// 良い例: 共通のインターフェースを使用
class Shape {
getArea() { throw new Error('Not implemented'); }
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() { return this.width * this.height; }
}
class Square extends Shape {
constructor(side) {
super();
this.side = side;
}
getArea() { return this.side * this.side; }
}
4. インターフェース分離の原則(ISP)
クライアントは、使用しないメソッドに依存させられるべきではありません。
違反例
// 悪い例: 巨大なインターフェース
class Worker {
work() { /* 仕事をする */ }
eat() { /* 食事をする */ }
sleep() { /* 睡眠をとる */ }
}
class Robot extends Worker {
work() { /* 仕事をする */ }
eat() { throw new Error('Robots do not eat'); } // 不要
sleep() { throw new Error('Robots do not sleep'); } // 不要
}
改善例
// 良い例: インターフェースを分離
class Workable {
work() { throw new Error('Not implemented'); }
}
class Eatable {
eat() { throw new Error('Not implemented'); }
}
class Sleepable {
sleep() { throw new Error('Not implemented'); }
}
class Human {
work() { /* 仕事をする */ }
eat() { /* 食事をする */ }
sleep() { /* 睡眠をとる */ }
}
class Robot {
work() { /* 仕事をする */ }
// eat()とsleep()は不要
}
5. 依存性逆転の原則(DIP)
上位モジュールは下位モジュールに依存すべきではなく、両者は抽象に依存すべきです。
違反例
// 悪い例: 具体的な実装に直接依存
class UserService {
constructor() {
this.database = new MySQLDatabase(); // 具体的な実装に依存
}
getUser(id) {
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
改善例
// 良い例: 抽象(インターフェース)に依存
class Database {
query(sql) { throw new Error('Not implemented'); }
}
class MySQLDatabase extends Database {
query(sql) { /* MySQL実装 */ }
}
class PostgreSQLDatabase extends Database {
query(sql) { /* PostgreSQL実装 */ }
}
class UserService {
constructor(database) {
this.database = database; // 抽象に依存(依存性注入)
}
getUser(id) {
return this.database.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// 使用
const userService = new UserService(new PostgreSQLDatabase());
// テスト時
const mockDatabase = { query: jest.fn() };
const testService = new UserService(mockDatabase);
SOLIDの実践
チェックリスト
□ クラスの責任は1つか?(SRP)
□ 新機能追加時に既存コードを修正せずに済むか?(OCP)
□ 派生クラスを基底クラスの代わりに使えるか?(LSP)
□ インターフェースは最小限か?(ISP)
□ 具体的な実装ではなく抽象に依存しているか?(DIP)
適用のバランス
過度な適用は避ける:
- 単純な問題に複雑なパターンを適用しない
- YAGNI(You Ain't Gonna Need It)を意識
- 必要に応じて段階的にリファクタリング
まとめ
SOLID原則は、保守性と拡張性の高いソフトウェアを設計するための指針です。すべての原則を常に厳密に適用する必要はありませんが、設計の判断基準として意識することで、より良いコードを書くことができます。
← 一覧に戻る