🧑🏫
品質の高いソフトウェア開発のための18の設計
1. 単一責任の原則(SRP)⭐️
目的:1つのクラスが持つ責任は1つだけにし、変更理由を限定する
チェックリスト
- 各クラスの責任は一文で説明できるか
- 「そして」を使わずにクラスの責任を説明できるか
- クラスが変更される理由は1つだけか
- クラスのメソッドは同じ目的に向かっているか
- クラスのサイズは大きすぎないか(目安:200~300行以下)
悪い例と良い例
// 悪い例: 複数の責任を持つ
class UserService {
public registerUser(user: User): void {
// ユーザ登録
}
public sendEmail(to: string, subject: string, body: string): void {
// メール送信
}
public generateReport(): string {
// レポート生成
return '...レポート内容...';
}
}
// 良い例: 責任が分割されている
class UserService {
public registerUser(user: User): void {
// ユーザ登録のみ
}
}
class EmailService {
public sendEmail(to: string, subject: string, body: string): void {
// メール送信のみ
}
}
class ReportService {
public generateReport(): string {
// レポート生成のみ
return '...レポート内容...';
}
}
利点
- 変更が容易: 1つの機能変更が他の機能に影響しない
- 理解しやすい: 各クラスの目的が明確
- テストが容易: 機能が分離されているため単体テストが書きやすい
- 再利用性が向上: 特定の責任に集中したコンポーネントは他の場所でも使いやすい
2.関心の分離(SoC)⭐️
目的:異なる機能や関心事を明確に分けて個別のコンポーネントに割り当てる
チェックリスト
- 異なる種類の問題(UI, ビジネスロジック, データアクセスなど)が分離されているか
- 各レイヤーが明確な責任を持っているか
- コンポーネント間の依存関係が適切に管理されているか
- 各モジュールが独立してテスト可能か
- コードが機能的なまとまりで整理されているか
悪い例良い例
// 悪い例: UIとデータ処理とビジネスロジックが混在
function handleSubmit(): void {
// UIからデータ取得
const nameInput = document.getElementById('name') as HTMLInputElement;
const name = nameInput.value;
// バリデーション(ビジネスロジック)
if (name.length < 3) {
alert('名前は3文字以上必要です');
return;
}
// APIリクエスト(データアクセス)
fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ name })
}).then(response => {
const resultElement = document.getElementById('result');
if (resultElement) {
resultElement.innerHTML = '成功!';
}
});
}
// 良い例: 関心事が分離されている
// UIコンポーネント
function UserForm(): void {
function handleSubmit(userData: { name: string}): void {
if (userService.validateUser(userData)) {
userService.saveUser(userData)
.then(() => uIService.showSuccess());
} else {
uiService.showError('バリデーションエラー');
}
}
}
// ビジネスロジック
interface UserData {
name: string;
}
const userService = {
validateUser(user: UserData): boolean {
return user.name.length >= 3;
}
saveUser(user: UserData): Promise<void> {
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify(user)
}).then(() => {});
}
};
// UI操作
const uiService = {
showSuccess(): void {
const resultElement = document.getElementById('result');
if (resultElement) {
resultElement.innerHTML = '成功!';
}
},
showError(message: string): void {
alert(message);
}
};
利点
- 保守性の向上: 特定の機能だけを変更可能
- テスト容易性: 各コンポーネントを個別にテスト可能
- チーム開発の効率化: 異なる専門家が異なるレイヤーを担当可能
- 再利用性: コンポーネントが再利用しやすくなる
- 技術変更の容易さ: 例えばUIフレームワークだけを変更しやすい
3.デメテルの法則(最少知識の原則)
目的:オブジェクトは直接の「友人」とだけ会話し、内部構造の詳細は知らないようにする。
チェックリスト
- メソッドチェーン(ドットの連鎖)が3つ以上続いていないか
- クラスが直接関係のないオブジェクトと相互作用していないか
- 「自分のもの」「渡されたもの」「自分で作ったもの」以外を呼んでいないか
- 他のオブジェクトの内部構造に依存していないか
- コレクションを返す場合、変更不可能なコレクションを返しているか
悪い例良い例
// 型定義
interface Country {
code: string;
}
interface Address {
contry: Country;
}
interface Customer {
address: Address;
}
interface Order {
customer: Customer;
}
// 悪い例: ドットの連鎖
function getCountryCode(order: Order): string {
return order.customer.address.country.code;
}
// 良い例: ドメインモデル全体でデメテルの法則を適用
class Country {
private code: string;
constructor(code: string) {
this.code = code;
}
public getCode(): string {
return this.code;
}
}
class Address {
private country: Country;
constructor(country: Country) {
this.country = country;
}
public getCountryCode(): string {
return this.country.getCode();
}
}
class Customer {
private address: Address;
constructor(address: Address) {
this.address = address;
}
public getCountryCode(): string {
return this.address/getCountryCode();
}
}
class Order {
private customer: Customer;
constructor(customer: Customer) {
this.customer = customer;
}
public getCustomerCountryCode(): string {
return this.customer.getCountryCode();
}
}
// 呼び出し側のコード
const order = new Order();
const countryCode = order.getCustomerCountryCode();
利点
- 結合度の低減: オブジェクト間の依存関係が減少
- カプセル化の強化: 内部構造の詳細が隠蔽される
- 変更の影響範囲の限定: 内部実装が変わっても呼び出し側への影響が少ない
- テストの容易さ: モックやスタブの作成が簡単
- コードの理解しやすさ: 複雑な依存関係が減り、流れが明確になる
4. Tell, Don't Askの原則⭐️
目的:オブジェクトの状態を取得して判断するのではなく、オブジェクトに行動を指示する。
チェックリスト
- オブジェクトの状態を取得して判断するのではなく、行動を指示しているか
- if文でオブジェクトの状態を確認してから操作していないか
- 決定ロジックが適切なオブジェクトに配置されているか
- ゲッターの連鎖の後に条件分岐がないか
- ビジネスルールが適切なオブジェクトにカプセル化されているか
悪い例良い例
// 型定義
interface Account {
balance: number;
}
// 悪い例: 状態を確認して外部から判断
function withdrawMoney(account: Account, amount: number): boolean {
if (account.balance >= amount) {
account.balance -= amount;
return true;
} else {
return false;
}
}
// 良い例: 行動を指示
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
public withdraw(amount: number): boolean {
if (this.balance >= amount) {
this.balance -= amount;
return true;
}
return false;
}
}
// 呼び出し側のコード
const account = new BankAccount(1000);
const success = account.withdraw(500);
利点
- カプセル化の強化: オブジェクトの内部データと実装が保護される
- 責任の適切な配置: ビジネスロジックが適切なクラスに配置される
- 変更に強い: 内部実装を変更しても、インターフェースが同じなら呼び出し側に影響がない
- コードの意図が明確: どのような操作をしたいのかが直接的に表現される
- バグの減少: オブジェクトの整合性が保たれやすくなる
5.知識の漏洩防止⭐️
目的:実装の詳細や内部構造を外部に露出しない
チェックリスト
- 実装詳細がインターフェイスに漏れていないか
- 具体的な実装クラスではなく抽象型を使用しているか
- ビジネスルールが適切な場所にカプセル化されているか
- 内部データ構造が外部に露出していないか
- 変更可能なオブジェクトがそのまま返されていないか
悪い例良い例
// 悪い例: 内部データ構造の露出
class Department {
private employees: Employee[] = [];
public getEmployees(): Employee[] {
return this.employees; // 変更可能な配列を直接返している
}
}
// 良い例: 適切なカプセル化
class Department {
private employees: Employee[] = [];
public getEmployee(): ReadonlyArray<Employee> {
return [...this.employees]; // 読み取り専用コピーを返す
}
public addEmployee(employee: Employee): void {
this.employees.push(employee);
}
}
利点
- 変更に強い: 内部実装が変わっても外部に影響が少ない
- 一貫性の保持: データ操作が制御され、整合性が保たれる
- 理解しやすいインターフェース: 必要な情報だけが公開される
- テストの容易さ: 実装を変えてもテストに影響しにくい
- バグの減少: 不正な状態変更が防げる
6.開放/閉鎖原則(OCP)
目的:クラスは拡張に開かれ、修正に閉じていること
チェックリスト
- 拡張時に既存コードを修正せずに拡張できるか
- 継承やインターフェース実装を適切に使用しているか
- ポリモーフィズムを活用して条件分岐を減らしているか
- フックやコールバックなど拡張ポイントが容易されているか
- 将来の変更が予測される箇所は抽象化されているか
悪い例良い例
// 悪い例: 新しい図形を追加するたびに既存クラスを修正する必要がある
class AreaCalculator {
public calculateArea(shape: any): number {
if (shape instanceof Rectangle) {
return shape.width * shape.height;
}
else if (shape instanceof Circle) {
return Math.PI * shape.radius * shape.radius;
}
// 新しい図形を追加するたびにここを修正する必要がある
return 0;
}
}
// 良い例: 拡張に開かれ、修正に閉じている
interface Shape {
calculateArea(): number;
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
public calculateArea(): number {
return this.width * this.height;
}
}
class Circle implements Shape {
constructor(private radius: number) {}
public calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
}
// 新しい図形を追加してもこのクラスは変更不要
class AreaCalculator {
public calculateArea(shape: Shape): number {
return shape.calculateArea();
}
}
利点
- 保守性の向上: 既存コードの変更リスクが低減される
- 拡張の容易さ: 新機能の追加が既存機能に影響を与えない
- 並行開発の促進: 異なる開発者が独立して機能を追加できる
- テストの簡素化: 既存機能のリグレッションテストが不要になる
- コードの安定性: 変更の影響範囲が限定される
7.依存性逆転の原則(DIP)⭐️
目的:高レベルモジュールは低レベルモジュールに依存せず、両者ともに抽象に依存する。
チェックリスト
- 高レベルモジュールは低レベルモジュールの実装に直接依存していないか
- 両方のモジュールが抽象(インターフェース)に依存しているか
- 依存性の方向が適切か(ドメインロジック <= インフラストラクチャ)
- 依存性の注入を適切に使用しているか
- テスト時にモックやスタブに置き換え可能か
悪い例良い例
// 型定義
interface User {
id: number;
name: string
}
// 悪い例: UserServiceがMySQLRepositoryに直接依存
class MySQLUserRepository {
public findById(id: number): User {
// MySQLデータベースからユーザを検索
return {id, name: 'ユーザー' + id};
}
}
class UserService {
private repository = new MySQLUserRepository();
public findUser(id: number): User {
return this.repository.findById(id);
}
}
// 良い例: 抽象(インターフェース)への依存
interface UserRepository {
findById(id: number): User;
}
calss MySQLUserRepository implements UserRepository {
public findById(id: number): User {
// MySQLデータベースからユーザーを検索
return { id, name: 'ユーザー' + id };
}
}
class MongoUserRepository implements UserRepository {
public findById(id: number): User {
// MongoDBからユーザーを検索
return { id, name: 'ユーザー' + id };
}
}
class UserService {
// 依存性の注入
constructor(private repository: UserRepository) {}
public findUser(id: number): User {
return this.repositroy.findById(id);
}
}
// 使用例
const mysqlRepo = new MySQLUSerRepository();
const userService = new UserService(mysqlRepo);
利点
- モジュールの分離: 高レベルモジュールと低レベルモジュールの分離
- テスト容易性: 依存関係をモックに置き換え可能
- 柔軟性: 実装の詳細を簡単に変更出来る
- 並行開発: 異なるチームが独立して開発可能
- 再利用性: コンポーネントを異なるコンテキストで再利用しやすい
8.インターフェース分離の原則(ISP)
目的:クライアントは使用しないメソッドに依存させるべきではない。
チェックリスト
- インターフェースが小さく焦点を絞ったものになっているか
- クライアントが使用しないメソッドに依存していないか
- 大きなインターフェースを機能別に分割できないか
- クライアント固有のインターフェースになっているか
- 「ファットインターフェース」を避けているか
悪い例良い例
// 悪い例: 巨大で汎用的なインターフェース
interface Worker {
work(): void;
eat(): void;
sleep(): void;
takeBreak(): void;
receiveSalary(): void;
fileReport(): void;
attendMeeting(): void;
}
// RobotWorkerにはsleep()やeat()は不要だが実装する必要がある
class RobotWorker implements Worker {
public work(): void {/* 実装 */ }
public eat(): void {
// ロボットは食べられないが実装する必要がある
throw new Error("操作はサポートされていません")
}
public sleep(): void {
// ロボットは眠らないが実装する必要がある
throw new Error("操作はサポートされていません");
}
// 他のメソッド...
public takeBreak(): void { /* 実装 */ }
public receiveSalary(): void { /* 実装 */}
public fileReport(): void { /* 実装 */}
public attendMeeting(): void { /* 実装 */}
}
// 良い例: 分割された特化したインターフェース
interface Workable {
work(): void;
}
interface Feedable {
eat(): void;
}
interface Sleepable {
sleep(): void;
}
interface Reportable {
fileReport(): void;
}
// 必要なインターフェースのみを実装
class RpbotWorker implements Workable, Reportable {
public work(): void { /* 実装 */}
public fileReport(): void {/* 実装 */}
}
class HumanWorker implements Workable, Feedable, Sleepable, Reportable {
public work(): void { /* 実装 */}
public eat(): void { /* 実装 */}
public sleep(): void { /* 実装 */}
public fileReport(): void { /* 実装 */}
}
利点
- 最小限の依存関係: クライアントは必要なメソッドにのみ依存
- 柔軟性: インターフェースを組み合わせて機能を構成出来る
- 実装の容易さ: 小さいインターフェースは実装が簡単
- 明確な責任: 各インターフェースが明確な役割を持つ
- 変更の影響範囲の制限: インターフェース変更の影響を最小限に抑える
9.レイヤード設計
目的:アプリケーションを責任の異なる層に分け、依存関係を一方向に保つ。
チェックリスト
- アプリケーションが明確なレイヤーに分割されているか
- 各レイヤーが明確な責任を持っているか
- 依存関係が上位レイヤーから下位レイヤーへの一方か
- レイヤー間の通信は明確なインターフェースを通じて行われているか
- レイヤー間で適切なデータ変換が行われているか
悪い例良い例
// 悪い例: レイヤーの混在
class UserController {
public async showUserProfile(userId: number, req: any): Promise<string> {
// データベース接続(データアクセスレイヤーの責任)
const connection = await mysql.createConnection({
host: 'localhost',
user: 'user',
password: 'pass',
database: 'mydb'
});
const [rows] = await connection.execute(
'SELECT * FROM users WHERE id = ?',
[userId]
);
// ビジネスロジック(サービスレイヤーの責任)
let user: any = null;
if (row.length > 0) {
user = rows[0];
// ユーザー権限チェック
if (!this.hasPermission(user, "view_profile")) {
return "redirect:/accsess-denied";
}
}
// ビューへのデータ受け渡し
const model: Record<string, any> = {
user: user
};
// テンプレート処理(ビューレイヤーの責任)
const template = await fs.readFile("user_profile.html", "utf8");
return thos.renderTemplate(template, model);
}
private hasPermission(user: any, permission: string): boolean {
// 権限チェックのロジック
return true;
}
private renderTemplate(template: string, model; Record<string, any>): string {
// テンプレートレンダリングロジック
return "HTML content"
}
}
// 良い例: 明確なレイヤー分割
// データ転送オブジェクト
interface UserProfileDTO {
id: number;
name: string;
email: string;
}
// プレゼンテーションレイヤー
class UserController {
constructor(
private userService: UserService,
private templateEngine: TemplateEngine
) {}
public async showUserProfile(userId: number, req: any): Promise<string> {
try {
// サービスレイヤーに処理を委譲
const userProfile = await this.userService.getUserProfile(userId);
// ビューにデータを渡す
const model = {
user: userProfile
};
return this.templateEngine.render("user_profile", model);
} catch (error) {
if (error instanceof AccessDeniedException) {
return "redirect:/access-denied";
}
throw error;
}
}
}
// ビジネスロジックレイヤー
class UserService {
constructor(
private userRepositroy: UserRepositroy,
private permissionService: PermissionService
) {}
public async getUserProfile(userId: number): Promise<UserProfileDTO> {
const user = await this.userRepository.findById(userId);
if (!user) {
throw new UserNotFoundException(userId);
}
if (!thos.permissionService.hasPermission(user, "view_profile")) {
throw new AccessDeniedException();
}
return {
id: user.id,
name: user.name,
email: user.email
};
}
}
// データアクセスレイヤー
interface UserRepository {
findById(userId; number): Promise<User | null>;
}
class MySQLUserRepository implements UserRepository {
public asnyc findById(userId: number): Promise<User | null> {
// データベースアクセスロジック
return { id: userId, name: "ユーザー" + userId, email: "user@example.com" }
}
}
// インフラストラクチャレイヤー
class TemplateEngine {
public render(templateName: string, model: Record<string, any>): string {
// テンプレートレンダリングロジック
return "HTML content";
}
}
// カスタム例外
class UserNotFoundException extends Error {}
class AccessDeniedException extends Error {}
利点
- 関心の分離: 各レイヤーが特定の責任に集中できる
- テスト容易性: 各レイヤーを個別にテストできる
- 保守性: 特定の部分を他のレイヤーに影響を与えずに変更できる
- 技術の交換容易性: 例えばデータアクセス層の実装だけを変更可能
- 理解しやすさ: アプリケーションの全体構造が明確になる
10.DRY原則(Don't Repeat Yourself)
目的:同じ知識やロジックをコード内で繰り返さない。
チェックリスト
- 同じコードやロジックが複数の場所で重複していないか
- 定数や設定値が一箇所で定義されているか
- ビジネスルールが一箇所で定義されているか
- ユーティリティ関数やヘルパーメソッドを適切に使用してるか
- 共通処理が適切に抽出されているか
悪い例良い例
// 悪い例: コードの重複
class UserValidator {
public valdateEmail(email: string): boolean {
// メールアドレスの検証ロジック
if (email === null || email == undefined || email === "") {
return false;
}
if (!email.include("@")) {
return false;
}
// その他の検証ロジック
return true;
}
}
class RegistrationService {
public registerUser(user: { email: string, name: string }): void {
// メールアドレスの検証ロジックが重複
const email = user.email;
if (email === null || email === undefind || email === "") {
throw new Error("Email is requried");
}
if (!email.include("@")) {
throw new Error("Invalid email format");
}
// ユーザ登録処理
console.log("User registered:", user);
}
}
// 良い例: 共通処理の抽出
class EmailValidator {
public static isValid(email: string): boolean {
if (email === null || email === undefind || email === "") {
return false;
}
if (!email.include("@") {
return false;
}
// その他の検証ロジック
return true;
}
}
class UserValidator {
public validateEmail(email: string): boolean {
return EmailValidator.isValid(email);
}
}
class RegistrationSevice {
public registerUser(user: {email: string, name: string }): void {
if (!EmailValidator.isValid(user.email)) {
throw new Error("Invalid email");
}
// ユーザ登録処理
console.log("User registered:", user);
}
}
利点
- 保守性の向上: 変更が一箇所で済み、整合性が保たれる
- バグの減少: 1つの修正が全ての同じロジックに適用される
- コードサイズの削減: 重複が排除され、コードベースがコンパクトになる
- テスト効率: テストすべきコードが少なくなる
- 一貫性の確保: 同じ処理が常に同じ方法で行われる
11. YAGNI原則(You Aren't Gonna Neet It)
目的:必要になるまで機能を実装しない
チェックリスト
- 将来必要になるかもしれないという理由だけの機能はないか
- 現時点で使われていない過度な抽象化はないか
- 現在の要件を満たす最もシンプルな実装になっているか
- 「あったら便利かも」程度の機能や拡張ポイントはないか
- コードの複雑さが現在の問題の複雑さに見合っているか
悪い例良い例
// 悪い例: 過剰な抽象化と将来のための機能
class UserManager {
private userRepository: UserRepository;
private permissionManager: PremissionManager;
private auditLogger: AuditLogger;
private notificationService: NotificationService;
private userCache: Map<number, User>;
constructor() {
this.userRepository = new UserRepository();
this.permissionManager = new PremissionManager();
this.auditLogger = new NotificationService();
this.userCache = new Map<number, User>();
}
// 現時点では単純なユーザー検索しか必要ないが、
// 「将来必要になるかも」と思って複雑な実装を作っている
public findUser(userId: number): User | null {
// キャッシュをチェック(現時点では不要)
const cachedUser = this.userCache.get(userId);
if (cachedUser) {
return cachedUser;
}
// 権限チェック(現時点では不要)
if (!this.permissionManager.canAccessUserData()) {
throw new Error("Access denied");
}
// ユーザー検索
const user = this.userRepository.findById(userId);
// 監査ログ(現時点では不要)
this.audit.logUserAccess(userId);
// 通知送信(現時点では不要)
this.notificationService.notifyUserAccess(userId);
// キャッシュに保存(現時点では不要)
if (user) {
this.userCache.set(userId, user);
}
return user;
}
}
// 良い例: 現在必要な機能に集中
class UserManager {
private userRepositiry: UserRepository;
constructor() {
this.userRepository = new UserRepository();
}
// 現在必要な機能だけを実装
public findUser(userId: number): User | null {
return this.userRepository.findById(userId);
}
}
// 機能が必要になった時点で追加する
// class UserAccesAuditor {
// public aditUserAccess(userId: number): void {
// // 監査ログ記録ロジック
// }
// }
利点
- シンプルなコード: 余分な複雑さがなく理解しやすい
- 開発速度の向上: 実際に必要な機能だけに集中できる
- 保守の容易さ: 不要な機能のメンテナンスコストがない
- 技術負債の削減: 使われない機能や過剰な設計がない
- リソースの効率的な使用: 本当に価値を生む機能に集中できる
12. 明確な命名規則
目的:名前は目的や役割を明確にし、誤解を招かないようにする。
チェックリスト
- 変数、メソッド、クラス名が目的や役割を明確に表しているか
- 一貫した命名規則を使用しているか(キャメルケース、スネークケースなど)
- 誤解を招く名前や略語を避けているか
- 命名から実装の詳細が推測できるか
- ドメイン用語が適切に反映されているか
悪い例良い例
// 悪い例: 不明確で一貫性のない名前
class DM {
private list: OR[];
constructor() {
this.list = [];
}
public proc(o: OR): void {
// 処理内容が名前から推測できない
this.list.push(o);
this.save();
}
public getList(): OR[] {
return this.list;
}
public save(): void {
// データ保存処理
console.log("Saving data...");
}
}
class OR {
private id: number;
private qty: number;
private val: number;
constructor(id: number, qtr: number, val: number) {
this.id = id;
this.qtr = qtr;
this.val = val;
}
// 何を計算しているのか不明確
public calc(): number {
return this.qtr * this.val;
}
}
// 良い例: 明確で説明的な名前
class DocumentManager {
private orders: Order[];
constructor() {
this.orders = [];
}
public processOrder(order: Order): void {
this.order.push(order);
this.saveOrders();
}
public getOrders(): Order[] {
return this.orders;
}
public saveOrders(): void {
// データ保存処理
console.log("Saving orders...");
}
}
class Order {
private orderId: number;
private quantity: number;
private unitPrice: number;
constructor(orderId: number, quantity: number, unitPrice: number) {
this.orderId = orderId;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public calculateTotalPrice(): number {
return this.quantity * this.unitPrice;
}
}
利点
- コードの自己文書: 名前自体がコードの目的を説明する
- 理解の容易さ: 初めてコードを読む人でも理解しやすい
- バグの減少: 誤解によるバグが減る
- メンテナンスの容易さ: 機能を探しやすく修正しやすい
- コラボレーションの向上: チーム内でのコミュニケーションが円滑になる
13.関数/メソッドの適切なサイズ
目的:関数は一つの責任に集中し、短く理解しやすくする。
チェックリスト
- メソッドは1つの責任/機能に集中しているか
- メソッドの長さは適切か(目安として20~30行以内、どんなに長くても50行以内)
- 複雑な条件分岐が多すぎないか
- 入れ子(ネスト)のレベルが深すぎないか(3レベル以内が理想)
- メソッドの引数の数は適切か(4つ以下が望ましい)
悪い例良い例
// 型定義
interface OrderItem {
productId: number;
quantity: number;
}
interface Order {
id: number;
customerId: number;
items: OrderItem[];
status: string;
totalPrice: number;
}
interface Product {
id: number;
name: string;
price: number;
stockQuantity: number;
}
// 悪い例: 長大で複数の責任を持つメソッド
class OrderProcessor {
private productRepository: any;
private orderRepository: any;
private customerRepository: any;
private paymentGateway: any;
private emailService: any;
public processOrder(order: Order): void {
// 1.在庫チェック
let allInStock = true;
for (const item of order.items) {
const product = this.productRepository.findById(item.productId);
if (!product) {
throw new Error(`Product not found: ${item.productId}`);
}
if (product.stockQuantity < item.quantity) {
allInStock = false;
break;
}
}
if (!allInStock) {
throw new Error("Insufficient stock");
}
// 2.価格計算
let totalPrice = 0;
for (const item of order.items) {
const product = this.productRepository.findById(item.productId);
let itemPrice = product.price * item.quantity;
// 割引の適用
if (item.quantity > 10) {
itemPrice *= 0.9; // 10%割引
}
totalPrice += itemPrice;
}
// 3.支払い処理
const result = this.paymentGatway.processPayment(order.customerId, totalPrice);
if (!result.isSuccessful) {
throw new Error(`Payment failed: ${result.errorMessage}`);
}
// 4.在庫の更新
for (const item of order.item) {
const product = this.productRepository.findById(item.productId);
product.stockQuantity -= item.quantity;
this.productRepository.save(product);
}
// 5.注文の保存
order.status = "PAID";
order.totalPrice = totalPrice;
this.orderRepostory.save(order);
// 6.確認メールの送信
const customer = this.customerRepository.findById(order.customerId);
this.emailService.sendOrderConfirmation(customer.email, order);
}
}
// 良い例: 責任ごとに分割された小さなメソッド
class OrderProcessor {
private productRepository: any;
private orderRepository: any;
private customerRepository: any;
private paymentGateway: any;
private emailService: any;
public processOrder(order: Order): void {
this.checkInventory(order);
this.calculateTotalPrice(order);
this.processPayment(order);
this.updateInventory(order);
this.saveOrder(order);
this.sendConfirmationEmail(order);
}
private checkInentory(order: Order): void {
for (const item of order.items) {
const product = this.productRepository.findById(item.productId);
if (!product) {
throw new Error(`Product not found: ${item.productId}`);
}
if (product.stockQuantity < item.quantity) {
throw new Error("Insufficient stock");
}
}
}
private calculateTotalPrice(order: Order): void {
let totalPrice = 0;
for (const item of order.item) {
totalPrice += this.calculateItemPrice(item);
}
order.totalPrice = totalPrice;
}
private calculateItemPrice(item: OrderItem): number {
const product = this.productRepository.findById(item.productId);
let itemPrice = product.price * item.quantity;
if (item.quantiry > 10) {
itemPrice *= 0.9 // 10%割引
}
return itemPrice
}
private processPayment(order: Order): void {
const result = this.paymentGateway.processPayment(
order.customerId,
order.totalPrice
);
if (!result.isSuccessful) {
throw new Error(`Payment faild: ${result.errorMessage}`);
}
}
private updateInventory(order: Order): void {
for (const item of order.items) {
const product = this.productRepository.findById(item.productId);
product.stockQuantity -= item.quantity;
this.productRepostory.save(product);
}
}
private saveOrder(order: Order): void {
order.status = "PAID";
this.orderRepository.save(order);
}
private sendConfirmationEmail(order: Order): void {
const customer = this.customerRepository.findById(order.customerId);
this.emailService.sendOrderConfirmation(customer.email, order);
}
}
利点
- 理解しやすさ: 小さなメソッドは一目で理解できる
- テスト容易性: 各機能を個別にテストできる
- 再利用性: 小さな機能単位で再利用できる
- バグの局所化: 問題が特定のメソッドに限定される
- 保守性: 単一の変更理由を持つメソッドは修正が容易
14.コメントとドキュメンテーション
目的:コードが「なぜそうなっているか」を説明し、複雑な部分を明確にする。
チェックリスト
- コードが「なぜそうなっているか」を説明するコメントがあるか
- 複雑なアルゴリズムや非直感的なコードに説明があるか
- 公開APIには適切なドキュメンテーションがあるか
- コメントが最新の状態に保たれているか
- 冗長なコメント(コードを単に言い換えただけ)を避けているか
悪い例良い例
// 悪い例: 無意味または誤解を招くコメント
// ユーザを取得する
function getUser(id: number): User {
return repository.findById(id);
}
// 価格を計算する
function calculatePrice(product: Product, quantity: number): number {
// 価格に数量をかける
const price = product.price * quantity;
// 結果を返す
return price;
}
// 良い例: 有用なコメント
/**
* 指定されたIDのユーザを取得する。
* ユーザが見つからない場合にはUserNotFoundExceptionをスローする。
* @param id 検索するユーザーのID
* @returns 見つかったユーザー
* @throws UserNotFoundException ユーザーが存在しない場合
*/
function getUser(id: number): User {
const user = repository.findById(id);
if (!user) {
throw new UserNotFoundException(id);
}
return user;
}
/**
* 製品の合計価格を計算する
* 数量が10を超える場合は10%の数量が割引適用される。
*
* @param product 価格計算の対象製品
* @param quantity 購入数量
* @returns 割引適用後の合計価格
*/
function calculatePrice(product: Product, quantity: number): number {
let price = product.price * quantity;
// 数量割引の適用(10個以上の10%割引)
if (quantity > 10) {
price * = 0.9;
}
return price;
}
// 複雑なケースの例
/**
* キャッシュの有効期限をチェックし、必要に応じて更新する。
* ここでは特殊なタイムスタンプ比較ロジックを使用している。
* 注意: システムタイムとデータベースタイムのずれを光量して
* 5分のバッファを設けている。
*/
function validateCaheTimestamp(): void {
// 実装
}
利点
- コードの意図の明確化: なぜそのように実装したがが分かる
- 学習効率の向上: 新しいチームメンバーが早く理解できる
- 複雑さの軽減: 難しい部分を説明することで理解を助ける
- API利用の容易さ: 使用方法やエッジケースが文書化されている
- メンテナンスの効率化: 将来の開発者が文脈を理解できる
15.例外の適切な使用
目的:例外は異常状態の処理のみに使用し、通常のフロー制業には使わない
チェックリスト
- 例外を通常のフロー制御に使用していないか
- 適切な粒度の例外クラス階層を定義しているか
- try-catchブロックの範囲が適切か
- 例外をキャッチした後に適切に処理しているか
- リソースの解放が保証されているか
悪い例良い例
// 悪い例: 例外の不適切な使用
function processFile(path: string): void {
let fileData: Buffer;
try {
// リソースの適切な解放がされていない
fileData = fs.readFileSync(path);
let index = 0;
// フロー生業に例外を使用
try {
while (true) {
if (index >= fileData.length) {
throw new Error("EOF");
}
const byte = fileData[index++];
// データ処理
}
} catch (e) {
if (e.message === "EOF") {
// 正常終了として使用
console.log("ファイル読み込み完了");
} else {
throw e;
}
}
} catch (e) {
// 全ての例外を一括処理
console.log("エラーが発生しました");
}
}
// 良い例: 例外の適切な使用
function processFile(path: string): void {
try {
const fileData = fs.readFileSync(path);
// 例外ではなく通常の条件でループ制御
for (let index = 0; index < fileData.length; index++) {
const byte = fileData[index];
// データ処理
}
console.log("ファイル読み込み完了");
} catch (e) {
if (e instanceof FileNotFoundError) {
// 具体的な例外ごとに異なる処理
console.error(`ファイルが見つかりません: ${path}`);
throw new ProcessingError(`File not found: ${path}`, e);
} else if (e instanceof IOError) {
console.error(`ファイル読み込み中にエラー発生: ${e.message}`);
throw new ProcessingError("Error reading file", e);
} else {
throw e;
}
}
}
// アプリケーション固有の例外階層
class ProcessingError extends Error {
constructor(message: string, public readonly cause?: Error) {
super(message);
this.name = "ProcessingError";
// クラス名を保持するためのセットアップ
Object.setPrototypeOf(this, ProcessingError.prototype);
}
}
class FileNotFoundError extends Error {}
class IOError extends Error {}
利点
- コードの明確さ: 例外的な状況と通常のフローが明確に分離される
- エラー処理の一貫性: 例外階層を使って統一的にエラー処理ができる
- リソース管理の確実性: 適切なリソース解放パターンでリソースリークを防止
- デバックのしやすさ: 適切な例外情報により問題の特定が容易になる
- 回復性の向上: 特定の例外に対して適切な回復処理を実装できる
16.防御的プログラミング
目的:予期しない入力や状況に対して、頑健性を持たせる。
チェックリスト
- メソッドの入力パラメータを検証しているか
- nullチェックを適切に行なっているか
- 境界条件(エッジケース)を考慮しているか
- 不変条件を明示的に確認しているか
- 外部システムからデータを信頼せず検証しているか
悪い例良い例
// 型定義
interface User {
id?: number;
name: string;
email: string;
age: nnumber;
}
// 悪い例: 型はあるが内容の検証がない
class UserService {
private userRepository: any;
private emailService: any;
pubilc createUser(name: string, email: string, age: number): User {
// 入力値の検証がないため、以下のような問題がある:
// - 空文字(型としては有効な文字列)
// - 不正なメールアドレス形式(@がないなど)
// - 非現実的な年齢(-10や500など)
const user: User = {
name, // ""でも有効
email, // "invalid"でも有効
age // -10 や 500でも有効
};
this.userRepository.save(user);
// 潜在的な問題:
// 1. 空の名前が保存される
// 2. 不正なメールアドレスでメール送信を試みる
// 3. UIでは負の年齢が表示される
this.emailSerive.sendWelcomeEmail(email, name.trim()); // name が "" の場合もtrim()は動くが空文字列が送られる
return user;
}
}
// 良い例: 防御的プログラミング
class UserService {
private userRepository: any;
private emailService: any;
public createUser(name: string, email: string, age: number): User {
// 入力パラメータの検証
if (!name || name.trim() === "") {
throw new Error("名前は必須です");
}
if (!email || !this.isValidEmail(email)) {
throw new Error("有効なメールアドレスが必要です");
}
if (age < 0 || age > 120) {
throw new Error("年齢は0~120の範囲で指定してください");
}
const user: User = {
name: name.trim(),
email: email.toLowerCase(),
age
};
const savedUser = this.userRepository.save(user);
// この時点で名前とメールアドレスが有効なことが保証されている
this.emailService.sendWelcomeEmail(savedUser.email, savedUser.name);
return savedUser;
}
private isValidEmail(email: string): boolean {
// 簡易的なメールアドレスのバリデーションロジック
return /^.+@.+\..+$/.test(email);
}
}
利点
- 堅牢性の向上: 予期しない入力や状況にも対応できる
- バグの予防: 早期の検証で問題を未然に防止
- デバッグの容易さ: エラーの原因が明確になる
- セキュリティの向上: 不正な入力によるセキュリティ問題を防止
- 自己文書化: 前提条件や制約が明示的になる
17.テスト駆動開発(TDD)
目的:コードを実装前にテストを書き、テスト通過を目指して開発する。
チェックリスト
- 機能実装前にテストを作成しているか
- 全ての要件が少なくとも1つのテストでカバーされているか
- テストは小さく焦点を絞ったものになっているか
- エッジケースと異常系のテストが含まれているか
- リファクタリング後もテストが通ることを確認しているか
悪い例良い例
// 先に実装してから、後からテストを追加
class Calculator {
public add(a: number, b: number): number {
return a + b;
}
public divide(a: number, b: number): number {
return a / b; // 0除算の考慮なし
}
}
// テストは後から追加(カバレッジだけのために)
// test.ts
describe('Calculator', () => {
if('should add two numbers correctly', () => {
const calc = new Calculator();
expect(calc.add(2, 3)).toBe(5);
});
// 0除算のテストがなく、バグを見逃している
});
// 良い例: テスト駆動開発
// まずテストを書く
// test.ts
desctibe('Calculator', () => {
let calculator: Calculator;
beforEatch(() => {
calculator = new Calculator();
});
describe('add', () => {
it('should add positive numbers correctly', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should handle negative numbers', () => {
except(calculator.add(2, -3)).toBe(-1);
})
});
describe('divide', () => {
it('should divide numbers correctly', () => {
expect(calculator.divide(6, 3)).toBe(2);
});
it('should throw error when dividing by zero', () => {
expect(() => calculator.divide(5, 0)).toThrowError('0で除算することはできません');
});
});
});
// テストに基づいて実装
class Calculator {
public add(a: number, b: number): number {
return a + b;
}
public divide(a: number, b: number): number {
if (b === 0) {
throw new Error('0で除算することはできません');
}
return a / b;
}
}
利点
- 仕様の明確化: テストが仕様としての役割を果たす
- デザインの改善: テスト容易性を考慮したデザインになる
- リグレッションの防止: リファクタリング時に既存機能の維持を確認できる
- バグの早期発見: 実装直後にバグを発見できる
- 開発の集中力維持: 小さなサイクルで成功体験を得られる
18. コマンドクエリ責務分離原則(CQRS)
目的:状態を変更するコマンドと状態を返すクエリを明確に分離する。
チェックリスト
- メソッドは「値を返す」か「状態を変更する」かのいずれか一方だけを行うか
- クエリ(値を返すメソッド)は副作用を持たないか
- コマンド(状態を変更するメソッド)は値を返さないか(成功/失敗のブール値は例外)
- オブジェクトの状態変化を追跡しやすいか
- 読み取りと書き込みの責任が適切に分離されているか
悪い例良い例
メソッドレベルでのCQRS
悪い例
class ShoppingCard {
private items: CartItem[] = [];
// 問題点: 状態を変更して値も返している
public addItem(product: Product, quantity: number): number {
const item = { product, quantity };
this.items.push(item);
// カートの現在の合計金額を計算して返す
const totalAmount = this.calculateTotal();
return totalAmount; // コマンドなのに値を返す
}
// 問題点: 状態を確認して副作用をもつ
public checkAndUpdateDiscounts(): CartItem[] {
let updated = false;
// 10個以上の同じ商品に割引適用
for (const item of this.item) {
if (item.quantity >= 10 && !item.hasDiscount) {
item.hasDiscount = true;
item.price = item.price * 0.9;
updated = true;
}
}
// 副作用: ログ記録
if (updated) {
this.logDiscountApplication();
}
// クエリとして結果を返す
return this.items; // 副作用を持つクエリ
}
private calculateTotal(): number {
return this.items.reduce((sum, item) =>
sum + item.product.price * item.quantity, 0);
}
private logDiscountApplication(): void {
console.log("Discounts applied at", new Date());
}
}
良い例
class ShoppingCart {
private items: CartItem[] = [];
// コマンド: カートに商品を追加(値を返さない)
public addItem(product: Product, quantity: number): void {
const item = { product, quantity };
this.item.push(item);
// 値を返さない
}
// コマンド: 割引を適用(値を返さない)
public applyDiscounts(): void {
let updated = false;
// 10個以上の同じ商品には割引適用
for (const item of this.items) {
if (item.quantity >= 10 && !item.hasDiscount) {
item.hasDiscount = true;
item.price = item.price * 0.9;
updated = true;
}
}
// 副作用はコマンド内に閉じ込める
if (updated) {
this.logDiscountApplication();
}
}
// クエリ: 割引が適用可能な商品があるか確認(副作用なし)
public hasDiscountableItems(): boolean {
return this.items.some(item =>
item.quantity >= 10 && !item.hasDiscount);
}
// クエリ: 合計金額を計算(副作用なし)
public calculateTotal(): number {
return this.items.reduce((sum, item) =>
sum + item.product.price * item.quantity, 0);
}
// クエリ: カート内の商品を取得(副作用なし)
public getItems(): ReadonlyArray<CartItem> {
// 読み取り専用コピーを返して外部からの変更を防ぐ
return [...this.items];
}
private logDiscountApplication(): void {
console.log("Diccounts applied at", new Date());
}
}
// 使用例
const cart = new ShoppingCart();
// コマンド実行
cart.addItem(product, 12);
// 割引適用前に確認クエリを実行
if (cart.hadDiscountableItems()) {
// コマンド実行
cart.applyDiscount();
}
// 結果確認のクエリを実行
const total = cart.calculateTotal();
console.log(`合計金額: ${total}円`)
クラスレベルでのCQRS
悪い例(責務が混在)
// 問題点: 読み取りと書き込みの責務が混在したサービス
class OrderService {
private database: Database;
constructor(database: Database) {
this.database = database;
}
// 注文作成(書き込み操作)
public createOrder(items: OrderItem[], userId: number): Order {
// 注文データの作成と保存
const order = new Order(items, userId);
this.database.save(order);
return order;
}
// 注文検索(読み取り操作)
public createOrder(items: OrderItem[], userId: number): Order {
// 注文データの作成と保存
const order = new Order(items, userId);
this.database.save(order);
return order;
}
// 注文検索(読み取り操作)
public findOrdersByUser(userId: number): Order[] {
return this.database.query(`SELECT * FROM orders WHERE userId = ${userId}`);
}
// 注文更新(書き込み操作)
public updateOrderStatus(orderId: number, status: string): boolean {
return this.database.execute(
`UPDATE orders SET status = '${status} WHERE id = ${orderId}'`
);
}
// 統計データの取得(複雑な読み取り操作)
public getOrderStatistics(year: number): OrderStatistics {
// 複雑な集計クエリ...
return statistics;
}
}
良い例(CQRSを適用)
// コマンド処理用サービス(書き込み専用)
class OrderCommandService {
private database: Database;
// 注文作成(戻り値は識別子のみ)
public createOrder(items: OrderItem[], userId: number): number {
const order = new Order(items, userId);
return this.database.save(order);
}
// 注文更新(成功/失敗のみ返す)
public updateOrderStatus(orderId: number, status: string): boolean {
return this.database.execute(
`UPDATE orders SET status = '${status}' WHERE id = ${orderId}`
);
}
}
// クエリ処理用サービス(読み取り専用)
class OrderQueryService {
private readonly database: Database;
constructor(database: Database) {
this.database = database;
}
// 注文作成(戻り値は識別子のみ)
public createOrder(items: OrderItem[], userId: number): number {
const order = new Order(items, userId);
return this.databse.save(order);
}
// 注文更新(成功/失敗のみ返す)
public updateOrderStatus(orderId: number, status: string): boolean {
return this.database.execute(
`UPDATE irders SET status = '${status}' WHERE id = ${orderId}`
);
}
}
// クエリ処理用サービス(読み取り専用)
class OrderQueryService {
private readonly database: Database;
constructor(database: Database) {
this.database = database;
}
// ユーザの注文検索
public findOrdersByUser(userId: number): OrderDto[] {
const orders = this.database.query(
`SELECT * FROM orders WHERE userId = ${userId}`
);
// データ変換(DTOパターン)
return orders.map(order => this.mapToDto(order));
}
// 注文詳細の取得
public getOrderDetail(orderId: number): OrderDetailsDto | null {
const order = this.database.queryOne(
`SELECT * FROM orders WHERE id =${orderId}`
);
if (!order) return null;
// 詳細データの取得と変換(最適化されたクエリ)
const items = this.database.query(
`SELECT * FROM order_item WHERE orderId = ${orderId}`
);
return {
...this.mapToDto(order),
items: items.map(item => this.mapItemToDto(item))
};
}
// 統計データの取得
public getOrderStatistics(year: number): OrderStatistics {
// 最適化された読み取り専用クエリ
return statistics;
}
private mapToDto(order: any): OrderDto {
// エンティティからDTOへの変換
return {
id: order.id,
data: new Date(order.createdAt),
status: order.status,
total: order.total
};
}
private mapToDto(order: any): OrderDto {
// エンティティからDTOへの変換
return {
id: order.id,
date: new Date(order.createdAt),
status: order.status,
total: order.total
};
}
private mapItemDto(item: any): OrderItemDto {
// ...変換ロジック
return dto;
}
}
// 使用例
// クライアントコード
class OrderController {
constructor(
private commandService: OrderCommandService,
private queryService: OrderQueryService
) {}
public placeOrder(req: Request, res: Respose): void {
// コマンド実行
const orderId = this.commandService,createOrder(
req.body.items,
req.user.id
);
// 別の操作でクエリ実行
const orderDetails = this.queryService.getOrderDetails(orderId);
res.json(orderDetails);
}
}
利点
- デバック容易性: 状態変更の追跡が容易
- テスト容易性: クエリは副作用がないためテストが簡単
- 推論容易性: メソッドの責任が明確
- 最適化容易性: 読み取りと書き込みを別々に最適化可能
- 並行処理への適合: 読み取り/書き込みの分離により並行処理が容易
Discussion