SOLID原則とは
SOLID原則は、保守性・拡張性の高いソフトウェア設計を実現するための5つの基本原則です。
Robert C. Martin(通称 Uncle Bob)が提唱しました。
🔹 S - 単一責任の原則(SRP: Single Responsibility Principle)
1つのクラスは、1つの責任のみを持つべき。
❌ 悪い例
class UserService {
saveUser(user: string) {
console.log(`Save user: ${user}`);
}
sendEmail(user: string) {
console.log(`Send email to ${user}`);
}
}
✅ 良い例
class UserRepository {
save(user: string) {
console.log(`Saved user: ${user}`);
}
}
class EmailService {
sendEmail(user: string) {
console.log(`Send email to ${user}`);
}
}
class UserService {
constructor(
private repo: UserRepository,
private mailer: EmailService
) {}
register(user: string) {
this.repo.save(user);
this.mailer.sendEmail(user);
}
}
🔹 O - オープン/クローズドの原則(OCP: Open/Closed Principle)
クラスは拡張に対して開かれ、修正に対して閉じられるべき。
❌ 悪い例
class Discount {
getPrice(type: string, price: number): number {
if (type === 'silver') return price * 0.9;
if (type === 'gold') return price * 0.8;
return price;
}
}
✅ 良い例
interface DiscountStrategy {
apply(price: number): number;
}
class SilverDiscount implements DiscountStrategy {
apply(price: number): number {
return price * 0.9;
}
}
class GoldDiscount implements DiscountStrategy {
apply(price: number): number {
return price * 0.8;
}
}
class PriceCalculator {
constructor(private strategy: DiscountStrategy) {}
calculate(price: number): number {
return this.strategy.apply(price);
}
}
🔹 L - リスコフの置換原則(LSP: Liskov Substitution Principle)
親クラスのオブジェクトは子クラスに置き換えても動作が破綻しないべき。
❌ 悪い例
class Bird {
fly() {
console.log("Flying");
}
}
class Penguin extends Bird {
fly() {
throw new Error("Penguins cannot fly");
}
}
✅ 良い例
interface Bird {
eat(): void;
}
interface FlyingBird extends Bird {
fly(): void;
}
class Sparrow implements FlyingBird {
eat() { console.log("Eating"); }
fly() { console.log("Flying"); }
}
class Penguin implements Bird {
eat() { console.log("Eating"); }
}
🔹 I - インターフェース分離の原則(ISP: Interface Segregation Principle)
使わないメソッドへの依存を強制されるべきでない。
❌ 悪い例
interface Machine {
print(): void;
scan(): void;
fax(): void;
}
class SimplePrinter implements Machine {
print() { console.log("Print"); }
scan() { throw new Error("Not supported"); }
fax() { throw new Error("Not supported"); }
}
✅ 良い例
interface Printer {
print(): void;
}
interface Scanner {
scan(): void;
}
class SimplePrinter implements Printer {
print() { console.log("Print"); }
}
class MultiFunctionMachine implements Printer, Scanner {
print() { console.log("Print"); }
scan() { console.log("Scan"); }
}
🔹 D - 依存関係逆転の原則(DIP: Dependency Inversion Principle)
高水準モジュールは低水準モジュールに依存せず、抽象に依存すべき。
❌ 悪い例
class MySQLUserRepository {
save(user: string) {
console.log(`Save ${user} to MySQL`);
}
}
class UserService {
private repo = new MySQLUserRepository(); // tightly coupled
}
✅ 良い例
interface UserRepository {
save(user: string): void;
}
class MySQLUserRepository implements UserRepository {
save(user: string) {
console.log(`Save ${user} to MySQL`);
}
}
class UserService {
constructor(private repo: UserRepository) {}
register(user: string) {
this.repo.save(user);
}
}
// 実行
const repo = new MySQLUserRepository();
const service = new UserService(repo);
service.register("Taro");
✅ まとめ
原則 | 名前 | 内容 | メリット |
---|---|---|---|
SRP | 単一責任の原則 | クラスは1つの責任だけ持つべき | 修正箇所の特定が簡単になる |
OCP | オープン/クローズド原則 | 拡張できるが、修正は不要に | 安全な機能追加ができる |
LSP | リスコフの置換原則 | 子クラスは親クラスとして使えるべき | 継承によるバグを防ぐ |
ISP | インターフェース分離原則 | 不要な依存を避けるべき | 柔軟な実装が可能になる |
DIP | 依存関係逆転の原則 | 抽象に依存する | モジュールの再利用性・テスト性向上 |
ユーザーサービスのCRUD実装で再現
ユーザーサービスのCRUD(Create / Read / Update / Delete)機能を、SOLID原則に従って保守性・テスト性・拡張性を意識した設計で実装する方法を解説します。
📦 ディレクトリ構成(例)
src/
├── domain/
│ └── user/
│ ├── User.ts
│ ├── UserRepository.ts
├── usecase/
│ └── user/
│ ├── CreateUserService.ts
│ ├── GetUserService.ts
│ ├── UpdateUserService.ts
│ └── DeleteUserService.ts
├── infrastructure/
│ └── user/
│ └── InMemoryUserRepository.ts
└── controller/
└── UserController.ts
🧱 Entity(SRP)
User.ts
export class User {
constructor(
public readonly id: string,
public name: string,
public email: string
) {}
changeName(newName: string) {
this.name = newName;
}
changeEmail(newEmail: string) {
this.email = newEmail;
}
}
- 単一責任の原則(SRP)に従い、ユーザーの振る舞いを管理する責任だけを持ちます。
🔌 Repository(DIP)
UserRepository.ts
import { User } from "./User";
export interface UserRepository {
findById(id: string): Promise<User | null>;
findAll(): Promise<User[]>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
- 抽象クラスを定義して依存関係逆転の原則(DIP) を実現。
InMemoryUserRepository.ts
(実装例)
import { UserRepository } from "../../domain/user/UserRepository";
import { User } from "../../domain/user/User";
export class InMemoryUserRepository implements UserRepository {
private users = new Map<string, User>();
async findById(id: string): Promise<User | null> {
return this.users.get(id) ?? null;
}
async findAll(): Promise<User[]> {
return Array.from(this.users.values());
}
async save(user: User): Promise<void> {
this.users.set(user.id, user);
}
async delete(id: string): Promise<void> {
this.users.delete(id);
}
}
🧠 UseCase層(SRP + OCP)
各ユースケースを1ファイル1クラスで実装し、単一責任 + 拡張可能性を担保します。
CreateUserService.ts
import { UserRepository } from "../../domain/user/UserRepository";
import { User } from "../../domain/user/User";
import { v4 as uuidv4 } from "uuid";
export class CreateUserService {
constructor(private readonly userRepository: UserRepository) {}
async execute(name: string, email: string): Promise<string> {
const user = new User(uuidv4(), name, email);
await this.userRepository.save(user);
return user.id;
}
}
同様に、以下のような形で他のユースケースも設計します:
GetUserService.ts
UpdateUserService.ts
DeleteUserService.ts
📡 Controller(DIP + ISP)
UserController.ts
import express, { Request, Response } from "express";
import { InMemoryUserRepository } from "../infrastructure/user/InMemoryUserRepository";
import { CreateUserService } from "../usecase/user/CreateUserService";
import { GetUserService } from "../usecase/user/GetUserService";
// インスタンス生成(DI)
const userRepository = new InMemoryUserRepository();
const createUserService = new CreateUserService(userRepository);
const getUserService = new GetUserService(userRepository);
export const userRouter = express.Router();
userRouter.post("/", async (req: Request, res: Response) => {
const { name, email } = req.body;
const id = await createUserService.execute(name, email);
res.status(201).json({ id });
});
userRouter.get("/:id", async (req: Request, res: Response) => {
const user = await getUserService.execute(req.params.id);
if (!user) return res.status(404).send();
res.json(user);
});
- ユースケースのインターフェースだけに依存し、Controllerはビジネスロジックを知らない状態に保つ。
🧪 補足:テストでのDI活用
const mockRepo: UserRepository = {
findById: jest.fn(),
findAll: jest.fn(),
save: jest.fn(),
delete: jest.fn()
};
const service = new CreateUserService(mockRepo);
- モックの注入が容易になることで、テストの独立性と信頼性が向上します。
🚀 おわりに
このようにSOLID原則を意識してCRUDを設計・実装することで、
- コードの見通しが良くなる
- 修正時の影響範囲が局所化できる
- 自動テストが書きやすくなる
といった多くのメリットがあります。
少しでも参考になれば幸いです。