Back to blog

Object-Oriented Design: Từ Requirements Đến Clean Code

oopsoftware-designdesign-patternstypescriptpythonjava
Object-Oriented Design: Từ Requirements Đến Clean Code

Giới Thiệu

Bạn đã biết bốn trụ cột của OOP. Bạn hiểu SOLID principles. Bạn implement được các design patterns. Nhưng khi ngồi xuống design một feature hay hệ thống mới từ đầu, bạn đứng hình: Bắt đầu từ đâu? Cần bao nhiêu class? Ai chịu trách nhiệm gì?

Object-Oriented Design (OOD) chính là cầu nối giữa việc biết concept OOP và việc xây dựng hệ thống thực tế. Đó là kỷ luật quyết định cần những object nào, mỗi object làm gì, và các object collaborate với nhau thế nào — trước khi viết một dòng code nào.

Bài viết này dạy bạn quy trình thực tế của OO design — các công cụ, principle và heuristic mà developer có experience sử dụng để đưa ra design decision mỗi ngày.

Bạn Sẽ Học Được

✅ Biến requirements thành objects bằng CRC cards
✅ Phân bổ responsibility dùng GRASP patterns
✅ Đo lường design quality bằng coupling và cohesion
✅ Áp dụng design heuristics thực tế để tránh sai lầm phổ biến
✅ Đi qua một design example hoàn chỉnh từ requirements đến code
✅ Biết khi nào nên dừng design và bắt đầu code

Yêu Cầu Trước

  • Quen thuộc với concept OOP (classes, interfaces, inheritance, polymorphism)
  • Hiểu cơ bản về SOLID Principles
  • Có experience viết code bằng ít nhất một ngôn ngữ OO

Tại Sao Design Quan Trọng

Viết code thì dễ. Viết code mà dễ thay đổi mới khó.

Không DesignCó Design
"Để code đại rồi tính sau""Để xác định objects và responsibilities trước"
God class 2000+ dòngClass gọn gàng với responsibility rõ ràng
Sửa một chỗ vỡ năm chỗThay đổi được isolate và dự đoán được
"Logic này bỏ đâu đây?"Mỗi phần logic đều có chỗ tự nhiên
Viết lại từ đầu mỗi 6 thángPhát triển cùng codebase nhiều năm

Design tốt không phải là làm perfect từ đầu — mà là đưa ra decision giúp giữ options mở khi requirements thay đổi.


Bước 1: Xác Định Objects Bằng CRC Cards

CRC (Class-Responsibility-Collaborator) cards là công cụ nhẹ nhàng để khám phá objects trong hệ thống. Được phát minh bởi Kent Beck và Ward Cunningham, chúng đã được sử dụng từ cuối thập niên 1980.

CRC Cards Hoạt Động Như Thế Nào

Mỗi card đại diện cho một class tiềm năng:

┌──────────────────────────────────────────────┐
│  Class Name: Order                           │
├──────────────────────┬───────────────────────┤
│  Responsibilities    │  Collaborators        │
│                      │                       │
│  - Calculate total   │  - OrderItem          │
│  - Apply discount    │  - DiscountPolicy     │
│  - Track status      │  - PaymentGateway     │
│  - Generate invoice  │  - InvoiceService     │
│                      │                       │
└──────────────────────┴───────────────────────┘

Quy Trình CRC

  1. Đọc requirements và gạch chân danh từ — đó là các class ứng viên
  2. Gạch chân động từ — đó là các responsibility ứng viên
  3. Tạo card cho mỗi danh từ chính
  4. Viết responsibilities ở cột bên trái
  5. Viết collaborators (class khác cần thiết) ở cột bên phải
  6. Role-play scenarios: Đi qua các use case xem cards có handle được không

Ví Dụ: Nhà Sách Online

Requirements: "Customers can browse books, add them to a shopping cart, and place orders. Orders can have discount codes applied. Customers receive email confirmations."

Danh từ (class ứng viên): Customer, Book, ShoppingCart, Order, DiscountCode, EmailConfirmation

Động từ (responsibility ứng viên): browse, add, place order, apply discount, send confirmation

┌──────────────────────────────────────────────┐
│  Class Name: ShoppingCart                    │
├──────────────────────┬───────────────────────┤
│  Responsibilities    │  Collaborators        │
│                      │                       │
│  - Add item          │  - Book               │
│  - Remove item       │  - CartItem           │
│  - Calculate subtotal│                       │
│  - Apply discount    │  - DiscountCode       │
│  - Create order      │  - Order              │
│                      │                       │
└──────────────────────┴───────────────────────┘
 
