Chương 01 · Foundations

OOP Fundamentals & 4 Pillars

Từ procedural đến object-oriented. Class, object, và bốn trụ cột Encapsulation / Abstraction / Inheritance / Polymorphism — định nghĩa chính xác, ví dụ TypeScript.

1. Vì sao cần OOP?

Hãy tưởng tượng bạn viết app banking theo kiểu procedural:

// procedural style — data và function tách rời
type Account = { id: number; balance: number; ownerId: number };

const accounts: Account[] = [];

function createAccount(ownerId: number): Account {
  const acc = { id: Date.now(), balance: 0, ownerId };
  accounts.push(acc);
  return acc;
}

function deposit(id: number, amount: number) {
  const acc = accounts.find(a => a.id === id);
  if (!acc) throw new Error('not found');
  if (amount < 0) throw new Error('invalid');
  acc.balance += amount;
}

function withdraw(id: number, amount: number) {
  const acc = accounts.find(a => a.id === id);
  if (!acc) throw new Error('not found');
  if (amount > acc.balance) throw new Error('insufficient');
  acc.balance -= amount;
}

App nhỏ chạy được. Vấn đề khi quy mô tăng:

  • Data và function rời rạc — ai đó có thể acc.balance = -1000 trực tiếp, bypass mọi check.
  • Logic bị scatter — quy tắc validate "amount > 0" copy ở 2 chỗ. Thêm 1 check mới phải tìm khắp codebase.
  • Khó test — function đụng global array.
  • Không có "ngữ nghĩa nghiệp vụ" — code thiếu khái niệm "account đang đóng vai trò gì" — chỉ là object plain.

Cùng vấn đề viết theo OOP:

class Account {
  private balance: number = 0;

  constructor(
    public readonly id: number,
    public readonly ownerId: number
  ) {}

  deposit(amount: number): void {
    this.assertPositive(amount);
    this.balance += amount;
  }

  withdraw(amount: number): void {
    this.assertPositive(amount);
    if (amount > this.balance) throw new Error('insufficient');
    this.balance -= amount;
  }

  getBalance(): number { return this.balance; }

  private assertPositive(amount: number): void {
    if (amount <= 0) throw new Error('amount must be positive');
  }
}

const acc = new Account(1, 100);
acc.deposit(500);
acc.balance = -1000;  // ❌ Compile error: balance is private

Lợi ích:

  • Data và behavior gắn liền — mọi thao tác lên balance đi qua method, không thể bypass.
  • Logic tập trung — quy tắc "amount > 0" ở 1 chỗ duy nhất.
  • Encapsulationbalance private, không ai sửa trực tiếp.
  • Code phản ánh nghiệp vụ — đọc account.deposit() tự nhiên hơn deposit(account.id).
OOP không phải "chỉ" OOP là 1 paradigm trong nhiều paradigm. FP (Functional Programming), procedural, logic programming đều có chỗ riêng. OOP đặc biệt mạnh khi state và behavior đi cùng nhau, và domain có nhiều "thứ" (entity) tương tác.

2. Class & Object — chính xác là gì?

2.1. Class

Classkhuôn mẫu mô tả cấu trúc và hành vi của một loại object. Chứa:

  • Field (thuộc tính / member variable / property) — data.
  • Method — function gắn với class.
  • Constructor — hàm tạo, khởi tạo object mới.
  • Static member — thuộc về class, không thuộc instance.
class User {
  // field
  private id: string;
  public name: string;
  public email: string;

  // static field
  static defaultRole = 'user';

  // constructor
  constructor(name: string, email: string) {
    this.id = crypto.randomUUID();
    this.name = name;
    this.email = email;
  }

  // instance method
  greet(): string {
    return `Hello, I'm ${this.name}`;
  }

  // static method
  static fromEmail(email: string): User {
    return new User(email.split('@')[0], email);
  }
}

2.2. Object (Instance)

Object = instance của class. Mỗi new ClassName() tạo 1 object mới với state riêng.

const alice = new User('Alice', 'alice@x.com');   // object 1
const bob   = new User('Bob',   'bob@x.com');     // object 2

alice.greet();   // "Hello, I'm Alice"
bob.greet();     // "Hello, I'm Bob"

