乾淨架構實際案例

案例:Todo App(Angular + Tauri + Rust)

原始架構問題

src/
├── components/        # UI + 業務邏輯混在一起
├── services/          # Mock data + API 混在一起
└── models/            # 只有 interface

重構後架構

src/
├── domain/
│   ├── entities/
│   │   └── task.ts            # Task 實體,包含業務規則
│   ├── repositories/
│   │   └── task-repository.ts # Repository 介面
│   └── value-objects/
│       └── priority.ts        # Priority 值物件
│
├── application/
│   ├── use-cases/
│   │   ├── create-task.ts
│   │   ├── update-task.ts
│   │   └── search-tasks.ts
│   └── dtos/
│       └── task-dto.ts
│
├── infrastructure/
│   ├── repositories/
│   │   └── sqlite-task-repository.ts  # SQLite 實作
│   └── services/
│       └── tauri-ipc-service.ts        # Tauri IPC 實作
│
├── presentation/
│   ├── components/
│   │   ├── dashboard/
│   │   ├── task-item/
│   │   └── edit-modal/
│   └── services/
│       └── task.facade.ts      # UI 調用的門面
│
└── bootstrap.ts

程式碼對比

Before:業務邏輯在 UI 裡

// components/dashboard/dashboard.component.ts
export class DashboardComponent {
  async addTodo(title: string) {
    if (!title.trim()) return;  // 業務規則在 UI
    const task = await invoke('create_task', { request: { title } }); // 硬性依賴 Tauri
    this.todos.update(list => [...list, task]); // UI 直接操作狀態
  }
}

After:業務邏輯在 Use Case

// application/use-cases/create-task.ts
export class CreateTaskUseCase {
  constructor(private taskRepo: TaskRepository) {}

  async execute(request: CreateTaskRequest): Promise<Task> {
    // 業務規則集中在這裡
    if (!request.title.trim()) {
      throw new ValidationError('Title cannot be empty');
    }

    const task = new Task({
      id: generateId(),
      title: request.title,
      status: 'todo',
      createdAt: new Date()
    });

    await this.taskRepo.save(task);
    return task;
  }
}

// presentation/components/dashboard/dashboard.component.ts
export class DashboardComponent {
  constructor(private createTask: CreateTaskUseCase) {}

  async addTodo(title: string) {
    try {
      await this.createTask.execute({ title });
      await this.loadData(); // 重新載入
    } catch (error) {
      this.showError(error.message);
    }
  }
}

案例:電商訂單系統

Domain Layer

// domain/entities/order.ts
export class Order {
  private items: OrderItem[] = [];

  get total(): number {
    return this.items.reduce((sum, item) => sum + item.subtotal, 0);
  }

  addItem(product: Product, quantity: number): void {
    if (quantity <= 0) throw new Error('Invalid quantity');
    if (product.stock < quantity) throw new Error('Insufficient stock');

    const existing = this.items.find(i => i.productId === product.id);
    if (existing) {
      existing.increaseQuantity(quantity);
    } else {
      this.items.push(new OrderItem(product, quantity));
    }
  }

  submit(): void {
    if (this.items.length === 0) throw new Error('Empty order');
    this.status = 'submitted';
  }
}

// domain/repositories/order-repository.ts
export interface OrderRepository {
  findById(id: string): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

Application Layer

// application/use-cases/place-order.ts
export class PlaceOrderUseCase {
  constructor(
    private orderRepo: OrderRepository,
    private productRepo: ProductRepository,
    private paymentService: PaymentService
  ) {}

  async execute(request: PlaceOrderRequest): Promise<OrderResult> {
    // 1. 建立訂單
    const order = new Order(request.userId);

    // 2. 加入商品
    for (const item of request.items) {
      const product = await this.productRepo.findById(item.productId);
      if (!product) throw new Error('Product not found');
      order.addItem(product, item.quantity);
    }

    // 3. 付款
    await this.paymentService.charge(order.total, request.paymentMethod);

    // 4. 儲存
    order.submit();
    await this.orderRepo.save(order);

    return { orderId: order.id, total: order.total };
  }
}

Infrastructure Layer

// infrastructure/repositories/postgres-order-repository.ts
export class PostgresOrderRepository implements OrderRepository {
  async findById(id: string): Promise<Order | null> {
    const row = await this.db.query('SELECT * FROM orders WHERE id = $1', [id]);
    return row ? this.toDomain(row) : null;
  }

  async save(order: Order): Promise<void> {
    await this.db.query(
      'INSERT INTO orders (id, user_id, status, total) VALUES ($1, $2, $3, $4)',
      [order.id, order.userId, order.status, order.total]
    );
  }
}

關鍵 takeaway

概念 說明
Domain 不動 業務規則改變時才改 Domain
Use Case 編排 負責流程控制,不含業務規則
Infrastructure 可換 換資料庫只改 Infrastructure
Presentation 最薄 只負責顯示和使用者互動