┌──────────────────────────────────────────────┐
│  Class Name: Order                           │
├──────────────────────┬───────────────────────┤
│  Responsibilities    │  Collaborators        │
│                      │                       │
│  - Store line items  │  - OrderItem          │
│  - Calculate total   │  - DiscountCode       │
│  - Track status      │                       │
│  - Confirm placement │  - NotificationService│
│                      │                       │
└──────────────────────┴───────────────────────┘

Dấu Hiệu Cảnh Báo Trên CRC Cards

  • Quá nhiều responsibilities: Card đang làm quá nhiều — tách ra
  • Không có collaborators: Class có thể chỉ là data holder — có thực sự cần không?
  • Quá nhiều collaborators: Class là coordinator, không phải doer — phân bổ lại responsibilities
  • Responsibilities trùng lặp: Hai cards cùng làm một việc — gộp lại

Bước 2: Phân Bổ Responsibility Bằng GRASP

GRASP (General Responsibility Assignment Software Patterns) là chín principle giúp bạn quyết định class nào nên chịu trách nhiệm gì. Được tạo bởi Craig Larman, GRASP trả lời câu hỏi cơ bản nhất trong OO design: "Ai làm gì?"

Pattern 1: Information Expert

Giao responsibility cho class có information cần thiết để hoàn thành nó.

Đây là GRASP pattern quan trọng nhất. Nếu class đã có data, nó nên chịu trách nhiệm về behavior sử dụng data đó.

// TypeScript — Information Expert
 
// ❌ Sai: Class bên ngoài tính tổng order
class OrderCalculator {
  calculateTotal(order: Order): number {
    let total = 0;
    for (const item of order.items) {
      total += item.price * item.quantity;
    }
    return total - order.discount;
  }
}
 
// ✅ Đúng: Order tự tính tổng (nó có data)
class Order {
  private items: OrderItem[] = [];
  private discount: number = 0;
 
  getTotal(): number {
    const subtotal = this.items.reduce(
      (sum, item) => sum + item.getSubtotal(), 0
    );
    return subtotal - this.discount;
  }
}
 
class OrderItem {
  constructor(
    private product: Product,
    private quantity: number
  ) {}
 
  getSubtotal(): number {
    return this.product.price * this.quantity;
  }
}
# Python — Information Expert
 
# ❌ Sai: Hàm bên ngoài tính tổng
def calculate_order_total(order):
    total = sum(item.price * item.quantity for item in order.items)
    return total - order.discount
 
# ✅ Đúng: Order biết cách tính tổng của chính nó
class Order:
    def __init__(self):
        self._items: list[OrderItem] = []
        self._discount: float = 0
 
    def get_total(self) -> float:
        subtotal = sum(item.get_subtotal() for item in self._items)
        return subtotal - self._discount
 
class OrderItem:
    def __init__(self, product: Product, quantity: int):
        self._product = product
        self._quantity = quantity
 
    def get_subtotal(self) -> float:
        return self._product.price * self._quantity

Pattern 2: Creator

Giao cho class B trách nhiệm tạo instance của class A nếu B chứa, aggregate, ghi nhận, hoặc sử dụng A nhiều.

// TypeScript — Creator
 
// ❌ Sai: Factory bên ngoài cho objects liên quan chặt
class OrderItemFactory {
  static create(product: Product, qty: number): OrderItem {
    return new OrderItem(product, qty);
  }
}
 
// ✅ Đúng: ShoppingCart tạo OrderItems (nó aggregate chúng)
class ShoppingCart {
  private items: CartItem[] = [];
 
  addItem(product: Product, quantity: number): void {
    const existing = this.items.find(
      item => item.product.id === product.id
    );
    if (existing) {
      existing.quantity += quantity;
    } else {
      this.items.push(new CartItem(product, quantity));
    }
  }
}

Pattern 3: Controller

Giao trách nhiệm xử lý system event cho class đại diện hệ thống hoặc một use case scenario.

// TypeScript — Controller
 
// ❌ Sai: UI trực tiếp thao tác domain objects
class CheckoutButton {
  onClick(): void {
    const order = new Order();
    order.addItems(cart.getItems());
    order.applyDiscount(discountCode);
    paymentGateway.charge(order.getTotal());
    emailService.sendConfirmation(order);
    inventory.reduce(order.getItems());
  }
}
 