User.fromEmail('carol@x.com');  // gọi static method, không cần instance

2.3. this — tham chiếu instance hiện tại

Trong method, this trỏ đến object đang gọi method. Lỗi phổ biến của JS dev: this mất context khi pass method như callback.

class Counter {
  count = 0;
  increment() { this.count++; }
}

const c = new Counter();
const inc = c.increment;
inc();   // ❌ TypeError: Cannot read 'count' of undefined

// Fix 1: bind
const inc2 = c.increment.bind(c);

// Fix 2: arrow function (lexical this)
class Counter2 {
  count = 0;
  increment = () => this.count++;   // arrow auto-bind
}

2.4. Constructor patterns

// TypeScript shorthand: parameter properties
class Point {
  constructor(
    public readonly x: number,
    public readonly y: number
  ) {}
  // Tương đương: declare 2 field + this.x = x; this.y = y;
}

// Multiple constructors qua static factory:
class Date {
  private constructor(public timestamp: number) {}

  static now(): Date { return new Date(Date.now()); }
  static fromString(s: string): Date { return new Date(Date.parse(s)); }
  static epoch(): Date { return new Date(0); }
}

3. Encapsulation — đóng gói

Encapsulation = gói dữ liệu và hành vi trong cùng 1 đơn vị (class) + ẩn chi tiết bên trong. Người dùng class chỉ tương tác qua public interface, không truy cập state trực tiếp.

3.1. Modifier (TypeScript)

ModifierTruy cập từ
public (default)Mọi nơi
protectedChính class + subclass
privateChỉ chính class
readonlyChỉ đọc, gán 1 lần ở constructor
class BankAccount {
  private balance: number = 0;
  protected owner: string;
  public readonly accountNumber: string;

  constructor(owner: string) {
    this.owner = owner;
    this.accountNumber = generateAccountNumber();
  }

  // Public method là "cửa" duy nhất tương tác với balance
  deposit(amount: number) {
    if (amount <= 0) throw new Error('invalid');
    this.balance += amount;
    this.logTransaction('deposit', amount);   // private helper
  }

  getBalance() { return this.balance; }

  private logTransaction(type: string, amount: number) {
    console.log(`[${this.accountNumber}] ${type}: ${amount}`);
  }
}

const acc = new BankAccount('Alice');
acc.deposit(100);          // ✓
acc.balance = 1_000_000;   // ❌ Compile error
acc.logTransaction(...);   // ❌ private

