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 Design | Có Design |
|---|---|
| "Để code đại rồi tính sau" | "Để xác định objects và responsibilities trước" |
| God class 2000+ dòng | Class 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áng | Phá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
- Đọc requirements và gạch chân danh từ — đó là các class ứng viên
- Gạch chân động từ — đó là các responsibility ứng viên
- Tạo card cho mỗi danh từ chính
- Viết responsibilities ở cột bên trái
- Viết collaborators (class khác cần thiết) ở cột bên phải
- 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._quantityPattern 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
| Pattern | Câu Hỏi Nó Trả Lời | Nguyên Tắc Chung |
|---|---|---|
| Information Expert | Ai nên làm việc này? | Ai có data thì làm |
| Creator | Ai nên tạo cái này? | Ai chứa/aggregate nó |
| Controller | Ai xử lý event này? | Use case hoặc system controller |
| Low Coupling | Làm sao giảm dependency? | Depend on abstraction |
| High Cohesion | Làm sao giữ class tập trung? | Mỗi class một mục đích rõ ràng |
| Polymorphism | Xử lý behavior theo type thế nào? | Dùng interface, không dùng conditional |
| Indirection | Decouple hai component thế nào? | Thêm mediator ở giữa |
| Pure Fabrication | Nếu không có domain class phù hợp? | Tạo service class |
| Protected Variations | Xử 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
- Depend on interface, không phải concrete class
- Dùng dependency injection thay vì tạo dependency bên trong
- Tuân thủ Law of Demeter: Chỉ nói chuyện với bạn trực tiếp
- 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.cityHeuristic 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 patternBảng Design Heuristics
| Heuristic | Khi Nào Áp Dụng | Dấu Hiệu Cảnh Báo |
|---|---|---|
| Tell, Don't Ask | Khi bạn lấy data chỉ để ra quyết định | Getter calls rồi conditional logic |
| Law of Demeter | Khi thấy chuỗi method call dài | a.getB().getC().doSomething() |
| Composition > Inheritance | Khi muốn extend class | Deep inheritance hierarchy (>2 level) |
| Interface-based Design | Khi depend on external system | Import trực tiếp concrete class |
| YAGNI | Khi 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 Định | GRASP Pattern | Lý Do |
|---|---|---|
| Ai tính fine? | Information Expert | Loan có due date và return date |
| Ai tạo Loans? | Creator | Member aggregate loans |
| Ai xử lý borrow workflow? | Controller | LibraryService điều phối use case |
| Ai gửi notification? | Pure Fabrication | NotificationService — không có domain counterpart |
| Support email/SMS/push thế nào? | Protected Variations | Notifier 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:
- Bạn giải thích được mục đích mỗi class trong một câu
- Bạn biết class nào xử lý mỗi use case chính
- Không class nào có hơn 5-7 responsibility
- Bạn đã role-play các scenario chính qua objects
- 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
- The Four Pillars of OOP — Nền tảng OO concept
- SOLID Principles Explained — Năm design principle cho clean code
- Inheritance and Composition — Khi nào dùng approach nào
- Strategy and Template Method Patterns — Behavioral pattern phổ biến
- Software Architecture Patterns — Đưa design lên system level
📬 Đă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.