// ✅ Đúng: Use case controller điều phối workflow
class CheckoutController {
  constructor(
    private orderService: OrderService,
    private paymentGateway: PaymentGateway,
    private notificationService: NotificationService
  ) {}
 
  checkout(cart: ShoppingCart, discountCode?: string): Order {
    const order = this.orderService.createFromCart(cart);
 
    if (discountCode) {
      order.applyDiscount(discountCode);
    }
 
    this.paymentGateway.charge(order.getTotal());
    this.notificationService.sendConfirmation(order);
 
    return order;
  }
}

Pattern 4: Low Coupling

Phân bổ responsibility sao cho coupling giữ thấp. Giảm impact của change.

Chúng ta sẽ đi sâu ở phần tiếp theo.

Pattern 5: High Cohesion

Phân bổ responsibility sao cho cohesion giữ cao. Giữ class tập trung.

Chúng ta sẽ đi sâu ở phần tiếp theo.

Pattern 6: Polymorphism

Khi behavior thay đổi theo type, giao responsibility cho các type bằng polymorphism.

// TypeScript — Polymorphism (GRASP)
 
// ❌ Sai: Logic điều kiện dựa trên type
class ShippingCalculator {
  calculate(order: Order): number {
    if (order.type === "standard") {
      return order.weight * 5;
    } else if (order.type === "express") {
      return order.weight * 10 + 15;
    } else if (order.type === "overnight") {
      return order.weight * 20 + 30;
    }
    return 0;
  }
}
 
// ✅ Đúng: Mỗi shipping type biết cách tính của mình
interface ShippingStrategy {
  calculate(weight: number): number;
}
 
class StandardShipping implements ShippingStrategy {
  calculate(weight: number): number {
    return weight * 5;
  }
}
 
class ExpressShipping implements ShippingStrategy {
  calculate(weight: number): number {
    return weight * 10 + 15;
  }
}
 
class OvernightShipping implements ShippingStrategy {
  calculate(weight: number): number {
    return weight * 20 + 30;
  }
}

Pattern 7: Indirection

Giao responsibility cho object trung gian để giảm direct coupling giữa các component.

# Python — Indirection
 
# ❌ Sai: Order coupling trực tiếp với tax API bên ngoài
class Order:
    def calculate_tax(self) -> float:
        response = requests.get(f"https://tax-api.com/rate?state={self.state}")
        rate = response.json()["rate"]
        return self.get_subtotal() * rate
 
# ✅ Đúng: TaxService làm trung gian giữa Order và external API
class TaxService:
    def __init__(self, api_client: TaxApiClient):
        self._api_client = api_client
 
    def calculate_tax(self, subtotal: float, state: str) -> float:
        rate = self._api_client.get_rate(state)
        return subtotal * rate
 
class Order:
    def __init__(self, tax_service: TaxService):
        self._tax_service = tax_service
 
    def get_tax(self) -> float:
        return self._tax_service.calculate_tax(
            self.get_subtotal(), self.state
        )

Pattern 8: Pure Fabrication

Khi không có domain object nào phù hợp, tạo service class để đạt low coupling và high cohesion.

// Java — Pure Fabrication
 
// Không có "PersistenceManager" nào trong domain e-commerce thực tế,
// nhưng nó là abstraction hữu ích cho design
 
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(String id);
    List<Order> findByCustomer(String customerId);
}
 
public class PostgresOrderRepository implements OrderRepository {
    private final DataSource dataSource;
 
    @Override
    public void save(Order order) {
        // SQL insert logic — không phải domain concern
    }
 
    @Override
    public Optional<Order> findById(String id) {
        // SQL select logic
    }
}

Pattern 9: Protected Variations

Xác định điểm có thể thay đổi và tạo stable interface bao quanh chúng.

// TypeScript — Protected Variations
 
// Payment provider sẽ thay đổi theo thời gian — protect against that
interface PaymentGateway {
  charge(amount: number, token: string): PaymentResult;
  refund(transactionId: string, amount: number): RefundResult;
}
 
class StripeGateway implements PaymentGateway {
  charge(amount: number, token: string): PaymentResult {
    // Stripe-specific implementation
  }
  refund(transactionId: string, amount: number): RefundResult {
    // Stripe-specific refund
  }
}
 