3.2. JavaScript private (#)

ECMAScript chính thức hỗ trợ hard private qua prefix # (Node 12+, modern browser):

class Account {
  #balance = 0;   // hard private — không truy cập được kể cả với reflection

  deposit(amount) { this.#balance += amount; }
  get balance() { return this.#balance; }
}

const a = new Account();
a.deposit(100);
console.log(a.balance);     // 100
console.log(a.#balance);    // SyntaxError

Khác với TypeScript private (chỉ check compile-time, runtime vẫn truy cập được), #hard private — không thể bypass.

3.3. Getter & Setter

class Temperature {
  private celsius: number;

  constructor(celsius: number) { this.celsius = celsius; }

  get fahrenheit(): number {
    return this.celsius * 9 / 5 + 32;
  }

  set fahrenheit(f: number) {
    this.celsius = (f - 32) * 5 / 9;
  }

  get c(): number { return this.celsius; }
}

const t = new Temperature(20);
console.log(t.fahrenheit);   // 68 — gọi như property, thực ra là method
t.fahrenheit = 100;
console.log(t.c);            // 37.77...

Getter/setter giúp chuyển từ field sang computed mà không phá API public.

3.4. Encapsulation ≠ chỉ là private

Encapsulation thực sự là về che giấu lý do thay đổi. Class không nên expose field mà chỉ vô tình "đóng gói" bằng private. Nếu mọi field đều có getter/setter trùng tên (anemic class), bạn không có encapsulation thực sự — chỉ có "named struct".

Anemic Domain Model — anti-pattern Class chỉ chứa data + getter/setter, mọi logic ở "service" bên ngoài. Trông hướng đối tượng nhưng thực ra là procedural đội lốt OOP. Sẽ học cách tránh ở Ch3.

4. Abstraction — trừu tượng hóa

Abstraction = tách cái gì object làm khỏi cách nó làm. Người dùng class chỉ thấy interface, không cần biết implementation.

4.1. Abstract class

abstract class Shape {
  abstract area(): number;          // không có body
  abstract perimeter(): number;

  // Method concrete dùng abstract method
  describe(): string {
    return `Shape with area ${this.area()}, perimeter ${this.perimeter()}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) { super(); }
  area() { return Math.PI * this.radius ** 2; }
  perimeter() { return 2 * Math.PI * this.radius; }
}

class Rectangle extends Shape {
  constructor(private w: number, private h: number) { super(); }
  area() { return this.w * this.h; }
  perimeter() { return 2 * (this.w + this.h); }
}

const shapes: Shape[] = [new Circle(5), new Rectangle(3, 4)];
shapes.forEach(s => console.log(s.describe()));

new Shape();   // ❌ Cannot create instance of abstract class

4.2. Interface

Interface = contract chỉ có signature, không có implementation. Class implement interface phải cung cấp đủ method.

interface Repository<T> {
  save(entity: T): Promise<void>;
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  delete(id: string): Promise<void>;
}

class UserRepository implements Repository<User> {
  async save(user: User) { /* ... */ }
  async findById(id: string) { return null; }
  async findAll() { return []; }
  async delete(id: string) { /* ... */ }
}

// Code dùng chỉ phụ thuộc vào interface, không phụ thuộc vào implementation:
async function backup(repo: Repository<User>) {
  const all = await repo.findAll();
  // ...
}

4.3. Abstract class vs Interface

Abstract classInterface
Có implementationCó thể (method concrete)Không (TypeScript)
Có fieldKhông (chỉ shape)
Multiple inheritanceKhông (TS/Java)Có (implement nhiều interface)
Khi nào dùngCó shared behavior, "is-a" mạnhChỉ định contract, "can-do"

4.4. Mức trừu tượng phù hợp

Quy tắc: abstraction nên ở mức nghiệp vụ, không ở mức kỹ thuật. Đừng tạo IFooManagerService chỉ để có interface — chỉ tạo khi có ≥ 2 implementation thực tế hoặc cần cho test.

5. Inheritance — kế thừa

Class B extends A → B có mọi field/method của A + có thể thêm/override. Mối quan hệ "is-a": Dog is-an Animal.

class Animal {
  constructor(public name: string) {}

  speak(): string { return 'Some sound'; }

  describe(): string { return `${this.name} says ${this.speak()}`; }
}

class Dog extends Animal {
  speak() { return 'Woof!'; }   // override
}

class Cat extends Animal {
  speak() { return 'Meow!'; }
}

const animals: Animal[] = [new Dog('Rex'), new Cat('Whiskers')];
animals.forEach(a => console.log(a.describe()));
// Rex says Woof!
// Whiskers says Meow!

5.1. super

Gọi method của parent từ subclass:

class Logger {
  log(msg: string) { console.log(`[LOG] ${msg}`); }
}

class TimestampLogger extends Logger {
  log(msg: string) {
    super.log(`${new Date().toISOString()} ${msg}`);   // gọi parent
  }
}

Constructor subclass phải gọi super(...) trước khi dùng this:

class Vehicle {
  constructor(public wheels: number) {}
}

class Car extends Vehicle {
  constructor(public brand: string) {
    super(4);   // ❗ phải gọi trước this
    // this.brand = brand;   // implicit do parameter property
  }
}

5.2. Vấn đề kế thừa đa cấp — "Fragile Base Class"

Cây kế thừa sâu (3+ cấp) tạo phụ thuộc khó debug. Sửa class A → tự nhiên B, C, D, E... vỡ. Đây là "fragile base class problem".

Animal ├── Mammal │ ├── Dog │ │ └── Poodle │ │ └── ToyPoodle ← 5 cấp, sửa Animal → ảnh hưởng tận đây │ └── Cat └── Bird

5.3. final / sealed

Đa số ngôn ngữ cho phép đánh dấu class không cho extend:

// Java:
public final class String { ... }   // không ai extend được

// Kotlin: class đóng kế thừa MẶC ĐỊNH (phải đánh dấu `open`)
class Foo { ... }     // không extend được
open class Bar { ... }  // extend được

TypeScript chưa có final chính thức, nhưng có thể workaround qua private constructor + static factory.

Quy tắc "Composition over Inheritance" — nguyên tắc Effective Java Item 18. Khi nghi ngờ, dùng composition (chứa object khác) thay vì kế thừa. Sẽ phân tích kỹ ở Ch3.

6. Polymorphism — đa hình

Cùng 1 method, nhiều type khác nhau cho hành vi khác nhau. Code gọi không cần biết type cụ thể. 3 loại chính:

6.1. Subtype Polymorphism (overriding)

Đã thấy ở 5.0. Method speak() khác nhau theo subtype.

6.2. Parametric Polymorphism (Generics)

Cùng code, nhiều type:

class Stack<T> {
  private items: T[] = [];
  push(x: T) { this.items.push(x); }
  pop(): T | undefined { return this.items.pop(); }
  peek(): T | undefined { return this.items[this.items.length - 1]; }
}

const numStack = new Stack<number>();
const strStack = new Stack<string>();
numStack.push(1);
strStack.push('hello');

// Generic function:
function identity<T>(x: T): T { return x; }
identity(5);        // T = number
identity('hello');  // T = string

6.3. Ad-hoc Polymorphism (Overloading)

Cùng tên, nhiều signature:

// TypeScript overload (chỉ ở type level, runtime là 1 function):
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any { return a + b; }

add(1, 2);          // number
add('a', 'b');      // string
add(1, 'b');        // ❌ no matching overload

// Java thật sự overload (compiler chọn dựa trên type):
class Calculator {
  int add(int a, int b)       { return a + b; }
  double add(double a, double b) { return a + b; }
  String add(String a, String b) { return a + b; }
}

6.4. Polymorphism qua Duck Typing (Python, JS)

"If it walks like a duck and quacks like a duck — it's a duck." Không cần inheritance:

def make_speak(thing):
    return thing.speak()

class Dog:
    def speak(self): return 'Woof'

class Robot:
    def speak(self): return 'Beep'

make_speak(Dog())     # 'Woof'
make_speak(Robot())   # 'Beep' — Robot không kế thừa gì, chỉ cần có method speak

TypeScript có "structural typing" tương tự — type tương thích nếu shape khớp, không cần explicit implements.

6.5. Sức mạnh thực sự của polymorphism

Code "open for extension, closed for modification" — thêm type mới không cần sửa code cũ:

// ❌ Procedural — sửa khi thêm shape:
function area(shape: any): number {
  if (shape.type === 'circle') return Math.PI * shape.r ** 2;
  if (shape.type === 'rect') return shape.w * shape.h;
  if (shape.type === 'triangle') return shape.b * shape.h / 2;
  // Thêm shape mới → phải sửa hàm này
}

// ✓ Polymorphic — thêm shape mới chỉ cần thêm class:
abstract class Shape { abstract area(): number; }
class Circle    extends Shape { constructor(public r: number) { super(); } area() { return Math.PI * this.r ** 2; } }
class Rect      extends Shape { constructor(public w: number, public h: number) { super(); } area() { return this.w * this.h; } }
class Triangle  extends Shape { constructor(public b: number, public h: number) { super(); } area() { return this.b * this.h / 2; } }
// Thêm Pentagon? Chỉ cần thêm class mới — không sửa code cũ.

Đây là tinh thần Open/Closed Principle (sẽ học Ch2).

7. Static vs Dynamic Dispatch

Câu hỏi: khi gọi obj.method(), compiler/runtime biết hay quyết định method nào chạy?

7.1. Static Dispatch (early binding)

Compiler quyết định ở compile-time. Nhanh, không có overhead. C++ non-virtual function, Java static method.

7.2. Dynamic Dispatch (late binding)

Runtime quyết định dựa trên actual type của object. Cần "vtable" — bảng pointer đến method.

abstract class Animal { abstract speak(): string; }
class Dog extends Animal { speak() { return 'Woof'; } }
class Cat extends Animal { speak() { return 'Meow'; } }

function trigger(a: Animal) {
  return a.speak();   // ← runtime: actual type Dog hay Cat?
}

trigger(new Dog());   // 'Woof'
trigger(new Cat());   // 'Meow'

Ở line a.speak(), compiler chỉ biết aAnimal. Phương thức nào chạy phụ thuộc runtime type — đây là dynamic dispatch.

7.3. Cost của dynamic dispatch

  • 1 indirection qua vtable lookup → ~1-2 ns/call. Hầu như không đáng kể.
  • Compiler khó inline. Trong hot loop, có thể thấy.
  • Modern JIT (V8, HotSpot) làm inline caching tối ưu.

99% trường hợp: đừng lo overhead. Code rõ > tối ưu sớm.

8. Class-based vs Prototype-based

Hai cách triển khai OOP khác nhau:

Class-based

Class là bản thiết kế. Object là instance.

  • Java, C++, Python, C#
  • Khái niệm rõ: class ≠ object
  • Inheritance qua extends

Prototype-based

Object kế thừa trực tiếp từ object khác (prototype).

  • JavaScript (gốc), Lua, Self
  • Mỗi object có __proto__
  • Inheritance qua prototype chain

8.1. JavaScript — prototype dưới capo

// "Class" trong JS thực ra là syntactic sugar cho prototype:
class Animal {
  constructor(name) { this.name = name; }
  speak() { return 'Sound'; }
}

// Tương đương:
function Animal(name) { this.name = name; }
Animal.prototype.speak = function() { return 'Sound'; };

// Khi gọi obj.speak():
// 1. JS tìm 'speak' trên obj — không có
// 2. Tìm trên obj.__proto__ (= Animal.prototype) — thấy → gọi
// 3. Nếu vẫn không có, tiếp tục lên Object.prototype

8.2. Hệ quả của prototype

  • Có thể thêm method vào prototype sau khi tạo object — và mọi instance "tự nhiên" có method đó.
  • Không có "private" thực sự ở JS cũ (cho đến ECMAScript 2022 với #).
  • this rebind theo cách gọi — gây nhiều confusion với người từ Java.

Class syntax JS che giấu prototype, nhưng hiểu prototype giúp debug khi gặp bug khó.

9. OOP vs FP vs Procedural — không loại trừ

ParadigmTinh thầnNgôn ngữ tiêu biểu
ProceduralFunction + sequence + stateC, Pascal
OOPObject = data + behavior, message-passingSmalltalk, Java, C++
FunctionalFunction thuần, immutable, no side-effectHaskell, Elixir, Clojure
Multi-paradigmKết hợp nhiềuTypeScript, Scala, Kotlin, Rust, Python

9.1. Modern code: hybrid

Code hiện đại ít khi "thuần" 1 paradigm:

  • Class cho domain entities (Order, User).
  • Pure function cho business rule (calculate tax, validate).
  • Immutable Value Object thay mutable struct.
  • map/filter/reduce thay for loop.
// Hybrid TypeScript:
class Order {
  constructor(
    public readonly id: string,
    public readonly items: ReadonlyArray<LineItem>,   // immutable
    public readonly customer: Customer
  ) {}

  // Pure method — không mutate, trả Order mới:
  addItem(item: LineItem): Order {
    return new Order(this.id, [...this.items, item], this.customer);
  }

  // Tính tổng = pure function (no side-effect):
  total(): Money {
    return this.items
      .map(i => i.subtotal())
      .reduce((sum, m) => sum.add(m), Money.zero());
  }
}

Class + immutability + pure method = best of both worlds. Sẽ chi tiết Ch3.

10. Bài tập

  1. Tạo class Rectangle với width, height private. Public method area(), perimeter(), getter cho dimension. Throw error nếu width/height ≤ 0.
  2. Refactor đoạn code sau từ procedural sang OOP:
    type Cart = { items: Array<{name: string; price: number; qty: number}> };
    function addItem(cart: Cart, item) { cart.items.push(item); }
    function total(cart: Cart) { return cart.items.reduce((s, i) => s + i.price * i.qty, 0); }
  3. Tạo class Shape abstract + 4 subclass Circle, Square, Rectangle, Triangle. Function totalArea(shapes: Shape[]) tính tổng area. Demo polymorphism.
  4. Tạo generic class Pair<A, B> với 2 field, method swap() trả Pair<B, A>.
  5. Giải thích sự khác biệt: private (TypeScript) vs #field (JavaScript). Khi nào nên dùng cái nào?
  6. Tại sao kế thừa đa cấp (5+ level) lại nguy hiểm? Cho ví dụ "fragile base class".

11. Quiz

Quiz cuối Chương 1

Encapsulation chủ yếu là về:

  • Đặt mọi field thành private
  • Có nhiều getter/setter
  • Gói data + behavior trong cùng đơn vị, ẩn chi tiết bên trong, chỉ lộ public interface
  • Inheritance
Encapsulation = đóng gói (data + behavior) + ẩn (information hiding). private chỉ là phương tiện. Class với mọi field private nhưng có getter/setter trùng tên (anemic) không có encapsulation thực.

Polymorphism qua overriding (subtype) cho phép:

  • 1 class có nhiều constructor
  • Cùng method call hoạt động khác nhau với type khác nhau, code gọi không cần biết type cụ thể
  • Generic type
  • Multiple inheritance
Subtype polymorphism: code gọi animal.speak() không quan tâm là Dog hay Cat. Runtime tự dispatch đến method đúng. Đây là sức mạnh chính của OOP cho mở rộng (Open/Closed Principle).

Abstract class khác Interface ở:

  • Không có gì khác
  • Interface chậm hơn
  • Abstract class không có method
  • Abstract class CÓ THỂ chứa implementation/state, interface chỉ là contract; class implement nhiều interface nhưng chỉ extend 1 abstract class
Abstract class: có thể có concrete method, field, constructor. Interface: chỉ shape. TypeScript/Java single inheritance + multiple interfaces — pattern phổ biến. Khi có shared behavior thực sự → abstract class; khi chỉ contract → interface.

Composition over Inheritance là quy tắc:

  • Khi nghi ngờ, dùng "has-a" (chứa object khác) thay vì "is-a" (kế thừa)
  • Không bao giờ dùng inheritance
  • Mọi class phải có 1 parent
  • Composition tốc độ nhanh hơn
Inheritance tạo tight coupling và fragile base class. Composition (chứa object khác như field) linh hoạt hơn — có thể đổi behavior runtime, dễ test, không có vấn đề diamond. Effective Java Item 18.

Trong obj.method(), dynamic dispatch nghĩa là:

  • Method được tải từ network
  • Compile-time chọn method
  • Runtime quyết định method nào chạy dựa trên actual type của obj
  • Method bị inline
Late binding/dynamic dispatch là nền cho subtype polymorphism. Ở line gọi method, compiler chỉ biết static type. Runtime nhìn vào actual type (qua vtable) để chọn đúng implementation. Cost thường < 1ns, không đáng kể.

JavaScript dùng:

  • Class-based OOP nguyên thủy
  • Prototype-based — class syntax chỉ là sugar; mọi object kế thừa qua prototype chain
  • Không hỗ trợ OOP
  • Interface bắt buộc
JS gốc prototype-based — object kế thừa trực tiếp từ object khác qua __proto__. ES2015 thêm class keyword nhưng dưới capo vẫn là prototype. Hiểu prototype giúp debug bug khó (this context, Object.create, etc.).

Anemic Domain Model là:

  • Domain quá nhỏ
  • Domain dùng quá nhiều RAM
  • Domain không có UI
  • Class chỉ chứa data + getter/setter, mọi logic ở "service" bên ngoài → procedural đội lốt OOP
Anti-pattern: Order class chỉ có id, items, total (field) + setItems/setTotal. Logic "thêm item, tính total" nằm ở OrderService. Mất hết encapsulation, business rule scatter, khó maintain. Thay bằng "rich domain model" với behavior trong class.

Sức mạnh chính của polymorphism trong design:

  • Thêm type mới mà không cần sửa code cũ — Open/Closed Principle
  • Tốc độ nhanh hơn
  • Tiết kiệm RAM
  • Compile nhanh hơn
Polymorphism = thay if/else với type sang dispatch tự động. Thêm Pentagon vào Shape hierarchy chỉ cần thêm class mới, không sửa hàm computeAreas. Đây là cách OOP scale codebase phức tạp.

Hoàn thành Chương 1. Tiếp theo: Chương 2 — SOLID Principles →