エンジニアとしての強み

各カテゴリの詳細を確認できます

単純性

複雑さを排除し、理解しやすく保守しやすいコードを書くための原則について紹介します。

責務の集約
関連する責任を適切に集約する

責務とは、ある目的を果たす責任を担っていること、責任範囲、役割を指します。同じ責任範囲、役割を持っているコードが散在している場合、変更対象ソースの構造が複雑で広くなってしまいます。

対策:

  • 責務を明確にしたソフトウェアアーキテクチャの採用

実装例:責務の集約

// 悪い例:責務が散在している
// user-controller.ts
export class UserController {
  async register(req, res) {
    const { name, email, password } = req.body;
    
    // パスワードのハッシュ化(セキュリティの責務)
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);
    
    // ユーザー作成(データアクセスの責務)
    const user = new User({ name, email, password: hashedPassword });
    await user.save();
    
    // トークン生成(認証の責務)
    const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
    
    // メール送信(通知の責務)
    const transporter = nodemailer.createTransport({/*...*/});
    await transporter.sendMail({
      to: email,
      subject: 'Welcome to our platform',
      text: `Hi ${name}, welcome to our platform!`
    });
    
    res.status(201).json({ token });
  }
}

// 良い例:責務が適切に分離されている
// user-controller.ts
export class UserController {
  constructor(
    private userService: UserService,
    private authService: AuthService,
    private notificationService: NotificationService
  ) {}

  async register(req, res) {
    const { name, email, password } = req.body;
    
    // ユーザー作成(ビジネスロジックの責務)
    const user = await this.userService.createUser({ name, email, password });
    
    // トークン生成(認証の責務)
    const token = await this.authService.generateToken(user);
    
    // メール送信(通知の責務)
    await this.notificationService.sendWelcomeEmail(user);
    
    res.status(201).json({ token });
  }
}

// user-service.ts
export class UserService {
  constructor(private userRepository: UserRepository, private passwordService: PasswordService) {}

  async createUser(userData: { name: string, email: string, password: string }): Promise<User> {
    const hashedPassword = await this.passwordService.hashPassword(userData.password);
    return this.userRepository.create({
      ...userData,
      password: hashedPassword
    });
  }
}

// auth-service.ts
export class AuthService {
  async generateToken(user: User): Promise<string> {
    return jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
  }
}

// notification-service.ts
export class NotificationService {
  constructor(private emailService: EmailService) {}

  async sendWelcomeEmail(user: User): Promise<void> {
    await this.emailService.sendEmail({
      to: user.email,
      subject: 'Welcome to our platform',
      text: `Hi ${user.name}, welcome to our platform!`
    });
  }
}
責務ごとの分割
過剰な責務の集中を避ける

過剰な責務を特定のコンポーネントを集約した場合、以下のような問題が生じます:

  • 開発フローの煩雑化:多くの理由でこのファイルは編集される可能性があります。
  • 責務の混交:本来分割されるべき責務が同一のコンポーネントに属することで、十分な時を経た後責務同士の結合度が高まる可能性があります。

実装例:責務の適切な分割

// 悪い例:単一のクラスに多くの責務が集中している
class OrderManager {
  // 注文の作成、更新、削除(CRUD操作)
  createOrder(orderData) { /* ... */ }
  updateOrder(orderId, orderData) { /* ... */ }
  deleteOrder(orderId) { /* ... */ }
  
  // 注文の検索と取得(クエリ操作)
  getOrderById(orderId) { /* ... */ }
  searchOrders(criteria) { /* ... */ }
  
  // 注文の処理と状態管理(ビジネスロジック)
  processOrder(orderId) { /* ... */ }
  cancelOrder(orderId) { /* ... */ }
  
  // 支払い処理(決済ロジック)
  processPayment(orderId, paymentDetails) { /* ... */ }
  refundPayment(orderId) { /* ... */ }
  
  // 在庫管理(インベントリロジック)
  updateInventory(orderId) { /* ... */ }
  checkInventory(productId) { /* ... */ }
  
  // 通知(通知ロジック)
  sendOrderConfirmation(orderId) { /* ... */ }
  notifyShipping(orderId) { /* ... */ }
}

// 良い例:責務ごとに分割されたクラス
// 注文のCRUD操作
class OrderRepository {
  createOrder(orderData) { /* ... */ }
  updateOrder(orderId, orderData) { /* ... */ }
  deleteOrder(orderId) { /* ... */ }
  getOrderById(orderId) { /* ... */ }
  searchOrders(criteria) { /* ... */ }
}

// 注文の処理と状態管理
class OrderService {
  constructor(
    private orderRepository: OrderRepository,
    private paymentService: PaymentService,
    private inventoryService: InventoryService,
    private notificationService: NotificationService
  ) {}
  
  processOrder(orderId) {
    const order = this.orderRepository.getOrderById(orderId);
    const paymentResult = this.paymentService.processPayment(order);
    
    if (paymentResult.success) {
      this.inventoryService.updateInventory(order);
      this.notificationService.sendOrderConfirmation(order);
      this.orderRepository.updateOrder(orderId, { status: 'PROCESSED' });
    }
    
    return paymentResult;
  }
  
  cancelOrder(orderId) { /* ... */ }
}

// 支払い処理
class PaymentService {
  processPayment(order) { /* ... */ }
  refundPayment(order) { /* ... */ }
}

// 在庫管理
class InventoryService {
  updateInventory(order) { /* ... */ }
  checkInventory(productId) { /* ... */ }
}

// 通知
class NotificationService {
  sendOrderConfirmation(order) { /* ... */ }
  notifyShipping(order) { /* ... */ }
}
抽象化(ポリモーフィズム)
インターフェースを通じた抽象化

