1. Code Smells — dấu hiệu code có vấn đề
Martin Fowler (sách Refactoring, 1999) liệt kê > 20 code smell. Đây là những cái phổ biến nhất:
1.1. Bloaters — code phình
- Long Method — function > 30-50 dòng. Khó đọc, khó test.
- Large Class — > 500 dòng, > 20 method. Vi phạm SRP.
- Long Parameter List — > 4 tham số. Khó nhớ thứ tự, dễ swap.
- Data Clumps — 2-3 field luôn đi cùng nhau ở nhiều chỗ → tạo class chứa cả nhóm.
- Primitive Obsession — dùng
string/numberraw cho concept (email, money) → tạo Value Object.
1.2. OO abusers — dùng OOP sai cách
- Switch Statements — switch theo type → thường dấu hiệu thiếu polymorphism. Refactor sang Strategy/State/Visitor.
- Temporary Field — field chỉ dùng trong vài method, lúc khác là null/0.
- Refused Bequest — subclass không dùng phần lớn parent → có thể không nên kế thừa.
- Alternative Classes with Different Interfaces — 2 class làm việc tương tự nhưng API khác.
1.3. Change preventers — khó thay đổi
- Divergent Change — 1 class phải sửa vì nhiều lý do khác nhau (vi phạm SRP).
- Shotgun Surgery — 1 thay đổi business → phải sửa nhiều class.
- Parallel Inheritance Hierarchies — thêm class ở hierarchy A → phải thêm tương ứng ở B.
1.4. Dispensables — thừa thãi
- Comments — comment giải thích "what" thay vì code rõ. Đa số comment có thể loại bằng đặt tên tốt.
- Duplicated Code — copy-paste 3 chỗ trở lên → DRY.
- Lazy Class — class quá nhỏ, không justify tồn tại.
- Data Class — class chỉ field + getter/setter (anemic).
- Speculative Generality — abstraction "phòng xa" mà không có biến thể thực.
- Dead Code — code không bao giờ chạy.
1.5. Couplers — coupling chặt
- Feature Envy — method 1 class dùng nhiều thứ của class khác hơn của chính nó. Có thể nên move method.
- Inappropriate Intimacy — 2 class biết quá nhiều về nội bộ nhau.
- Message Chains —
a.b().c().d()— vi phạm Demeter. - Middle Man — class chỉ delegate gần như mọi thứ sang class khác.
2. Refactoring catalog — kỹ thuật sửa
Fowler liệt kê > 70 refactoring. Top 10 thực dụng nhất:
2.1. Extract Method
// ❌ Long method
function printOrder(order: Order) {
console.log('=== Order ===');
console.log('ID:', order.id);
console.log('Customer:', order.customer.name);
let total = 0;
for (const item of order.items) {
total += item.price * item.qty;
}
console.log('Total:', total);
}
// ✓ Extract:
function printOrder(order: Order) {
printHeader(order);
console.log('Total:', calculateTotal(order));
}
function printHeader(o: Order) {
console.log('=== Order ===');
console.log('ID:', o.id);
console.log('Customer:', o.customer.name);
}
function calculateTotal(o: Order): number {
return o.items.reduce((s, i) => s + i.price * i.qty, 0);
}
2.2. Extract Variable
// ❌
if (order.customer.country === 'VN' && order.total > 1_000_000 && order.items.length > 5) { ... }
// ✓
const isVnCustomer = order.customer.country === 'VN';
const isLargeOrder = order.total > 1_000_000;
const hasManyItems = order.items.length > 5;
if (isVnCustomer && isLargeOrder && hasManyItems) { ... }
2.3. Inline Method/Variable
Ngược của extract — nếu helper không add value (chỉ rename), inline lại.
2.4. Extract Class
Class quá lớn → tách field/method liên quan ra class riêng (đã thấy ở SRP refactor Ch2).
2.5. Move Method/Field
Feature Envy → move method sang class mà nó "đang ghen".
// ❌ Feature Envy
class Order {
customer: Customer;
isVipDiscount(): boolean {
return this.customer.totalSpent() > 10_000_000 && this.customer.yearsActive() > 3;
}
}
// ✓ Method thuộc về Customer
class Customer {
totalSpent(): number { /* ... */ }
yearsActive(): number { /* ... */ }
isVip(): boolean {
return this.totalSpent() > 10_000_000 && this.yearsActive() > 3;
}
}
class Order {
isVipDiscount() { return this.customer.isVip(); }
}
2.6. Replace Conditional with Polymorphism
Switch theo type → polymorphism (Strategy/State/inheritance).
// ❌
function pay(method: string, amount: number) {
switch (method) {
case 'card': chargeCard(amount);
case 'paypal': chargePaypal(amount);
case 'crypto': chargeCrypto(amount);
}
}
// ✓ Strategy
interface PaymentMethod { pay(amount: number): void; }
class CardPayment implements PaymentMethod { pay(amount: number) {} }
class PaypalPayment implements PaymentMethod { pay(amount: number) {} }
class CryptoPayment implements PaymentMethod { pay(amount: number) {} }
function pay(method: PaymentMethod, amount: number) {
method.pay(amount);
}
2.7. Replace Magic Number with Named Constant
// ❌
if (employee.workedYears > 5) salary *= 1.15;
// ✓
const SENIORITY_THRESHOLD_YEARS = 5;
const SENIORITY_BONUS = 1.15;
if (employee.workedYears > SENIORITY_THRESHOLD_YEARS) salary *= SENIORITY_BONUS;
2.8. Introduce Parameter Object
Long parameter list → gom thành object.
// ❌
search(query, page, size, sortBy, order, filters, language, region) { }
// ✓
type SearchOptions = { query: string; page: number; size: number; sortBy: string; order: 'asc'|'desc'; filters: Filter[]; language: string; region: string; };
search(options: SearchOptions) { }
2.9. Replace Type Code with Subclasses (or Strategy)
// ❌
class Employee {
type: 'engineer' | 'manager' | 'salesperson';
payAmount(): number {
if (this.type === 'engineer') return baseSalary;
if (this.type === 'manager') return baseSalary + bonus;
if (this.type === 'salesperson') return baseSalary + commission;
}
}
// ✓ Subclass:
abstract class Employee { abstract payAmount(): number; }
class Engineer extends Employee { payAmount() { return baseSalary; } }
class Manager extends Employee { payAmount() { return baseSalary + bonus; } }
2.10. Encapsulate Field
Public field → private + getter/setter (hoặc method ý nghĩa hơn).
2.11. Extract Interface
Khi cần multiple implementation hoặc mock cho test.
3. Anti-patterns nổi tiếng
3.1. God Object / God Class
1 class biết và làm tất cả: 5000 dòng, 100 method, 50 field. Vi phạm SRP nặng. Sửa: tách theo concern.
3.2. Spaghetti Code
Logic nhảy linh tinh, GOTO, callback chained, không có cấu trúc. Sửa: refactor thành module + function thuần.
3.3. Lava Flow
Code chết / code "không ai biết để làm gì" tích lũy theo thời gian. Không ai dám xóa vì sợ break. Sửa: có test → mạnh dạn xóa.
3.4. Golden Hammer
"Khi bạn có búa, mọi vấn đề là cái đinh." Áp dụng 1 pattern/tool cho mọi thứ. Vd: dùng Singleton khắp nơi, dùng MongoDB cho mọi thứ.
3.5. Magic Numbers / Strings
if (status === 5), if (role === 'a'). Đặt tên const hoặc enum.
3.6. Yo-yo Problem
Hierarchy quá sâu, đọc 1 method phải nhảy lên xuống 5 cấp parent. Hậu quả của over-inheritance.
3.7. Boat Anchor
Code/feature thêm "vì có thể cần" nhưng không bao giờ dùng. YAGNI nói: xóa.
3.8. Premature Optimization
Tối ưu code chưa profile chỉ rõ là chậm. Donald Knuth: "Premature optimization is the root of all evil."
3.9. Copy-Paste Programming
3 chỗ giống nhau trở lên → DRY. Nhưng cẩn thận: không phải mọi giống nhau đều cùng 1 lý do thay đổi.
3.10. Cargo Cult Programming
Áp dụng pattern/practice mà không hiểu vì sao. "Vì big tech làm vậy". Mỗi quyết định phải có lý do cụ thể với context của bạn.
4. Case study refactor — Order Total Calculation
4.1. Code "trước"
function calculateOrderTotal(order: any) {
let total = 0;
for (let i = 0; i < order.items.length; i++) {
let item = order.items[i];
let lineTotal = item.price * item.qty;
// Discount theo category
if (item.category === 'electronics' && item.qty >= 3) {
lineTotal = lineTotal * 0.95;
}
if (item.category === 'food' && item.expiry < new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)) {
lineTotal = lineTotal * 0.7;
}
total += lineTotal;
}
// VIP discount
if (order.customer.totalSpent > 10000000 && order.customer.yearsActive > 3) {
total = total * 0.9;
}
// Holiday
let now = new Date();
if (now.getMonth() === 11 && now.getDate() >= 20) {
total = total - 50000;
}
// Shipping
if (order.customer.country === 'VN') {
if (total > 500000) total += 0;
else total += 30000;
} else {
total += 200000;
}
if (total < 0) total = 0;
return total;
}
Vấn đề:
- Long method, dày đặc magic numbers/dates.
- Mọi rule mới phải sửa hàm này (vi phạm OCP).
- Khó test từng rule riêng.
- Logic VIP, holiday, shipping trộn lẫn.
- Không có Value Object — Money là number.
4.2. Refactor
// Step 1: Value Object
class Money {
constructor(public readonly amount: number, public readonly currency: string = 'VND') {}
add(other: Money) { return new Money(this.amount + other.amount, this.currency); }
subtract(other: Money) { return new Money(Math.max(0, this.amount - other.amount), this.currency); }
multiply(factor: number) { return new Money(this.amount * factor, this.currency); }
static zero() { return new Money(0); }
}
// Step 2: Strategy cho rule
interface PricingRule {
apply(order: Order, current: Money): Money;
}
class CategoryDiscountRule implements PricingRule {
apply(order: Order, current: Money): Money {
let total = Money.zero();
for (const item of order.items) {
let line = new Money(item.price).multiply(item.qty);
if (item.category === 'electronics' && item.qty >= 3) {
line = line.multiply(0.95);
}
if (item.category === 'food' && item.willExpireWithin(7)) {
line = line.multiply(0.70);
}
total = total.add(line);
}
return total;
}
}
class VipDiscountRule implements PricingRule {
static readonly MIN_SPENT = 10_000_000;
static readonly MIN_YEARS = 3;
static readonly DISCOUNT = 0.9;
apply(order: Order, current: Money): Money {
if (order.customer.totalSpent > VipDiscountRule.MIN_SPENT &&
order.customer.yearsActive > VipDiscountRule.MIN_YEARS) {
return current.multiply(VipDiscountRule.DISCOUNT);
}
return current;
}
}
class HolidayPromoRule implements PricingRule {
static readonly PROMO_AMOUNT = new Money(50_000);
apply(order: Order, current: Money): Money {
return this.isHoliday() ? current.subtract(HolidayPromoRule.PROMO_AMOUNT) : current;
}
private isHoliday(): boolean {
const now = new Date();
return now.getMonth() === 11 && now.getDate() >= 20;
}
}
class ShippingRule implements PricingRule {
static readonly DOMESTIC = new Money(30_000);
static readonly INTERNATIONAL = new Money(200_000);
static readonly FREE_THRESHOLD = new Money(500_000);
apply(order: Order, current: Money): Money {
if (order.customer.country !== 'VN') {
return current.add(ShippingRule.INTERNATIONAL);
}
if (current.amount > ShippingRule.FREE_THRESHOLD.amount) return current;
return current.add(ShippingRule.DOMESTIC);
}
}
// Step 3: Pricing engine
class PricingEngine {
constructor(private rules: PricingRule[]) {}
calculate(order: Order): Money {
return this.rules.reduce((current, rule) => rule.apply(order, current), Money.zero());
}
}
// Wire:
const engine = new PricingEngine([
new CategoryDiscountRule(),
new VipDiscountRule(),
new HolidayPromoRule(),
new ShippingRule(),
]);
const total = engine.calculate(order);
4.3. So sánh
- Mỗi rule test riêng được.
- Thêm rule mới — chỉ implement
PricingRule, inject vào engine. OCP. - Magic numbers thành named constant trong class.
- Money là Value Object — không thể có total âm.
- Tổng dòng tăng (~80 dòng vs 30) nhưng maintainability tốt hơn nhiều.
5. Kiến trúc Layered (3-tier / N-tier)
Pattern kiến trúc cổ điển. Tách app thành các layer chồng nhau, mỗi layer chỉ phụ thuộc layer dưới.
Quy tắc: phụ thuộc đi xuống. Domain không import Infrastructure. Application không import Presentation.
5.1. Vấn đề với layered cổ điển
Domain phụ thuộc Infrastructure (qua import implementation cụ thể). Khi DB đổi, Domain phải sửa. Vi phạm DIP.
Hexagonal/Clean architecture giải quyết vấn đề này.
6. Hexagonal Architecture (Ports & Adapters)
Alistair Cockburn (2005): Domain ở giữa, mọi thứ khác (UI, DB, external service) ở ngoài, kết nối qua "port" (interface) + "adapter" (implementation).
Domain chỉ định nghĩa port (interface) theo nhu cầu. Adapter là implementation cụ thể (DB driver, HTTP client). Domain không biết Postgres hay Sendgrid.
6.1. Lợi ích
- Test domain với in-memory adapter (không cần DB thật).
- Đổi DB từ Postgres sang MongoDB — chỉ thay adapter.
- Domain logic thuần khiết, không bị "ô nhiễm" bởi tech detail.
6.2. Clean Architecture (Bob Martin, 2012)
Mở rộng Hexagonal với 4 layer concentric:
- Entities (innermost) — domain objects, business rules.
- Use Cases — application-specific logic.
- Interface Adapters — controllers, gateways, presenters.
- Frameworks & Drivers (outermost) — DB, web framework, devices.
Dependency rule: phụ thuộc chỉ đi vào trong (outer phụ thuộc inner, không ngược).
7. DDD-lite — 5 Building Blocks
Eric Evans (2003) đặt nền cho Domain-Driven Design. 5 building block là vốn tối thiểu để áp dụng:
7.1. Entity
Object có identity ổn định qua thời gian (ID không đổi, attribute có thể đổi). Hai entity khác nhau dù mọi field giống nhau, nếu khác ID.
class Customer {
constructor(
public readonly id: CustomerId, // identity
private name: string,
private email: Email,
) {}
changeEmail(newEmail: Email) { this.email = newEmail; } // attribute thay đổi
changeName(newName: string) { this.name = newName; }
// id KHÔNG đổi
}
7.2. Value Object
Object định danh qua giá trị thuộc tính, không có ID, immutable. (Đã đào sâu Ch3.)
class Money { /* immutable, equals theo amount + currency */ }
class Address { /* equals theo street+city+country */ }
class DateRange { /* equals theo start+end */ }
7.3. Aggregate
Cụm Entity + Value Object được protect bằng boundary transaction. Có 1 "Aggregate Root" — gateway duy nhất để truy cập.
// Aggregate root
class Order {
private _items: OrderItem[] = [];
constructor(
public readonly id: OrderId,
public readonly customerId: CustomerId,
) {}
addItem(productId: ProductId, qty: number, price: Money) {
if (this._items.length >= 100) throw new Error('max 100 items');
this._items.push(new OrderItem(productId, qty, price));
}
removeItem(productId: ProductId) { /* ... */ }
total(): Money {
return this._items.reduce((s, i) => s.add(i.subtotal()), Money.zero());
}
// External code KHÔNG được tham chiếu OrderItem trực tiếp
get items(): ReadonlyArray<OrderItem> { return this._items; }
}
// OrderItem là entity nội bộ aggregate, không tự đứng riêng
class OrderItem {
constructor(public readonly productId: ProductId, public qty: number, public price: Money) {}
subtotal() { return this.price.multiply(this.qty); }
}
Quy tắc:
- 1 transaction sửa duy nhất 1 aggregate (consistency boundary).
- Aggregate khác chỉ tham chiếu qua ID, không tham chiếu trực tiếp.
- Aggregate giữ invariant nội bộ.
7.4. Repository
Tập trừu tượng để load/save aggregate. Ẩn database.
// Domain interface
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
findByCustomer(customerId: CustomerId): Promise<Order[]>;
}
// Infrastructure implementation
class PostgresOrderRepository implements OrderRepository {
constructor(private db: PgClient) {}
async findById(id: OrderId) {
const row = await this.db.query('SELECT * FROM orders WHERE id = $1', [id.value]);
return row ? this.toDomain(row) : null;
}
async save(order: Order) { /* ... */ }
async findByCustomer(customerId: CustomerId) { /* ... */ return []; }
private toDomain(row: any): Order { /* mapping */ return new Order(...); }
}
Repository không phải DAO. DAO trả flat data; Repository trả full aggregate (với behavior).
7.5. Domain Service
Logic domain mà không thuộc về 1 Entity hoặc VO cụ thể. Ví dụ: chuyển tiền giữa 2 account.
class TransferService {
async transfer(from: AccountId, to: AccountId, amount: Money) {
const sourceAcc = await this.repo.findById(from);
const destAcc = await this.repo.findById(to);
if (!sourceAcc || !destAcc) throw new Error('not found');
sourceAcc.withdraw(amount); // entity behavior
destAcc.deposit(amount);
await this.repo.save(sourceAcc);
await this.repo.save(destAcc);
// Lưu ý: 2 aggregate trong 1 transaction → cần xử lý nhất quán
}
}
Domain Service ≠ Application Service. Domain Service vẫn ở domain layer, dùng entity/VO. Application Service orchestrate use case (gọi repository, gửi email).
8. Bounded Context
Khái niệm cốt lõi nhất của DDD chiến lược (strategic). Một Bounded Context là phạm vi mà 1 model có nghĩa nhất quán. Cùng 1 từ "Customer" có thể có 2 model khác nhau ở 2 context:
- Sales context: Customer = (id, name, email, totalSpent, vipStatus, leadScore).
- Shipping context: Customer = (id, addresses, deliveryPreferences, signatureRequired).
2 context KHÔNG share 1 class Customer khổng lồ. Mỗi context có model riêng. Ánh xạ giữa context qua "Context Map" (bằng ID + ACL — anti-corruption layer).
8.1. Microservice = Bounded Context?
Thường đúng. Mỗi microservice nên 1 BC. Nhưng KHÔNG nhất thiết — 1 monolith có thể có nhiều BC tách bằng module/package.
8.2. Ubiquitous Language
Trong 1 BC, dev và domain expert dùng cùng 1 ngôn ngữ. Code dùng đúng từ business: order.cancel(), không order.update(2).
9. Khi nào dùng DDD?
DDD KHÔNG phải cho mọi project.
9.1. Phù hợp khi
- Domain phức tạp, business rule nhiều.
- Project lifetime dài (3+ năm).
- Có domain expert làm việc cùng dev.
- Team đủ kinh nghiệm.
9.2. KHÔNG phù hợp
- CRUD đơn giản (TODO list, blog).
- Prototype, MVP, hackathon.
- Domain chủ yếu là technical (compiler, search engine).
- Team chưa quen với pattern OOP cơ bản.
Bắt đầu nhỏ: dùng Value Object + Rich Domain Model. Khi phức tạp đủ, thêm Aggregate boundaries. Khi team scale, tách Bounded Context.
10. Bài tập
- Tìm 5 code smell trong 1 file mã nguồn của project bạn từng làm. Đề xuất refactoring cụ thể cho từng cái.
- Refactor đoạn code dưới — áp dụng Replace Conditional with Polymorphism + Extract Class:
function calculatePay(employee: any) { if (employee.type === 'hourly') return employee.hours * employee.rate; if (employee.type === 'salary') return employee.salary; if (employee.type === 'commission') return employee.salary + employee.salesAmount * 0.1; } - Thiết kế DDD cho domain "Online Library":
- Identify Entity vs Value Object.
- Identify Aggregate boundaries.
- Define Repository interface.
- Identify possible Bounded Context.
- Convert 1 anemic domain model trong project bạn sang Rich Domain Model. Đo: cùng tính năng tốn bao nhiêu dòng code service trước/sau?
- So sánh: Hexagonal Architecture vs Layered Architecture cổ điển. Khi nào dùng cái nào?
- Liệt kê 5 anti-pattern bạn đã gặp trong code thực tế (của bản thân hoặc đồng nghiệp). Mỗi cái nói cách bạn fix/sẽ fix.
11. Quiz
Quiz cuối Chương 8 (chương cuối)
"Feature Envy" code smell:
Order.isVipDiscount() chủ yếu gọi customer.totalSpent() + customer.yearsActive(), method này "envy" Customer — di chuyển method sang Customer (customer.isVip()) là hợp lý.Replace Conditional with Polymorphism áp dụng khi:
if(type==='card') chargeCard()... bằng method.pay(amount) với strategy. Thêm payment method mới = thêm class, không sửa code cũ."God Object" anti-pattern:
Hexagonal Architecture (Ports & Adapters):
Trong DDD, Aggregate Root là:
Bounded Context giải quyết:
Donald Knuth: "Premature optimization is the root of all evil" có nghĩa:
Khi nào KHÔNG nên áp dụng DDD?
🎉 Hoàn thành toàn bộ 8 chương OOP & Design Patterns!
Bạn đã đi qua: 4 trụ cột OOP → 5 nguyên lý SOLID → Class design mastery → 23 GoF patterns → Anti-patterns + Refactoring + DDD-lite. Đây là vốn từ vựng thiết yếu để bạn nói chuyện thiết kế phần mềm với đồng nghiệp senior. Pattern không phải đích đến — chúng là công cụ để bạn nhận ra vấn đề và lựa chọn giải pháp đúng.