🧑‍🏫

品質の高いソフトウェア開発のための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