乾淨架構實際案例
案例: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 最薄 |
只負責顯示和使用者互動 |