1. Composition over Inheritance
"Favor object composition over class inheritance." — Gang of Four (1994), Effective Java Item 18 (Bloch)
Inheritance ("is-a") là công cụ mạnh nhưng dễ tạo coupling chặt và hierarchy fragile. Composition ("has-a") — class chứa object khác như field — linh hoạt hơn, dễ thay runtime, không có vấn đề diamond.
1.1. Vấn đề với inheritance — Bird/Penguin
// ❌ Inheritance — hierarchy ép buộc
class Bird {
fly() {} eat() {} layEgg() {}
}
class Eagle extends Bird {} // OK
class Sparrow extends Bird {} // OK
class Penguin extends Bird {
fly() { throw new Error('Penguins do not fly'); } // ❌ vi phạm LSP
}
class Kiwi extends Bird {
fly() { throw new Error('Kiwis do not fly'); } // ❌ tương tự
}
Cây phân loại sinh học không khớp behavioral classification. Mở rộng: Duck — bay, bơi, đi bộ — phải kế thừa từ đâu?
1.2. Composition — gắn behavior
// ✓ Composition — bird có behavior, không "là" behavior
interface FlyBehavior { fly(): void; }
interface SwimBehavior { swim(): void; }
interface QuackBehavior { quack(): void; }
class FlyWithWings implements FlyBehavior { fly() { console.log('flying'); } }
class CannotFly implements FlyBehavior { fly() { /* nothing */ } }
class SwimNormal implements SwimBehavior { swim() { console.log('swimming'); } }
class CannotSwim implements SwimBehavior { swim() { /* nothing */ } }
class Bird {
constructor(
private flyBehavior: FlyBehavior,
private swimBehavior: SwimBehavior
) {}
fly() { this.flyBehavior.fly(); }
swim() { this.swimBehavior.swim(); }
// Có thể đổi behavior runtime:
setFlyBehavior(b: FlyBehavior) { this.flyBehavior = b; }
}
// Tạo bird đa dạng:
const eagle = new Bird(new FlyWithWings(), new CannotSwim());
const penguin = new Bird(new CannotFly(), new SwimNormal());
const duck = new Bird(new FlyWithWings(), new SwimNormal());
// Penguin gãy cánh? Đổi behavior:
penguin.setFlyBehavior(new CannotFly()); // không cần subclass mới
Đây chính là Strategy Pattern (Ch7) — composition là nền cho rất nhiều pattern.
1.3. Khi NÀO inheritance vẫn đúng
- Thực sự "is-a" và hành vi phù hợp (không chỉ tên).
- Subclass mở rộng, không hạn chế hành vi parent.
- Hierarchy không sâu (≤ 2 cấp thường an toàn).
- Parent là abstract/interface, không phải concrete với state.
Ví dụ tốt: UnsupportedOperationException extends RuntimeException extends Exception — tăng dần specificity.
1.4. Quy tắc "kế thừa từ class bạn không kiểm soát"
Tránh extend class third-party. Joshua Bloch: "Inheritance is only safe within a package." Class bạn không kiểm soát có thể đổi behavior parent ở version sau, gãy subclass của bạn. Thay vì extend, dùng composition + delegation (Decorator pattern).
2. Immutability — bất biến
Object immutable không thể thay đổi sau khi tạo. Mọi "thay đổi" trả về object MỚI. Lợi ích: thread-safe miễn phí, không cần defensive copy, dễ reasoning, không bug "ai đó sửa state của tôi".
2.1. Cách tạo immutable class
class Money {
constructor(
public readonly amount: number, // readonly = chỉ gán 1 lần
public readonly currency: string
) {
if (amount < 0) throw new Error('amount must be non-negative');
Object.freeze(this); // runtime ngăn mọi sửa đổi
}
// Mọi "thay đổi" trả Money mới:
add(other: Money): Money {
if (this.currency !== other.currency) throw new Error('currency mismatch');
return new Money(this.amount + other.amount, this.currency);
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency);
}
toString() { return `${this.amount} ${this.currency}`; }
}
const a = new Money(100, 'USD');
const b = a.add(new Money(50, 'USD')); // b mới, a vẫn là 100 USD
console.log(a.toString()); // "100 USD"
console.log(b.toString()); // "150 USD"
2.2. Defensive copy với mutable input
Nếu constructor nhận mutable object (array, Date), copy để bảo vệ:
class Period {
private readonly _start: Date;
private readonly _end: Date;
constructor(start: Date, end: Date) {
// Defensive copy — caller có thể sửa Date của họ về sau
this._start = new Date(start);
this._end = new Date(end);
if (this._start > this._end) throw new Error('invalid range');
}
// Trả copy, không trả tham chiếu nội bộ
get start() { return new Date(this._start); }
get end() { return new Date(this._end); }
}
2.3. Cost của immutability
- Tạo object mới mỗi lần "thay đổi" → memory + GC pressure.
- Hot loop với immutable object có thể chậm.
Trade-off: với value object nhỏ và domain entity, immutability đáng giá. Với buffer/big array, mutable phù hợp hơn.
Persistent data structure (Immutable.js, Immer) giúp "structural sharing" — tạo "phiên bản" mới chỉ copy diff, đỡ overhead.
2.4. readonly không phải immutable
class Foo {
readonly tags: string[] = []; // readonly field, KHÔNG immutable
}
const f = new Foo();
f.tags = []; // ❌ readonly
f.tags.push('a'); // ✓ array bên trong vẫn mutable!
// Sửa: ReadonlyArray + freeze
class Bar {
readonly tags: ReadonlyArray<string>;
constructor(tags: string[]) {
this.tags = Object.freeze([...tags]);
}
}
3. Value vs Reference Semantics
3.1. Phân biệt
Hai object có thể "bằng nhau" theo 2 nghĩa:
| Reference equality | Value equality | |
|---|---|---|
| So sánh | a === b (cùng object trong RAM) | cùng giá trị thuộc tính |
| Phù hợp với | Entity (User, Order — định danh qua ID) | Value Object (Money, Email) |
| Mutable? | Thường có thể | Luôn immutable |
const a = new Money(100, 'USD');
const b = new Money(100, 'USD');
a === b; // false — 2 object khác nhau trong RAM
a.equals(b); // true — cùng giá trị, nếu định nghĩa equals đúng
3.2. Value Object — quy tắc
- Immutable.
- Định danh bởi giá trị thuộc tính (không có ID).
- Có method
equals()+hashCode()dựa trên thuộc tính. - Có
toString()hữu ích. - Tự validate ở constructor.
class Email {
private static REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
constructor(public readonly value: string) {
if (!Email.REGEX.test(value)) throw new Error('invalid email');
}
get domain() { return this.value.split('@')[1]; }
equals(other: Email): boolean {
return this.value.toLowerCase() === other.value.toLowerCase();
}
toString() { return this.value; }
}
// Dùng:
const e1 = new Email('alice@x.com');
const e2 = new Email('ALICE@X.COM');
e1.equals(e2); // true (case-insensitive)
3.3. Primitive Obsession — antipattern
Lưu email, money, address dưới dạng string/number raw. Vấn đề:
- Validate scattered ở mọi nơi dùng.
- Lẫn lộn:
function transfer(from: string, to: string, amount: number)— dễ swap params. - Type system không bảo vệ —
userIdvàorderIdđềustring, hoán đổi nhầm không bị catch.
// ❌ Primitive obsession:
function transfer(fromAccount: string, toAccount: string, amount: number) {}
// ✓ Branded types — Type-safe primitive:
type AccountId = string & { readonly __brand: 'AccountId' };
type UserId = string & { readonly __brand: 'UserId' };
function transfer(from: AccountId, to: AccountId, amount: Money) {}
const userId = 'u_123' as UserId;
const accId = 'a_456' as AccountId;
transfer(userId, accId, ...); // ❌ Compile error — kiểu không khớp
4. equals & hashCode contract
Trong Java, mọi class kế thừa Object.equals() và Object.hashCode(). Override phải tuân contract:
4.1. Contract của equals
- Reflexive:
x.equals(x)= true. - Symmetric:
x.equals(y)⟺y.equals(x). - Transitive:
x.equals(y) && y.equals(z)→x.equals(z). - Consistent: gọi nhiều lần kết quả như nhau (nếu state không đổi).
x.equals(null)= false.
4.2. Contract của hashCode
- Nếu
x.equals(y)= true, BẮT BUỘCx.hashCode() == y.hashCode(). - Ngược lại:
x.hashCode() == y.hashCode()KHÔNG bắt buộcequals.
Vi phạm contract → HashMap, HashSet hoạt động sai (object "biến mất" khỏi set).
4.3. Implement đúng (Java)
public final class Money {
private final long cents;
private final String currency;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money m = (Money) o;
return cents == m.cents && currency.equals(m.currency);
}
@Override
public int hashCode() {
return Objects.hash(cents, currency); // dùng helper
}
}
4.4. JavaScript / TypeScript
JS không có equals tự động — === chỉ là reference. Tự định nghĩa:
class Money {
constructor(public readonly cents: number, public readonly currency: string) {}
equals(other: unknown): boolean {
if (!(other instanceof Money)) return false;
return this.cents === other.cents && this.currency === other.currency;
}
}
// Set/Map dùng reference equality → không tự dùng được:
const set = new Set();
set.add(new Money(100, 'USD'));
set.has(new Money(100, 'USD')); // false — 2 object khác
// Workaround: serialize key
const map = new Map();
map.set('100|USD', new Money(100, 'USD'));
Hoặc dùng thư viện immutable.js với value-equality built-in.
5. Interface design — minimal API surface
Quy tắc Joshua Bloch:
"When in doubt, leave it out. APIs, like organisms, evolve. It's easy to add a method later — impossible to remove."
5.1. Quy tắc
- Public ít nhất có thể — private mặc định, public khi cần thiết.
- Tham số ít — > 4 tham số là code smell, gom thành object.
- Tên method rõ nghĩa —
order.markPaid()rõ hơnorder.update(2). - Một method một việc — không có flag
doSomething(force: boolean)điều khiển 2 hành vi khác nhau. - Tránh primitive obsession — dùng VO thay raw string.
5.2. Boolean parameter — code smell
// ❌ Khó đọc, dễ swap
file.copy('a.txt', 'b.txt', true, false); // true là gì?
// ✓ Tách method
file.copyOverwriting('a.txt', 'b.txt');
file.copyPreservingTimestamp('a.txt', 'b.txt');
// HOẶC named parameter object:
file.copy({ src: 'a.txt', dst: 'b.txt', overwrite: true, preserveTimestamp: false });
5.3. Method chaining (fluent interface)
class QueryBuilder {
private filters: Filter[] = [];
private sortBy: string | null = null;
private limitN = 100;
where(f: Filter): this { this.filters.push(f); return this; }
orderBy(col: string): this { this.sortBy = col; return this; }
limit(n: number): this { this.limitN = n; return this; }
build(): Query { /* ... */ }
}
const q = new QueryBuilder()
.where({ col: 'active', op: '=', val: true })
.orderBy('created_at')
.limit(20)
.build();
6. Dependency Injection
Đã đề cập DIP ở Ch2. Đây ta đi sâu cách thực thi.
6.1. Constructor injection (khuyên dùng)
class OrderService {
constructor(
private orders: OrderRepository,
private mailer: NotificationSender,
private clock: Clock = new SystemClock() // có default
) {}
async checkout(order: Order) { /* ... */ }
}
// Wire ở composition root:
const service = new OrderService(
new PostgresOrderRepo(db),
new SendgridSender(apiKey),
);
Lợi: dependency rõ trong signature, immutable sau khi tạo, dễ test.
6.2. Setter injection
class OrderService {
private mailer?: NotificationSender;
setMailer(m: NotificationSender) { this.mailer = m; }
}
Phù hợp khi dependency thực sự optional. Nhược: object có thể "chưa hoàn chỉnh" khi sử dụng.
6.3. Method injection
class ReportGenerator {
generate(orders: Order[], formatter: Formatter): string {
return orders.map(o => formatter.format(o)).join('\n');
}
}
Phù hợp khi dependency dùng 1 lần cho 1 method, không phải state lâu dài.
6.4. DI Container
Khi app lớn, manual wiring trở nên rườm rà. DI container (NestJS, Spring, .NET DI) tự động wire qua decorator/annotation:
// NestJS style:
@Injectable()
class OrderService {
constructor(
private readonly orders: OrderRepository,
private readonly mailer: NotificationSender,
) {}
}
@Module({
providers: [
OrderService,
{ provide: OrderRepository, useClass: PostgresOrderRepository },
{ provide: NotificationSender, useClass: SendgridSender },
],
})
class AppModule {}
6.5. Khi DI quá tay
- App nhỏ < 10 service: manual wiring đủ rõ.
- "Inject" stateless utility (Math, Date.now) — thường overkill.
- Tạo interface chỉ để có thể inject, dù chỉ 1 implementation: trừ khi cho test, là wasted.
7. Law of Demeter — "Don't talk to strangers"
Method M của class C chỉ nên gọi method của:
- Chính C.
- Object được tạo trong M.
- Tham số truyền vào M.
- Field của C.
Không nên gọi method của object trả về từ method khác (chuỗi a.b().c().d()).
// ❌ Vi phạm — "train wreck"
const country = order.getCustomer().getAddress().getCountry().getCode();
// Vi phạm vì:
// 1. order biết Customer (OK — field)
// 2. order biết về Address (CHỈ qua Customer — không nên)
// 3. order biết về Country (càng tệ)
// Nếu Customer đổi cấu trúc, order code phải sửa.
// ✓ Encapsulate trên Customer:
class Customer {
private address: Address;
getCountryCode(): string { return this.address.getCountryCode(); }
}
class Address {
private country: Country;
getCountryCode(): string { return this.country.code; }
}
const country = order.getCustomer().getCountryCode();
// HOẶC tốt hơn:
const country = order.getCustomerCountryCode();
7.1. Khi vi phạm OK
Fluent builder, jQuery, query builder — chuỗi method là chính API:
const q = qb.where(...).orderBy(...).limit(20); // không vi phạm Demeter — đây là DSL
Demeter cảnh báo về chuỗi navigation qua đối tượng khác, không phải fluent API trên cùng builder.
7.2. Tinh thần
Demeter giúp giảm coupling. Class A biết quá nhiều về cấu trúc của Class B → A vỡ khi B đổi. Encapsulate "navigation path" vào method công khai.
8. Anemic vs Rich Domain Model
8.1. Anemic — "OOP thiếu máu"
// ❌ Class chỉ chứa data, getter/setter
class Order {
id: string = '';
items: LineItem[] = [];
status: string = 'pending';
total: number = 0;
// chỉ getter/setter
}
// Logic ở "service" bên ngoài
class OrderService {
addItem(order: Order, item: LineItem) {
order.items.push(item);
order.total += item.price * item.qty;
}
cancel(order: Order) {
if (order.status === 'shipped') throw new Error('cannot cancel shipped');
order.status = 'cancelled';
}
}
Vấn đề:
- Logic scatter ở service bên ngoài.
- Có thể bypass:
order.status = 'cancelled'trực tiếp. - Encapsulation = 0. Mọi field public hoặc có setter.
- Nhiều service operate trên cùng entity → khó trace.
8.2. Rich Domain Model
// ✓ Class có behavior, bảo vệ invariant
class Order {
private constructor(
public readonly id: OrderId,
private _items: LineItem[],
private _status: OrderStatus
) {}
static create(id: OrderId): Order {
return new Order(id, [], 'pending');
}
// Method bảo vệ invariant
addItem(item: LineItem): void {
if (this._status !== 'pending') {
throw new Error('Cannot add to non-pending order');
}
this._items = [...this._items, item];
}
cancel(): void {
if (this._status === 'shipped') {
throw new Error('Cannot cancel shipped order');
}
this._status = 'cancelled';
}
total(): Money {
return this._items
.map(i => i.subtotal())
.reduce((s, m) => s.add(m), Money.zero());
}
get status() { return this._status; }
get items(): ReadonlyArray<LineItem> { return this._items; }
}
// Service mỏng — chỉ điều phối:
class OrderService {
constructor(private orders: OrderRepository) {}
async cancelOrder(id: OrderId) {
const order = await this.orders.findById(id);
if (!order) throw new NotFound();
order.cancel(); // logic trong Order, không trong service
await this.orders.save(order);
}
}
Lợi:
- Invariant đảm bảo — không thể bypass.
- Logic tập trung — đọc Order là hiểu Order làm gì.
- Service mỏng, dễ refactor.
8.3. Khi anemic OK
- DTO (Data Transfer Object) — class chỉ để serialize/deserialize, không có business logic.
- Read model trong CQRS — chỉ projection cho hiển thị.
- Form / view model — chỉ binding với UI.
Nhưng đừng nhầm DTO với Domain Entity — chúng là 2 khái niệm khác nhau.
9. Naming & Cohesion
9.1. Class names — danh từ cụ thể
- ✓
OrderRepository,EmailValidator,PaymentProcessor - ✗
Manager,Helper,Utility— quá generic, code smell - ✗
OrderManager— class này thực sự làm gì? Quá rộng. Tên cụ thể hơn:OrderRefundProcessor.
9.2. Method names — verb cụ thể
- ✓
order.cancel(),user.activate(),cart.checkout() - ✗
process(),handle(),execute()— vague - ✗
doIt(),run()— vague
9.3. Cohesion — class "đồng nhất"
Class có cohesion cao = mọi method dùng cùng các field. Cohesion thấp → nhiều "subgroup" method, dấu hiệu nên tách class.
// ❌ Cohesion thấp — 2 nhóm method độc lập
class UserStuff {
// Group 1: dùng email, name
validateEmail() { /* dùng this.email */ }
formatDisplayName() { /* dùng this.name */ }
// Group 2: dùng password, lastLogin
hashPassword() { /* dùng this.password */ }
recordLogin() { /* dùng this.lastLogin */ }
}
// → Tách thành Profile và Credential
10. Bài tập
- Refactor đoạn code sau từ inheritance sang composition. Bird, Penguin, Duck. Cho phép mỗi loài có hành vi fly/swim/quack tổ hợp tự do.
- Tạo Value Object
EmailAddressimmutable: validate format, equals case-insensitive, toString. Implement đầy đủ. - Tạo Value Object
Moneyvớiadd,subtract,multiply,equals, throw error khi cộng/trừ tiền tệ khác nhau. - Branded type cho TypeScript: tạo
UserIdvàOrderIdbranded types. Demo type safety: function nhận UserId không nhận OrderId. - Refactor Anemic sang Rich:
class Cart { items: any[] = []; total: number = 0; } class CartService { add(cart, item) { cart.items.push(item); cart.total += item.price; } remove(cart, idx) { cart.total -= cart.items[idx].price; cart.items.splice(idx, 1); } checkout(cart) { if (cart.items.length === 0) throw; } } - Đoạn code vi phạm Law of Demeter. Tìm và sửa:
if (order.getCustomer().getMembership().getTier() === 'GOLD') { applyDiscount(); } - Khi nào constructor injection vs setter injection? Cho 2 ví dụ thực tế của mỗi loại.
11. Quiz
Quiz cuối Chương 3
"Composition over Inheritance" có nghĩa là:
Value Object KHÔNG có đặc điểm:
Contract của equals:
Anemic Domain Model:
order.status='shipped' bypass mọi rule. Rich Model: logic trong class, service mỏng.Law of Demeter cảnh báo về:
Constructor Injection so với Setter Injection:
Primitive Obsession là khi:
readonly field array trong TypeScript:
readonly tags: string[] = không thể gán tags = [], nhưng tags.push('x') vẫn được. Để immutable thực sự: readonly tags: ReadonlyArray<string> + freeze ở constructor.Hoàn thành Chương 3. Tiếp theo: Chương 4 — Creational Patterns (5 mẫu) →