Tư duy trừu tượng hoá — trụ cột quan trọng nhất của OOP
Abstraction là gì, 4 trụ cột OOP và vai trò trừu tượng hoá. Quy trình 5 bước xác định abstraction từ bài toán, 3 level of abstraction, và bộ công cụ Java (interface, abstract class, sealed, record) để hiện thực chúng.
TL;DR: Abstraction là quá trình bỏ chi tiết không quan trọng, giữ điểm chung quan trọng giữa nhiều thực thể, để xử lý chúng đồng nhất — và là trụ cột duy nhất của OOP thuộc về tư duy thay vì cú pháp. Bài này đưa quy trình 5 bước xác định abstraction từ bài toán (liệt kê thực thể, tìm điểm chung theo ngữ cảnh, loại detail, verify bằng use case, evolve theo requirements), 3 level trừu tượng (concrete class, abstract class, interface) và bộ công cụ Java tương ứng. Insight quan trọng nhất: abstraction phụ thuộc ngữ cảnh — cùng một đối tượng có nhiều abstraction đúng, tuỳ use case bạn đang phục vụ.
Bạn làm app quản lý thư viện. Yêu cầu ban đầu: cho mượn sách. Đơn giản — viết class Book với title, author, isbn, method borrow() / return(). 50 dòng code, xong.
Tuần sau, sếp yêu cầu thêm: cho mượn tạp chí (có issue, year), DVD (duration, region), audiobook (duration, narrator), ebook (fileSize, format). Mỗi loại có rule riêng — sách hạn mượn 14 ngày, DVD 7 ngày, ebook unlimited.
Làm thế nào?
Cách 1 — mỗi loại 1 class + copy logic borrow/return cho từng cái:
class Book { borrow() { ... 50 dong ... } }
class Magazine { borrow() { ... 50 dong copy ... } }
class DVD { borrow() { ... 50 dong copy ... } }
class Audiobook { ... }
class Ebook { ... }
5 class × 50 dòng copy = 250 dòng duplicate logic. Fix bug một chỗ thì phải sửa lặp lại ở cả 5 chỗ. Thêm loại mới lại copy lần thứ 6.
Cách 2 — nhìn ra điểm chung, trừu tượng hoá:
Tất cả 5 loại đều là "thứ có thể cho mượn". Chung: có identifier, có người đang mượn, có ngày mượn, có ngày trả, có rule "được mượn bao lâu". Khác: loại cụ thể, rule chi tiết.
abstract class LibraryItem {
String id;
User borrower;
LocalDate borrowedAt;
abstract int borrowDurationDays(); // Moi con override
void borrow(User u) {
// Logic chung: check available, set borrower, set date
}
void returnItem() { ... }
}
class Book extends LibraryItem {
int borrowDurationDays() { return 14; }
}
class DVD extends LibraryItem {
int borrowDurationDays() { return 7; }
}
class Ebook extends LibraryItem {
int borrowDurationDays() { return Integer.MAX_VALUE; }
}
50 dòng logic chung viết 1 lần trong LibraryItem. Mỗi con chỉ tả điểm khác biệt (borrowDurationDays). Thêm loại mới: 1 class ngắn extends + override.
Đây là abstraction — kỹ năng cốt lõi của OOP, và của software engineering nói chung.
Bài này không dạy syntax — syntax (abstract class, interface) ở các bài kế. Bài này dạy cách nghĩ:
- 4 trụ cột OOP — abstraction đóng vai gì.
- Abstraction là gì (không phải "giấu chi tiết" như Wikipedia nói).
- Cách xác định abstraction từ bài toán — process cụ thể 5 bước.
- 3 level of abstraction và khi chọn mức nào.
- Java cung cấp gì để hiện thực abstraction.
Còn câu hỏi ngược lại — khi nào KHÔNG nên trừu tượng, và cách nhận diện abstraction sai — là chủ đề của bài 02 ngay sau bài này.
1. 4 trụ cột OOP — bức tranh tổng
OOP (Object-Oriented Programming) đứng trên 4 trụ cột:
| Trụ cột | Ý nghĩa | Java hiện thực |
|---|---|---|
| Encapsulation | Giấu chi tiết implementation, expose API | private/public, getter/setter |
| Inheritance | Class con kế thừa class cha | extends |
| Polymorphism | Một interface, nhiều implementation | Method override, dynamic dispatch |
| Abstraction | Tư duy tổng quát hoá, giữ "cái gì" bỏ "làm thế nào" | abstract class, interface |
3 cột đầu là cơ chế (mechanism) — Java cung cấp keyword cụ thể. Cột 4 — Abstraction — là tư duy (thinking) — không có keyword, nhưng xuyên suốt mọi thiết kế tốt.
Quan hệ giữa chúng:
- Abstraction là gốc — quyết định "mô hình gì".
- Encapsulation là kỹ thuật che detail theo abstraction đã chọn.
- Inheritance là phương tiện share code giữa các class cùng abstraction.
- Polymorphism là khả năng thay implementation dựa trên abstraction.
Thiết kế OOP tốt bắt đầu bằng abstraction tốt. Abstraction sai thì cả 3 cột còn lại phải gắng gượng bù đắp — code càng ngày càng phức tạp.
2. Abstraction là gì
Định nghĩa thường gặp — chưa đủ
Sách giáo khoa: "Abstraction là giấu chi tiết implementation, chỉ expose interface".
Định nghĩa này đúng nhưng thiếu 70%. Nó mô tả kết quả, không mô tả quá trình và ý nghĩa cốt lõi.
Định nghĩa đầy đủ
Abstraction = quá trình bỏ đi chi tiết không quan trọng cho ngữ cảnh, giữ lại điểm chung quan trọng giữa nhiều thứ, để làm việc với chúng đồng nhất.
3 phần quan trọng:
- Bỏ đi chi tiết: không mô tả mọi thứ — chọn lọc.
- Giữ điểm chung: tìm common denominator giữa nhiều case cụ thể.
- Đồng nhất: sau abstraction, code xử lý các case thông qua một interface chung.
Ví dụ đời thường
Cùng một chiếc ô tô, ba hệ thống khác nhau cần ba tập thuộc tính khác nhau — và thứ bị bỏ ở hệ thống này lại là trung tâm ở hệ thống kia:

Ở trạm thu phí, abstraction là Vehicle(type, weight, plateNumber) — động cơ và màu sơn irrelevant. Ở gara thì chính mã máy mới là thứ không bỏ được: RepairableVehicle(model, engineCode, lastService). Còn với cảnh sát giao thông là IdentifiableVehicle(plate, owner, insuranceExpiry).
3 abstraction khác nhau cho cùng 1 đối tượng, cả ba đều đúng. Abstraction phụ thuộc ngữ cảnh — chọn theo use case hiện tại, không theo bản thân đối tượng.
Tầm quan trọng: giảm complexity
Não người xử lý được ~7 khái niệm cùng lúc (Miller's magic number). Nếu mỗi API làm việc với Sedan, SUV, Truck, Motorbike, Bicycle, Bus, Van... dev phải nhớ 7 class, mỗi class có quirks riêng. Thay bằng abstraction Vehicle, giờ chỉ 1 concept.
Code:
// Khong abstract
void chargeToll(Sedan s) { ... }
void chargeToll(SUV s) { ... }
void chargeToll(Truck t) { ... }
// ... 5 more overloads
Trừu tượng hoá lại, toàn bộ overload gom về một method duy nhất:
// Abstract
void chargeToll(Vehicle v) {
int fee = v.type().baseFee() + v.weight() * 1000;
...
}
API caller không cần biết xe cụ thể. Thêm loại xe mới: implement Vehicle, chargeToll chạy không sửa.
3. Cách xác định abstraction từ bài toán
Đây là kỹ năng khó nhất — không có công thức cứng. Nhưng có quy trình định hướng:
Bước 1 — liệt kê các thực thể cụ thể
Ví dụ app quản lý tài chính cá nhân. Thực thể:
- Ví tiền mặt
- Thẻ tín dụng Visa
- Thẻ tín dụng Mastercard
- Tài khoản ngân hàng Techcombank
- Tài khoản ngân hàng Vietcombank
- Ví MoMo
- Ví ZaloPay
- Bitcoin wallet
8 thực thể. Nếu viết 8 class riêng, bạn nhân 8 lần boilerplate.
Bước 2 — tìm điểm chung theo ngữ cảnh
Ngữ cảnh: app xem số dư, chuyển tiền, xem lịch sử. Điểm chung:
- Tất cả đều có số dư (balance).
- Tất cả đều chuyển tiền đi (debit) hoặc nhận tiền (credit).
- Tất cả đều có lịch sử giao dịch (transactions).
Đó là Account — abstraction đúng cho ngữ cảnh này.
interface Account {
BigDecimal balance();
void debit(BigDecimal amount);
void credit(BigDecimal amount);
List<Transaction> transactions();
}
Bước 3 — loại bỏ detail không thuộc ngữ cảnh
Trong abstraction Account, bỏ:
- Loại thẻ Visa/Mastercard — app không quan tâm network.
- Ngân hàng Techcombank vs Vietcombank — cùng là "bank account".
- Bitcoin address — app coin wallet như account thông thường.
Detail này thuộc về implementation của class con:
class BankAccount implements Account {
String bankName;
String accountNumber;
// logic cu the cho bank
}
class CryptoWallet implements Account {
String walletAddress;
String blockchain;
// logic cu the cho crypto
}
class EWallet implements Account {
String provider; // "MoMo", "ZaloPay"
// logic cu the cho e-wallet
}
Bước 4 — verify bằng use case
Test abstraction có đúng không: viết code dùng nó không biết implementation cụ thể.
public BigDecimal totalBalance(List<Account> accounts) {
return accounts.stream()
.map(Account::balance)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
Method này chạy với bất kỳ combination nào: ví + thẻ + crypto + e-wallet — abstraction đã cover hết.
Nếu phải viết if (account instanceof BankAccount) ... else if (...), abstraction chưa đủ hoặc sai hướng.
Bước 5 — chấp nhận sửa abstraction khi yêu cầu thay đổi
Requirements đổi: thêm feature "chuyển khoản quốc tế" chỉ áp dụng bank account. Abstraction Account không có method này.
Không nên thêm internationalTransfer() vào Account chung — không phải tất cả đều có. Giải pháp:
- Tạo sub-abstraction:
interface InternationalTransferable extends Account. BankAccount implements Account, InternationalTransferable.- Code muốn international transfer cast / check instanceof.
Đây gọi là Interface Segregation Principle (ISP) — SOLID bài sau. Abstraction không phải "viết 1 lần cho mãi mãi" — evolve với requirements.
4. 3 level of abstraction
Abstraction có nhiều tầng, từ thấp (gần concrete) đến cao (trừu tượng nặng):
Level 1 — Concrete class (không abstract)
class ArrayList<E> {
Object[] data;
int size;
void add(E e) { ... }
}
Impl cụ thể, có state và logic đầy đủ. Instantiate trực tiếp.
Level 2 — Abstract class
abstract class AbstractList<E> implements List<E> {
// Logic chung cho moi List: equals, hashCode, iterator
public boolean equals(Object o) { ... }
// Method abstract - con bat buoc implement
public abstract E get(int index);
public abstract int size();
}
Có một phần logic, một phần để lại cho con. ArrayList, LinkedList, Vector đều extends AbstractList để share logic common.
Dùng khi: có điểm chung code (không chỉ contract) giữa các class con.
Level 3 — Interface (pure contract)
interface List<E> {
void add(E e);
E get(int index);
int size();
// ... khong co logic, chi contract
}
Chỉ là hợp đồng — khai báo method, không implementation (trước Java 8 — hiện có default method nhưng tinh thần giữ là contract-focused).
Dùng khi: nhiều implementation không có chung code, chỉ chung semantic.
Chọn level nào
Câu hỏi chốt là có logic chung giữa các con hay không: không có thì interface (pure contract); có thì abstract class để viết logic đó một lần — trừ khi ô extends đã bị dùng cho việc khác, khi đó quay về interface với default method hoặc composition.
Quy tắc thực tế:
- Interface trước. Nếu impl class thực sự share logic, refactor sang abstract class sau.
- Abstract class khi có 30-70% code chung không viết 2 lần.
- Concrete class khi không cần abstraction (chỉ 1 loại, không dự kiến mở rộng).
Java 8+ có default method trong interface, nên interface cũng chứa được logic. Ranh giới abstract class vs interface mờ hơn. Nhưng abstract class vẫn có ưu thế: field instance, constructor, protected helper.
5. Java cung cấp gì để hiện thực abstraction
Bộ công cụ xếp theo đúng ba mức ở phần trên, từ trừu tượng thuần tuý xuống cụ thể: interface (chỉ mô tả "can-do", không state, implement được nhiều cái) → abstract class (mô tả "is-a", giữ được state và logic chung, chỉ một cha) → concrete class. Dùng abstract class khi abstraction có state chung và template method cho con customize; dùng interface cho capability-based abstraction (Runnable, Comparable, Iterable — "thứ nào chạy được", "thứ nào so sánh được", "thứ nào duyệt được"). Ranh giới thiết kế giữa chúng đi sâu ở bài 05, 06 và 07 của module này.
Các công cụ nâng cao (Java 16+)
sealed class / sealed interface(Java 17+): Giới hạn danh sách con được phép thừa kế nhằm tạo ra các phân cấp trừu tượng đóng kín (Closed Hierarchy). Chúng ta sẽ tìm hiểu chi tiết ở bài 07.record(final ở Java 16): Lớp dữ liệu bất biến (Immutable), giúp thiết kế nhanh các cấu trúc dữ liệu phẳng hay Value Object trong lập trình hướng đối tượng.- Generic
<T>: Abstraction trên kiểu — class/method tổng quát over types (sẽ học kỹ ở Module 3).
6. 📚 Deep Dive
Sách:
- "Clean Code" - Robert C. Martin Ch.6 Objects and Data Structures, Ch.10 Classes — abstraction principles.
- "Domain-Driven Design" - Eric Evans — abstraction ở level business domain.
Paper / essay:
- "On the Criteria To Be Used in Decomposing Systems into Modules" - Parnas (1972) — foundational paper về abstraction.
- "The Power of Abstraction" - Barbara Liskov — Turing Award lecture.
Ghi chú: Abstraction là skill phát triển qua năm — không học từ 1 bài. Mỗi code review, mỗi refactor là cơ hội rèn. Rule: khi viết class mới, tự hỏi "abstraction tôi đang tạo là gì? Ngữ cảnh nào? Có ≥2 implementation không?".
7. Tóm tắt
- 4 trụ cột OOP: Encapsulation, Inheritance, Polymorphism, Abstraction. Abstraction là tư duy, 3 cột kia là cơ chế.
- Abstraction = bỏ chi tiết không quan trọng, giữ điểm chung quan trọng, xử lý đồng nhất.
- Abstraction phụ thuộc ngữ cảnh — cùng đối tượng có nhiều abstraction khác nhau tuỳ use case.
- Quy trình xác định abstraction: (1) liệt kê thực thể, (2) tìm điểm chung theo ngữ cảnh, (3) loại detail, (4) verify bằng use case, (5) evolve khi requirements đổi.
- 3 level: concrete class → abstract class (logic chung) → interface (pure contract). Dùng interface trước, refactor abstract class khi thực sự chia sẻ code.
- Java tools:
abstract class,interface, generic<T>,record(Java 16) và phân cấp đóng kínsealed(Java 17). - Abstraction đúng là tiền đề cho mọi thiết kế mở rộng được — bài 02 sẽ chỉ cách nhận diện abstraction sai.
8. Tự kiểm tra
- Q1Vì sao abstraction là trụ cột quan trọng nhất trong 4 trụ cột OOP?
- Q2Vì sao nói abstraction 'phụ thuộc ngữ cảnh'? Cho ví dụ một đối tượng có 2 abstraction khác nhau đều đúng.
- Q3Cho yêu cầu: app quản lý media của 1 studio gồm movie, podcast, short-form video (TikTok style). Abstraction nào phù hợp nếu mục đích app là 'upload, gán metadata, publish'?
- Q4Khi nào nên dùng interface, khi nào nên dùng abstract class?
- Q5Bước 'verify bằng use case' (bước 4) kiểm tra điều gì, và dấu hiệu nào cho thấy abstraction chưa đúng?
Bài tiếp theo: Nhận diện abstraction sai — over-engineering trap và cách sửa
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên