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 = -1000trự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.
- Encapsulation —
balanceprivate, không ai sửa trực tiếp. - Code phản ánh nghiệp vụ — đọc
account.deposit()tự nhiên hơndeposit(account.id).
2. Class & Object — chính xác là gì?
2.1. Class
Class là khuô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)
| Modifier | Truy cập từ |
|---|---|
public (default) | Mọi nơi |
protected | Chính class + subclass |
private | Chỉ chính class |
readonly | Chỉ đọ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), # là 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".
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 class | Interface | |
|---|---|---|
| Có implementation | Có thể (method concrete) | Không (TypeScript) |
| Có field | Có | Không (chỉ shape) |
| Multiple inheritance | Không (TS/Java) | Có (implement nhiều interface) |
| Khi nào dùng | Có shared behavior, "is-a" mạnh | Chỉ đị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".
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.
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 a là Animal. 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
#). thisrebind 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ừ
| Paradigm | Tinh thần | Ngôn ngữ tiêu biểu |
|---|---|---|
| Procedural | Function + sequence + state | C, Pascal |
| OOP | Object = data + behavior, message-passing | Smalltalk, Java, C++ |
| Functional | Function thuần, immutable, no side-effect | Haskell, Elixir, Clojure |
| Multi-paradigm | Kết hợp nhiều | TypeScript, 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/reducethay 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
- Tạo class
Rectanglevớiwidth,heightprivate. Public methodarea(),perimeter(), getter cho dimension. Throw error nếu width/height ≤ 0. - 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); } - Tạo class
Shapeabstract + 4 subclassCircle,Square,Rectangle,Triangle. FunctiontotalArea(shapes: Shape[])tính tổng area. Demo polymorphism. - Tạo generic class
Pair<A, B>với 2 field, methodswap()trảPair<B, A>. - 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? - 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ề:
Polymorphism qua overriding (subtype) cho phép:
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 ở:
Composition over Inheritance là quy tắc:
Trong obj.method(), dynamic dispatch nghĩa là:
JavaScript dùng:
Anemic Domain Model là:
Sức mạnh chính của polymorphism trong design:
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 →