class PayPalGateway implements PaymentGateway {
  charge(amount: number, token: string): PaymentResult {
    // PayPal-specific implementation
  }
  refund(transactionId: string, amount: number): RefundResult {
    // PayPal-specific refund
  }
}
 
// Đổi payment provider không cần thay đổi business logic
class CheckoutService {
  constructor(private paymentGateway: PaymentGateway) {}
 
  processPayment(order: Order, paymentToken: string): void {
    const result = this.paymentGateway.charge(
      order.getTotal(), paymentToken
    );
    order.markAsPaid(result.transactionId);
  }
}

Bảng Tham Khảo Nhanh GRASP

PatternCâu Hỏi Nó Trả LờiNguyên Tắc Chung
Information ExpertAi nên làm việc này?Ai có data thì làm
CreatorAi nên tạo cái này?Ai chứa/aggregate nó
ControllerAi xử lý event này?Use case hoặc system controller
Low CouplingLàm sao giảm dependency?Depend on abstraction
High CohesionLàm sao giữ class tập trung?Mỗi class một mục đích rõ ràng
PolymorphismXử lý behavior theo type thế nào?Dùng interface, không dùng conditional
IndirectionDecouple hai component thế nào?Thêm mediator ở giữa
Pure FabricationNếu không có domain class phù hợp?Tạo service class
Protected VariationsXử lý change trong tương lai thế nào?Wrap variation sau interface

Bước 3: Đo Lường Design Quality

Hai metric chi phối việc đánh giá OO design: coupling và cohesion. Mọi design decision bạn đưa ra nên hướng tới low coupling và high cohesion.

Coupling: Class Của Bạn Kết Nối Chặt Cỡ Nào?

Coupling đo mức độ một class phụ thuộc vào class khác. High coupling nghĩa là thay đổi lan tỏa khắp hệ thống.

Các Mức Coupling (Tốt Đến Xấu)

// TypeScript — Ví Dụ Coupling
 
// ✅ Message Coupling (Tốt nhất): Chỉ biết tên method
class OrderService {
  notify(order: Order): void {
    this.notifier.send(order.getCustomerEmail(), "Order confirmed");
  }
}
 
// ⚠️ Data Coupling: Truyền primitive data
class ShippingService {
  calculateCost(weight: number, distance: number): number {
    return weight * 0.5 + distance * 0.1;
  }
}
 
// ⚠️ Stamp Coupling: Truyền cả object nhưng chỉ dùng một phần
class EmailService {
  sendConfirmation(order: Order): void {
    // Chỉ dùng email và orderId, nhưng nhận cả Order
    this.send(order.customerEmail, `Order ${order.id} confirmed`);
  }
}
 
// ❌ Control Coupling: Truyền flag điều khiển behavior
class ReportGenerator {
  generate(data: SalesData, format: "pdf" | "csv" | "html"): void {
    if (format === "pdf") { /* ... */ }
    else if (format === "csv") { /* ... */ }
    else if (format === "html") { /* ... */ }
  }
}
 
// ❌ Content Coupling (Tệ nhất): Truy cập trực tiếp internal
class BadAuditor {
  audit(order: Order): void {
    // Chọc vào internal state — phá vỡ encapsulation
    const items = (order as any)._internalItems;
    const rawDiscount = (order as any)._discountValue;
  }
}

Cách Giảm Coupling

  1. Depend on interface, không phải concrete class
  2. Dùng dependency injection thay vì tạo dependency bên trong
  3. Tuân thủ Law of Demeter: Chỉ nói chuyện với bạn trực tiếp
  4. Dùng events/observers cho cross-cutting concern

Cohesion: Class Của Bạn Tập Trung Cỡ Nào?

Cohesion đo mức độ liên quan giữa các responsibility trong một class. High cohesion nghĩa là class làm một việc tốt.

Các Mức Cohesion (Tệ Đến Tốt)

// TypeScript — Ví Dụ Cohesion
 
// ❌ Coincidental Cohesion (Tệ nhất): Method không liên quan gộp chung
class Utilities {
  formatDate(date: Date): string { /* ... */ }
  calculateTax(amount: number): number { /* ... */ }
  sendEmail(to: string, body: string): void { /* ... */ }
  compressImage(path: string): Buffer { /* ... */ }
}
 
// ❌ Logical Cohesion: Liên quan theo danh mục, không theo mục đích
class DataParser {
  parseJSON(data: string): object { /* ... */ }
  parseXML(data: string): object { /* ... */ }
  parseCSV(data: string): object { /* ... */ }
  parseYAML(data: string): object { /* ... */ }
}
 
