SOLID 原則
SOLID原則は、オブジェクト指向プログラミングにおける設計原則です。TypeScriptでこれらの原則を適用する方法を学びましょう。
SOLID原則とは
SOLID原則は以下の5つの原則の頭文字を取ったものです:
- Single Responsibility Principle(単一責任の原則)
- Open-Closed Principle(オープン・クローズドの原則)
- Liskov Substitution Principle(リスコフの置換原則)
- Interface Segregation Principle(インターフェース分離の原則)
- Dependency Inversion Principle(依存性逆転の原則)
単一責任の原則(SRP)
クラスは単一の責任のみを持つべきです。
Bad
❌ Bad: 複数の責任を持つクラス
// ❌ 複数の責任を持つクラス
class TodoList {
private items: string[] = [];
addItem(text: string): void {
this.items.push(text);
}
removeItem(index: number): void {
this.items.splice(index, 1);
}
toString(): string {
return this.items.join('\n');
}
// ファイル操作の責任も持っている
saveToFile(filename: string): void {
// ファイルを保存する処理
}
loadFromFile(filename: string): void {
// ファイルを読み込む処理
}
}
Good
✅ Good: 責任を分離
// ✅ 責任を分離
class TodoList {
private items: string[] = [];
addItem(text: string): void {
this.items.push(text);
}
removeItem(index: number): void {
this.items.splice(index, 1);
}
getItems(): string[] {
return [...this.items];
}
toString(): string {
return this.items.join('\n');
}
}
class FileManager {
save(filename: string, data: string): void {
// ファイルを保存する処理
}
load(filename: string): string {
// ファイルを読み込む処理
return '';
}
}
// 使用時
const todoList = new TodoList();
const fileManager = new FileManager();
todoList.addItem('Buy groceries');
fileManager.save('todos.txt', todoList.toString());
オープン・クローズドの原則(OCP)
クラスは拡張に対して開いていて、修正に対して閉じているべきです。
Bad
❌ Bad: 新しい支払い方法を追加するたびに修正が必要
// ❌ 新しい支払い方法を追加するたびに修正が必要
class PaymentProcessor {
processPayment(amount: number, method: string): void {
if (method === 'creditCard') {
console.log(`Processing credit card payment of ${amount}`);
} else if (method === 'paypal') {
console.log(`Processing PayPal payment of ${amount}`);
} else if (method === 'bitcoin') {
// 新しい支払い方法を追加するたびに修正が必要
console.log(`Processing Bitcoin payment of ${amount}`);
}
}
}
Good
✅ Good: 拡張に開いていて、修正に閉じている
// ✅ 拡張に開いていて、修正に閉じている
interface PaymentMethod {
process(amount: number): void;
}
class CreditCardPayment implements PaymentMethod {
process(amount: number): void {
console.log(`Processing credit card payment of ${amount}`);
}
}
class PayPalPayment implements PaymentMethod {
process(amount: number): void {
console.log(`Processing PayPal payment of ${amount}`);
}
}
// 新しい支払い方法を追加しても既存コードを修正する必要がない
class BitcoinPayment implements PaymentMethod {
process(amount: number): void {
console.log(`Processing Bitcoin payment of ${amount}`);
}
}
class PaymentProcessor {
processPayment(amount: number, method: PaymentMethod): void {
method.process(amount);
}
}
// 使用時
const processor = new PaymentProcessor();
processor.processPayment(100, new CreditCardPayment());
processor.processPayment(200, new BitcoinPayment());
リスコフの置換原則(LSP)
派生クラスは、基底クラスと置換可能であるべきです。
Bad
❌ Bad: 派生クラスが基底クラスの契約を破っている
// ❌ 派生クラスが基底クラスの契約を破っている
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(size: number) {
super(size, size);
}
// 正方形は幅と高さが同じでなければならないため、契約を破る
setWidth(width: number): void {
this.width = width;
this.height = width; // 高さも変更される
}
setHeight(height: number): void {
this.width = height;
this.height = height; // 幅も変更される
}
}
// 問題が発生
function resizeRectangle(rect: Rectangle): void {
rect.setWidth(10);
rect.setHeight(5);
console.log(rect.getArea()); // Rectangle: 50, Square: 25(予期しない結果)
}
Good
✅ Good: 共通のインターフェースを定義し、それぞれ独立したクラスとして実装
// ✅ 共通のインターフェースを定義し、それぞれ独立したクラスとして実装
interface Shape {
getArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
setWidth(width: number): void {
this.width = width;
}
setHeight(height: number): void {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square implements Shape {
constructor(private size: number) {}
setSize(size: number): void {
this.size = size;
}
getArea(): number {
return this.size * this.size;
}
}
// Shape を扱う関数は両方で正しく動作する
function printArea(shape: Shape): void {
console.log(`Area: ${shape.getArea()}`);
}
インターフェース分離の原則(ISP)
クライアントは、使用しないメソッドに依存することを強制されるべきではありません。
Bad
❌ Bad: 大きすぎるインターフェース
// ❌ 大きすぎるインターフェース
// (Worker はブラウザ標準の型と同名のため WorkerRole とする)
interface WorkerRole {
work(): void;
eat(): void;
sleep(): void;
}
class Human implements WorkerRole {
work(): void {
console.log('Working...');
}
eat(): void {
console.log('Eating...');
}
sleep(): void {
console.log('Sleeping...');
}
}
class Robot implements WorkerRole {
work(): void {
console.log('Working...');
}
eat(): void {
// ロボットは食べない!
throw new Error('Robots do not eat');
}
sleep(): void {
// ロボットは眠らない!
throw new Error('Robots do not sleep');
}
}
Good
✅ Good: インターフェースを分離
// ✅ インターフェースを分離
interface Workable {
work(): void;
}
interface Eatable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
class Human implements Workable, Eatable, Sleepable {
work(): void {
console.log('Working...');
}
eat(): void {
console.log('Eating...');
}
sleep(): void {
console.log('Sleeping...');
}
}
class Robot implements Workable {
work(): void {
console.log('Working...');
}
// eat と sleep は不要
}
依存性逆転の原則(DIP)
上位モジュールは下位モジュールに依存すべきではなく、両者は抽象に依存すべきです。
Bad
❌ Bad: 具象クラスに直接依存
// ❌ 具象クラスに直接依存
class MySQLDatabase {
save(data: string): void {
console.log(`Saving to MySQL: ${data}`);
}
}
class UserRepository {
private database = new MySQLDatabase(); // 具象クラスに依存
saveUser(user: string): void {
this.database.save(user);
}
}
Good
✅ Good: 抽象(インターフェース)に依存
// ✅ 抽象(インターフェース)に依存
interface Database {
save(data: string): void;
}
class MySQLDatabase implements Database {
save(data: string): void {
console.log(`Saving to MySQL: ${data}`);
}
}
class PostgreSQLDatabase implements Database {
save(data: string): void {
console.log(`Saving to PostgreSQL: ${data}`);
}
}
class UserRepository {
// インターフェースに依存(依存性注入)
constructor(private database: Database) {}
saveUser(user: string): void {
this.database.save(user);
}
}
// 使用時(データベースを簡単に切り替え可能)
const mysqlRepo = new UserRepository(new MySQLDatabase());
const postgresRepo = new UserRepository(new PostgreSQLDatabase());
練習問題
以下のコードをSOLID原則に従ってリファクタリングしてください
// 問題
class ReportGenerator {
private data: any[];
constructor(data: any[]) {
this.data = data;
}
generatePdfReport(): void {
console.log('Generating PDF report...');
// PDF生成ロジック
}
generateExcelReport(): void {
console.log('Generating Excel report...');
// Excel生成ロジック
}
generateHtmlReport(): void {
console.log('Generating HTML report...');
// HTML生成ロジック
}
saveToFile(filename: string): void {
console.log(`Saving to ${filename}...`);
}
sendByEmail(email: string): void {
console.log(`Sending to ${email}...`);
}
}
解答:
✅ Good: SOLID原則に従ったリファクタリング
// ✅ SOLID原則に従ったリファクタリング
// データを持つクラス(単一責任)
interface ReportData {
title: string;
content: string[];
}
// レポートフォーマッタのインターフェース(OCP, ISP)
interface ReportFormatter {
format(data: ReportData): string;
}
class PdfFormatter implements ReportFormatter {
format(data: ReportData): string {
return `[PDF] ${data.title}\n${data.content.join('\n')}`;
}
}
class ExcelFormatter implements ReportFormatter {
format(data: ReportData): string {
return `[Excel] ${data.title}\n${data.content.join(',')}`;
}
}
class HtmlFormatter implements ReportFormatter {
format(data: ReportData): string {
return `<html><h1>${data.title}</h1><p>${data.content.join('</p><p>')}</p></html>`;
}
}
// 出力先のインターフェース(OCP, ISP)
interface ReportOutput {
output(content: string, destination: string): void;
}
class FileOutput implements ReportOutput {
output(content: string, destination: string): void {
console.log(`Saving to file: ${destination}`);
// ファイル保存ロジック
}
}
class EmailOutput implements ReportOutput {
output(content: string, destination: string): void {
console.log(`Sending email to: ${destination}`);
// メール送信ロジック
}
}
// レポート生成クラス(DIP - 抽象に依存)
class ReportGenerator {
constructor(
private formatter: ReportFormatter,
private output: ReportOutput
) {}
generate(data: ReportData, destination: string): void {
const formattedContent = this.formatter.format(data);
this.output.output(formattedContent, destination);
}
}
// 使用例
const data: ReportData = {
title: 'Monthly Report',
content: ['Item 1', 'Item 2', 'Item 3'],
};
const pdfToFile = new ReportGenerator(new PdfFormatter(), new FileOutput());
pdfToFile.generate(data, 'report.pdf');
const htmlToEmail = new ReportGenerator(new HtmlFormatter(), new EmailOutput());
htmlToEmail.generate(data, 'user@example.com');
適用した原則:
- SRP: データ、フォーマット、出力を分離
- OCP: 新しいフォーマットや出力先を追加しやすい
- LSP: 各実装はインターフェースの契約を守る
- ISP: インターフェースを小さく保つ
- DIP: 具象ではなく抽象に依存
SOLID原則のまとめ
| 略字 | 原則 | 概要 |
|---|---|---|
| S | 単一責任 | クラスは 1 つの責任だけ持つ |
| O | 開放閉鎖 | 拡張に開き、修正に閉じる |
| L | リスコフ | 派生クラスは置換可能である |
| I | 分離 | 必要なインターフェースだけ提供 |
| D | 依存逆転 | 抽象に依存し、具象に依存しない |
次に読む
- クラス設計 — SOLID を支えるクラスの基礎
- TypeScript Tips — 型システムで原則を表現する