Java OO & Functional/Tư duy trừu tượng hoá — trụ cột quan trọng nhất của OOP
1/38
Bài 1 / 38~14 phútKế thừa & Đa hìnhMiễn phí lượt xem

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ĩaJava hiện thực
EncapsulationGiấu chi tiết implementation, expose APIprivate/public, getter/setter
InheritanceClass con kế thừa class chaextends
PolymorphismMột interface, nhiều implementationMethod override, dynamic dispatch
AbstractionTư 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ý 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

Đưa ra các đối tượng: sedan, suv, xe tải, xe máy, xe đạp. Abstraction để xử lý "đi qua trạm thu phí":

  • Chi tiết không quan trọng: động cơ, số cửa, màu sơn, biển số.
  • Điểm chung quan trọng: loại (ô tô/xe máy), trọng lượng, có biển số hay không.
  • Abstraction: Vehicle với field type, weight, plateNumber.

Chỉ những field/method này được dùng ở trạm thu phí. Động cơ, màu sơn — irrelevant.

Abstraction phụ thuộc ngữ cảnh. Cùng chiếc xe, ở:

  • Trạm thu phí: abstraction Vehicle(type, weight, plate).
  • Gara: abstraction RepairableVehicle(model, engineCode, lastService).
  • Công an giao thông: IdentifiableVehicle(plate, owner, insuranceExpiry).

3 abstraction khác nhau cho cùng 1 đối tượng. Chọn theo use case hiện tại.

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();
}

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