// ⚠️ Temporal Cohesion: Liên quan vì xảy ra cùng thời điểm
class ApplicationStartup {
  initialize(): void {
    this.loadConfig();
    this.connectDatabase();
    this.startServer();
    this.registerRoutes();
    this.warmCache();
  }
}
 
// ✅ Functional Cohesion (Tốt nhất): Mọi thứ phục vụ một mục đích
class PasswordHasher {
  private readonly saltRounds = 12;
 
  async hash(password: string): Promise<string> {
    return bcrypt.hash(password, this.saltRounds);
  }
 
  async verify(password: string, hash: string): Promise<boolean> {
    return bcrypt.compare(password, hash);
  }
}

Cân Bằng Coupling-Cohesion

Nguyên tắc chung: Nếu tăng cohesion khiến coupling cũng tăng, bạn đã đi quá xa. Tìm điểm cân bằng.


Bước 4: Áp Dụng Design Heuristics

Design heuristic là quy tắc ngón tay cái giúp bạn ra decision nhanh. Chúng không phải luật tuyệt đối — chúng là guardrail.

Heuristic 1: Shy, DRY, Tell the Other Guy

  • Shy (Encapsulation): Giảm thiểu những gì mỗi class expose
  • DRY (Don't Repeat Yourself): Gom shared logic vào một chỗ
  • Tell, Don't Ask: Bảo object làm gì, đừng hỏi data rồi tự xử lý
// TypeScript — Tell, Don't Ask
 
// ❌ Ask: Kéo data ra, rồi tự quyết bên ngoài
function processOrder(order: Order): void {
  const total = order.getTotal();
  const status = order.getStatus();
  if (status === "pending" && total > 0) {
    order.setStatus("processing");
    paymentService.charge(total, order.getPaymentToken());
    order.setStatus("paid");
  }
}
 
// ✅ Tell: Để order tự xử lý state transition
class Order {
  process(paymentService: PaymentService): void {
    if (!this.canBeProcessed()) {
      throw new Error("Order cannot be processed");
    }
    this.status = "processing";
    paymentService.charge(this.total, this.paymentToken);
    this.status = "paid";
  }
 
  private canBeProcessed(): boolean {
    return this.status === "pending" && this.total > 0;
  }
}

Heuristic 2: Law of Demeter

Chỉ nói chuyện với bạn trực tiếp. Đừng nói chuyện với người lạ.

Một method chỉ nên gọi method trên:

  • Object của chính nó (this)
  • Parameter của nó
  • Object nó tạo ra
  • Component trực tiếp (field)
# Python — Law of Demeter
 
# ❌ Vi phạm: Chọc xuyên qua objects (train wreck)
def get_city(order):
    return order.get_customer().get_address().get_city()
 
# ✅ Tuân thủ Demeter: Hỏi collaborator trực tiếp
class Order:
    def get_shipping_city(self) -> str:
        return self._customer.get_shipping_city()
 
class Customer:
    def get_shipping_city(self) -> str:
        return self._address.city

Heuristic 3: Ưu Tiên Composition Hơn Inheritance

Inheritance tạo tight coupling giữa class cha và con. Composition cho bạn code reuse tương tự nhưng flexible hơn.

// Java — Composition over Inheritance
 
// ❌ Inheritance: Tight coupling, fragile hierarchy
public class LoggingOrderService extends OrderService {
    @Override
    public Order createOrder(Cart cart) {
        logger.info("Creating order...");
        Order order = super.createOrder(cart);
        logger.info("Order created: " + order.getId());
        return order;
    }
}
 
// ✅ Composition: Flexible, swappable, testable
public class LoggingOrderService implements OrderService {
    private final OrderService delegate;
    private final Logger logger;
 
    public LoggingOrderService(OrderService delegate, Logger logger) {
        this.delegate = delegate;
        this.logger = logger;
    }
 
    @Override
    public Order createOrder(Cart cart) {
        logger.info("Creating order...");
        Order order = delegate.createOrder(cart);
        logger.info("Order created: " + order.getId());
        return order;
    }
}

Heuristic 4: Design Cho Interface, Không Phải Implementation

// TypeScript — Interface-based design
 
// ❌ Phụ thuộc vào implementation detail
class OrderProcessor {
  private db: PostgresDatabase;
  private mailer: SmtpMailer;
 
  process(order: Order): void {
    this.db.insertRow("orders", order.toRow());
    this.mailer.sendSmtpMessage(order.email, "Order confirmed");
  }
}
 
// ✅ Phụ thuộc vào interface — implementation có thể thay đổi thoải mái
class OrderProcessor {
  constructor(
    private repository: OrderRepository,
    private notifier: Notifier
  ) {}
 
  process(order: Order): void {
    this.repository.save(order);
    this.notifier.notify(order.email, "Order confirmed");
  }
}

Heuristic 5: Đừng Design Thứ Chưa Cần (YAGNI)

You Aren't Gonna Need It.

// ❌ Over-designed: Support 5 format export "phòng khi cần"
interface ExportStrategy { /* ... */ }
class JSONExporter implements ExportStrategy { /* ... */ }
class XMLExporter implements ExportStrategy { /* ... */ }
class CSVExporter implements ExportStrategy { /* ... */ }
class PDFExporter implements ExportStrategy { /* ... */ }
class ExcelExporter implements ExportStrategy { /* ... */ }
 
// ✅ Vừa đủ: Chỉ build thứ cần ngay bây giờ
class ReportExporter {
  exportToCSV(report: Report): string {
    // Format duy nhất khách hàng yêu cầu
    return report.rows.map(row => row.join(",")).join("\n");
  }
}
// Khi nào cần PDF, LÚC ĐÓ mới thêm strategy pattern

Bảng Design Heuristics

HeuristicKhi Nào Áp DụngDấu Hiệu Cảnh Báo
Tell, Don't AskKhi bạn lấy data chỉ để ra quyết địnhGetter calls rồi conditional logic
Law of DemeterKhi thấy chuỗi method call dàia.getB().getC().doSomething()
Composition > InheritanceKhi muốn extend classDeep inheritance hierarchy (>2 level)
Interface-based DesignKhi depend on external systemImport trực tiếp concrete class
YAGNIKhi thêm feature "phòng khi cần"Interface chưa dùng, abstract class chỉ có 1 impl

Kết Hợp Tất Cả: Walkthrough Thiết Kế

Hãy design một hệ thống quản lý thư viện từ requirements đến code.

Requirements

"Librarians can add books to the catalog. Members can search for books, borrow them (max 3 at a time), and return them. Overdue books incur a daily fine. Members receive email notifications when books are due soon."

Bước 1: Xác Định Objects (CRC)

Danh từ: Librarian, Book, Catalog, Member, Loan, Fine, EmailNotification

Động từ: add, search, borrow, return, calculate fine, send notification

Sau khi tạo CRC cards và role-play scenarios:

Bước 2: Phân Bổ Responsibility (GRASP)

Quyết ĐịnhGRASP PatternLý Do
Ai tính fine?Information ExpertLoan có due date và return date
Ai tạo Loans?CreatorMember aggregate loans
Ai xử lý borrow workflow?ControllerLibraryService điều phối use case
Ai gửi notification?Pure FabricationNotificationService — không có domain counterpart
Support email/SMS/push thế nào?Protected VariationsNotifier interface wrap variation

Bước 3: Viết Code

// TypeScript — Hệ Thống Quản Lý Thư Viện
 
// Domain Objects
class Book {
  constructor(
    public readonly id: string,
    public readonly title: string,
    public readonly author: string,
    public readonly isbn: string,
    private available: boolean = true
  ) {}
 
  isAvailable(): boolean {
    return this.available;
  }
 
  markBorrowed(): void {
    if (!this.available) {
      throw new Error(`Book "${this.title}" is not available`);
    }
    this.available = false;
  }
 
  markReturned(): void {
    this.available = true;
  }
}
 
class Loan {
  private static readonly FINE_PER_DAY = 0.50;
  private static readonly LOAN_PERIOD_DAYS = 14;
  private returnedAt?: Date;
 
  public readonly dueDate: Date;
 
  constructor(
    public readonly book: Book,
    public readonly borrowedAt: Date = new Date()
  ) {
    this.dueDate = new Date(borrowedAt);
    this.dueDate.setDate(this.dueDate.getDate() + Loan.LOAN_PERIOD_DAYS);
  }
 
  isOverdue(now: Date = new Date()): boolean {
    if (this.returnedAt) return false;
    return now > this.dueDate;
  }
 
  // Information Expert: Loan có due date và return date
  calculateFine(now: Date = new Date()): number {
    if (!this.isOverdue(now)) return 0;
    const overdueDays = Math.ceil(
      (now.getTime() - this.dueDate.getTime()) / (1000 * 60 * 60 * 24)
    );
    return overdueDays * Loan.FINE_PER_DAY;
  }
 
  markReturned(): void {
    this.returnedAt = new Date();
    this.book.markReturned();
  }
 
  isActive(): boolean {
    return !this.returnedAt;
  }
}
 
class Member {
  private static readonly MAX_LOANS = 3;
  private loans: Loan[] = [];
 
  constructor(
    public readonly id: string,
    public readonly name: string,
    public readonly email: string
  ) {}
 
  // Creator: Member aggregate Loans
  borrow(book: Book): Loan {
    if (this.getActiveLoans().length >= Member.MAX_LOANS) {
      throw new Error(`Member "${this.name}" has reached the borrowing limit`);
    }
    book.markBorrowed();
    const loan = new Loan(book);
    this.loans.push(loan);
    return loan;
  }
 
  returnBook(book: Book): number {
    const loan = this.loans.find(
      l => l.book.id === book.id && l.isActive()
    );
    if (!loan) {
      throw new Error(`No active loan found for "${book.title}"`);
    }
    const fine = loan.calculateFine();
    loan.markReturned();
    return fine;
  }
 
  getActiveLoans(): Loan[] {
    return this.loans.filter(loan => loan.isActive());
  }
 
  getOverdueLoans(): Loan[] {
    return this.getActiveLoans().filter(loan => loan.isOverdue());
  }
}
 
// Pure Fabrication: Không có domain counterpart
class Catalog {
  private books: Map<string, Book> = new Map();
 
  addBook(book: Book): void {
    this.books.set(book.id, book);
  }
 
  search(query: string): Book[] {
    const lower = query.toLowerCase();
    return Array.from(this.books.values()).filter(
      book =>
        book.title.toLowerCase().includes(lower) ||
        book.author.toLowerCase().includes(lower) ||
        book.isbn.includes(lower)
    );
  }
 
  getAvailableBooks(): Book[] {
    return Array.from(this.books.values()).filter(b => b.isAvailable());
  }
}
 
// Protected Variations: Phương thức notification có thể thay đổi
interface Notifier {
  send(to: string, subject: string, message: string): Promise<void>;
}
 
// Controller: Điều phối use case borrow
class LibraryService {
  constructor(
    private catalog: Catalog,
    private notifier: Notifier
  ) {}
 
  async borrowBook(member: Member, bookId: string): Promise<Loan> {
    const book = this.catalog.search(bookId)[0];
    if (!book) throw new Error("Book not found");
 
    const loan = member.borrow(book);
 
    await this.notifier.send(
      member.email,
      `Book Borrowed: ${book.title}`,
      `Due date: ${loan.dueDate.toLocaleDateString()}`
    );
 
    return loan;
  }
 
  async returnBook(member: Member, bookId: string): Promise<number> {
    const book = this.catalog.search(bookId)[0];
    if (!book) throw new Error("Book not found");
 
    const fine = member.returnBook(book);
 
    if (fine > 0) {
      await this.notifier.send(
        member.email,
        "Overdue Fine",
        `You owe $${fine.toFixed(2)} for late return of "${book.title}"`
      );
    }
 
    return fine;
  }
}

Phân Tích Design

Kiểm tra design theo các principle:

PrincipleĐánh Giá
Information Expert✅ Loan tính fine (có due date), Member check limit (có loans)
Creator✅ Member tạo Loan objects (aggregate chúng)
Controller✅ LibraryService điều phối workflow
Low Coupling✅ Notifier interface decouple khỏi email implementation
High Cohesion✅ Mỗi class có mục đích tập trung
Protected Variations✅ Notification method wrap sau interface
Tell, Don't Ask✅ member.borrow(book) thay vì check limit bên ngoài
Law of Demeter✅ Không có chuỗi method call dài

Sai Lầm Design Phổ Biến

Sai Lầm 1: Anemic Domain Model

Object chỉ là data holder với getter/setter, trong khi mọi logic nằm trong service class.

// ❌ Anemic: Domain object chỉ là túi data
class Order {
  id: string;
  items: OrderItem[];
  status: string;
  discount: number;
}
 
// Mọi logic nằm ngoài object
class OrderService {
  calculateTotal(order: Order): number { /* ... */ }
  applyDiscount(order: Order, code: string): void { /* ... */ }
  canBeCancelled(order: Order): boolean { /* ... */ }
  cancel(order: Order): void { /* ... */ }
}
 
// ✅ Rich: Domain object sở hữu behavior của nó
class Order {
  private items: OrderItem[] = [];
  private status: OrderStatus = "pending";
  private discount: number = 0;
 
  getTotal(): number { /* ... */ }
  applyDiscount(policy: DiscountPolicy): void { /* ... */ }
  cancel(): void {
    if (this.status !== "pending") {
      throw new Error("Only pending orders can be cancelled");
    }
    this.status = "cancelled";
  }
}

Sai Lầm 2: God Class

Một class làm mọi thứ. Hàng chục method, hàng trăm dòng, và được import ở khắp nơi.

Triệu chứng: File 500+ dòng, 10+ dependency, method không sử dụng nhau.

Cách sửa: Tách theo responsibility. Nếu class có method gộp được theo theme, mỗi theme là ứng viên cho class riêng.

Sai Lầm 3: Premature Abstraction

Tạo interface, abstract class và factory trước khi có hai concrete implementation.

// ❌ Premature: Chỉ có một implementation
interface IUserRepository { /* ... */ }
interface IUserService { /* ... */ }
interface IUserValidator { /* ... */ }
abstract class BaseUserHandler { /* ... */ }
class UserRepositoryFactory { /* ... */ }
class UserServiceImpl implements IUserService { /* ... */ }
 
// ✅ Vừa đủ: Concrete class, extract interface KHI cần polymorphism
class UserRepository {
  save(user: User): void { /* ... */ }
  findById(id: string): User | null { /* ... */ }
}

Sai Lầm 4: Feature Envy

Method dùng data của class khác nhiều hơn class của mình.

# ❌ Feature Envy: Method này thuộc về Order, không phải ReportGenerator
class ReportGenerator:
    def generate_order_summary(self, order: Order) -> str:
        total = sum(item.price * item.qty for item in order.items)
        tax = total * order.tax_rate
        shipping = order.weight * order.shipping_rate
        return f"Total: {total + tax + shipping}"
 
# ✅ Chuyển logic đến nơi có data
class Order:
    def get_summary(self) -> str:
        return f"Total: {self.get_total()}"
 
    def get_total(self) -> float:
        return self._subtotal() + self._tax() + self._shipping()

Khi Nào Nên Dừng Design

Design là phương tiện, không phải mục đích. Đây là dấu hiệu bạn sẵn sàng code:

  1. Bạn giải thích được mục đích mỗi class trong một câu
  2. Bạn biết class nào xử lý mỗi use case chính
  3. Không class nào có hơn 5-7 responsibility
  4. Bạn đã role-play các scenario chính qua objects
  5. Bạn có hình ảnh rõ ràng về interface giữa các component

Và dấu hiệu bạn đang over-design:

  • Interface nhiều hơn concrete class
  • Design cho requirement chưa ai yêu cầu
  • Dành thời gian vẽ diagram nhiều hơn code
  • Mỗi operation đơn giản cần 5 class và 3 interface

"Design tốt nhất là design đơn giản nhất hoạt động được và dễ thay đổi."


Tổng Kết

Object-Oriented Design là skill cải thiện qua practice. Đây là toolkit của bạn:

Quy Trình OO Design:
✅ CRC Cards — Khám phá object và responsibility từ requirements
✅ GRASP Patterns — Phân bổ responsibility có hệ thống
✅ Coupling & Cohesion — Đo lường và cải thiện design quality
✅ Design Heuristics — Áp dụng quy tắc ngón tay cái

Principle Cần Nhớ:
✅ Giao behavior cho class có data (Information Expert)
✅ Bảo object làm gì, đừng hỏi state (Tell, Don't Ask)
✅ Depend on abstraction, không phải implementation (Protected Variations)
✅ Chỉ nói chuyện với bạn trực tiếp (Law of Demeter)
✅ Build thứ cần bây giờ, không phải thứ có thể cần sau (YAGNI)

Bài Tập Practice: Lấy một feature bạn vừa build và vẽ CRC cards cho nó. Responsibility có phân bổ tốt không? GRASP có suggest chuyển behavior nào không? Phân tích ngược như này là cách nhanh nhất để mài sắc design skill.


Đọc Thêm

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.