オブジェクトの種類ではなく従うインターフェースが使用できるモジュールの十分条件である場合、ソース上でもその仮定を表現することができます。

以下の利点があります:

  • 特定のオブジェクト毎にソースが存在しないので、インターフェースレベルの変更影響範囲を小さくすることができます。

実装例:ポリモーフィズムによる抽象化

// インターフェース定義
interface Logger {
  log(message: string): void;
  error(message: string, error?: Error): void;
  warn(message: string): void;
  info(message: string): void;
}

// コンソールロガーの実装
class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
  
  error(message: string, error?: Error): void {
    console.error(`[ERROR] ${message}`, error);
  }
  
  warn(message: string): void {
    console.warn(`[WARN] ${message}`);
  }
  
  info(message: string): void {
    console.info(`[INFO] ${message}`);
  }
}

// ファイルロガーの実装
class FileLogger implements Logger {
  private filePath: string;
  
  constructor(filePath: string) {
    this.filePath = filePath;
  }
  
  log(message: string): void {
    this.writeToFile(`[LOG] ${message}`);
  }
  
  error(message: string, error?: Error): void {
    this.writeToFile(`[ERROR] ${message} ${error ? error.stack : ''}`);
  }
  
  warn(message: string): void {
    this.writeToFile(`[WARN] ${message}`);
  }
  
  info(message: string): void {
    this.writeToFile(`[INFO] ${message}`);
  }
  
  private writeToFile(content: string): void {
    // ファイルへの書き込み処理
  }
}

// サービスクラス - 具体的なロガーの実装に依存せず、インターフェースに依存
class UserService {
  private logger: Logger;
  
  // 依存性注入によりロガーを受け取る
  constructor(logger: Logger) {
    this.logger = logger;
  }
  
  createUser(userData: any): void {
    try {
      this.logger.info(`Creating user: ${userData.email}`);
      // ユーザー作成ロジック
      this.logger.log(`User created successfully: ${userData.email}`);
    } catch (error) {
      this.logger.error(`Failed to create user: ${userData.email}`, error);
      throw error;
    }
  }
}

// 使用例
// 開発環境ではコンソールロガーを使用
const devLogger = new ConsoleLogger();
const devUserService = new UserService(devLogger);

// 本番環境ではファイルロガーを使用
const prodLogger = new FileLogger('/var/log/app.log');
const prodUserService = new UserService(prodLogger);

// テスト環境ではモックロガーを使用
class MockLogger implements Logger {
  logs: string[] = [];
  errors: string[] = [];
  warnings: string[] = [];
  infos: string[] = [];
  
  log(message: string): void { this.logs.push(message); }
  error(message: string, error?: Error): void { this.errors.push(message); }
  warn(message: string): void { this.warnings.push(message); }
  info(message: string): void { this.infos.push(message); }
}

const mockLogger = new MockLogger();
const testUserService = new UserService(mockLogger);
ネストされた分岐の除去
コードの複雑さを減らす

外の分岐に依存しない処理が深いスコープに重複すると、コードの理解が難しくなります。

解決策:

同じような処理が他の分岐にも存在する場合は外側の分岐を抜けた後に処理を移します。

実装例:ネストされた分岐の除去

// 悪い例:ネストされた分岐が多い
function processOrder(order, user) {
  let result = { success: false, message: '' };
  
  if (order) {
    if (order.items && order.items.length > 0) {
      if (user) {
        if (user.isActive) {
          if (user.balance >= order.total) {
            // 注文処理
            user.balance -= order.total;
            order.status = 'PROCESSED';
            result.success = true;
            result.message = 'Order processed successfully';
            
            // メール送信処理
            sendEmail(user.email, 'Order Confirmation', `Your order #${order.id} has been processed.`);
          } else {
            result.message = 'Insufficient balance';
            
            // メール送信処理
            sendEmail(user.email, 'Order Failed', `Your order #${order.id} failed due to insufficient balance.`);
          }
        } else {
          result.message = 'User account is inactive';
          
          // メール送信処理
          sendEmail(user.email, 'Order Failed', `Your order #${order.id} failed because your account is inactive.`);
        }
      } else {
        result.message = 'User not found';
      }
    } else {
      result.message = 'Order has no items';
    }
  } else {
    result.message = 'Order not found';
  }
  
  return result;
}

// 良い例:早期リターンとネストの削減
function processOrder(order, user) {
  // 入力検証を先に行い、早期リターン
  if (!order) {
    return { success: false, message: 'Order not found' };
  }
  
  if (!order.items || order.items.length === 0) {
    return { success: false, message: 'Order has no items' };
  }
  
  if (!user) {
    return { success: false, message: 'User not found' };
  }
  
  if (!user.isActive) {
    const result = { success: false, message: 'User account is inactive' };
    sendOrderFailureEmail(user, order, 'your account is inactive');
    return result;
  }
  
  if (user.balance < order.total) {
    const result = { success: false, message: 'Insufficient balance' };
    sendOrderFailureEmail(user, order, 'insufficient balance');
    return result;
  }
  
  // すべての条件を満たした場合のみ実行される処理
  user.balance -= order.total;
  order.status = 'PROCESSED';
  
  // 成功メール送信
  sendOrderSuccessEmail(user, order);
  
  return { success: true, message: 'Order processed successfully' };
}

// メール送信処理を別の関数に抽出
function sendOrderSuccessEmail(user, order) {
  sendEmail(user.email, 'Order Confirmation', `Your order #${order.id} has been processed.`);
}

function sendOrderFailureEmail(user, order, reason) {
  sendEmail(user.email, 'Order Failed', `Your order #${order.id} failed due to ${reason}.`);
}