flowchart TD
    A[Co abstraction can thiet khong?] --> B{Co logic chung giua cac con?}
    B -->|Khong| C[Interface - pure contract]
    B -->|Co| D{Single inheritance co han che khong?}
    D -->|Khong| E[Abstract class - share logic]
    D -->|Co| F[Interface voi default method<br/>hoac 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

Java cung cấp các công cụ ngôn ngữ đa dạng để biểu diễn các cấp độ trừu tượng khác nhau, từ trừu tượng thuần túy đến triển khai cụ thể:

graph TD
    Abstract[Truu tuong - High Abstraction]
    Concrete[Cu the - Low Abstraction]

    Interface["Interface (Pure Contract)<br/>Chi mo ta 'Can-do' (hop dong hanh vi)"]
    AbstractClass["Abstract Class (Template)<br/>Mo ta 'Is-a' & giu trang thai (State)"]
    ConcreteClass["Concrete Class (Implementation)<br/>Doi tuong cu the trong thuc te"]

    Interface --> AbstractClass
    AbstractClass --> ConcreteClass

    Abstract -.-> Interface
    Abstract -.-> AbstractClass
    ConcreteClass -.-> Concrete

Chi tiết về cách sử dụng và ranh giới thiết kế giữa chúng sẽ được đi sâu ở bài 05, 06 và 07 của module này:

abstract class

  • Class không instantiate được trực tiếp.
  • Có method abstract (không body) + method concrete (có body).
  • Class con extends phải implement abstract method.
  • Single inheritance (1 parent).

Dùng cho: abstraction có state chung (field) + logic chung + template method cho con customize.

interface

  • Pure contract (Hợp đồng thuần túy).
  • Java 8+ có default method (cho phép có logic trong interface).
  • Class có thể implement nhiều interface.
  • Không có state (field = public static final).

Dùng 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".

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

📚 Deep Dive — sách và paper nên đọc

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:

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ín sealed (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

Tự kiểm tra
Q1
Vì sao abstraction là trụ cột quan trọng nhất trong 4 trụ cột OOP?

3 trụ cột khác (encapsulation, inheritance, polymorphism) là cơ chế — Java cung cấp keyword để implement. Abstraction là tư duy — quyết định mô hình gì, biên abstraction ở đâu.

Cơ chế không có tư duy dẫn tới áp dụng sai:

  • Encapsulation với field sai vẫn leak implementation.
  • Inheritance với hierarchy sai sinh ra fragile base class.
  • Polymorphism với abstraction sai khiến method override không có ý nghĩa.

Thiết kế OOP tốt bắt đầu bằng abstraction tốt. Abstraction sai khiến cả 3 cột còn lại gắng gượng bù đắp — code ngày càng phức tạp, refactor khó.

Trong thực tế, senior dev phân biệt với junior chính ở kỹ năng abstraction — không phải syntax. Học syntax 1 tuần, học abstraction 5-10 năm.

Q2
Vì 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.

Abstraction là kết quả của việc chọn lọc chi tiết theo use case — đổi use case thì tập "chi tiết quan trọng" đổi theo, nên abstraction cũng đổi. Không có abstraction "đúng tuyệt đối" cho một đối tượng; chỉ có abstraction đúng cho một ngữ cảnh.

Ví dụ cùng một chiếc ô tô:

  • Ở trạm thu phí: Vehicle(type, weight, plateNumber) — phí tính theo loại và trọng lượng, không ai quan tâm engine.
  • Ở gara sửa chữa: RepairableVehicle(model, engineCode, lastService) — engine và lịch sử bảo dưỡng là trung tâm, trọng lượng irrelevant.

Hệ quả thực hành: khi thiết kế interface, đừng hỏi "đối tượng này CÓ gì" mà hỏi "ngữ cảnh này CẦN gì". Model mọi thứ một đối tượng có vào một abstraction là dấu hiệu god abstraction (bài 02 phân tích kỹ).

Q3
Cho 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'?

Bước 1 — liệt kê thực thể: movie, podcast, short video. Cả 3 đều khác nhau về content type.

Bước 2 — tìm điểm chung theo ngữ cảnh "upload + metadata + publish":

  • Đều có file binary (bytes).
  • Đều có title, description, tags, author.
  • Đều có status (draft → published).
  • Đều cần action: upload, update metadata, publish.

Bước 3 — abstraction:

interface MediaItem {
  String id();
  String title();
  Instant createdAt();
  MediaStatus status();

  void upload(InputStream file);
  void updateMetadata(Metadata m);
  void publish();
}

class Movie implements MediaItem { int duration; String director; ... }
class Podcast implements MediaItem { int duration; List<Episode> episodes; ... }
class ShortVideo implements MediaItem { int duration; String creator; ... }

Bước 4 — verify use case:

void publishAll(List<MediaItem> items) {
  items.forEach(MediaItem::publish);
}

Chạy đồng nhất cho movie/podcast/video — abstraction đúng.

Detail cụ thể (director, episodes, creator) là implementation của class con, không thuộc abstraction. Abstraction chỉ giữ điểm chung cho ngữ cảnh.

Q4
Khi nào nên dùng interface, khi nào nên dùng abstract class?

Interface:

  • Không có logic chung, chỉ contract.
  • Nhiều impl có thể không liên quan họ hàng (vd Comparable — String, Integer, Date đều implement, không cùng hierarchy).
  • Muốn class implement nhiều abstraction (Java single inheritance, multiple interface).
  • Capability-based: "thứ nào có thể X" — Runnable, Iterable, Serializable.

Abstract class:

  • Có logic chung (30-70%) giữa các con — viết 1 lần trong parent.
  • Cần field instance (interface không có).
  • Cần constructor với logic setup.
  • Template method pattern — parent define skeleton, con fill blanks.

Thực tế hybrid: khai báo abstraction bằng interface, có abstract class helper cho common code. Ví dụ List (interface) + AbstractList (abstract class share code) + ArrayList / LinkedList (concrete).

Rule: interface trước. Nếu phát hiện share code đáng kể, thêm abstract class helper.

Q5
Bướ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ước này kiểm tra abstraction có thực sự đồng nhất hoá được các thực thể không: viết một đoạn code dùng abstraction mà không biết implementation cụ thể nào đứng sau (vd totalBalance(List<Account>) chạy với mọi tổ hợp ví, thẻ, crypto).

Dấu hiệu abstraction chưa đúng:

  • Code dùng abstraction phải instanceof / cast xuống class cụ thể để xử lý case riêng — nghĩa là điểm chung bạn chọn chưa đủ cho ngữ cảnh.
  • Một số implementation phải ném UnsupportedOperationException cho method không áp dụng — nghĩa là abstraction chứa method KHÔNG chung (god abstraction).
  • Caller cần biết "đang là loại nào" để gọi đúng thứ tự / tránh method cấm — hợp đồng không tự mô tả đủ.

Khi gặp các dấu hiệu này: hoặc thu hẹp abstraction (bỏ method không chung, tách sub-interface theo ISP), hoặc thừa nhận các thực thể không nên đứng chung một abstraction.

Bài tiếp theo: Nhận diện abstraction sai — over-engineering trap và cách sửa

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt 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