Java — Từ Zero đến Senior/Tư duy trừu tượng hoá — trụ cột quan trọng nhất của OOP
~25 phútKế thừa & Đa hìnhMiễn phí

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á. Cách xác định abstraction từ bài toán, 3 level of abstraction, khi nào trừu tượng - khi nào không. Ứng dụng trong Java qua interface, abstract class, polymorphism.

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 1 chỗ → phải fix 5 chỗ. Thêm loại mới → copy lần 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ể.
  • 3 level of abstraction và khi chọn mức nào.
  • Khi nào trừu tượng — và khi nào không (over-engineering trap).
  • Java cung cấp gì để hiện thực abstraction.

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 → cả 3 cột còn lạ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

// 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 → 8× 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 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 → interface có thể chứa 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. Khi nào KHÔNG trừu tượng — Over-engineering trap

Abstraction mạnh, nhưng lạm dụng tệ hơn không dùng. Dấu hiệu over-engineering:

Trap 1 — Abstract class cho 1 implementation

interface UserRepository {
    User findById(long id);
}

class UserRepositoryImpl implements UserRepository {
    User findById(long id) { ... }
}

Interface UserRepositorychỉ 1 implementation UserRepositoryImpl. Abstraction thừa — chỉ tăng 1 file, không thêm giá trị.

Trừ khi: có test double (mock) — nhưng modern testing dùng Mockito mock class trực tiếp, không cần interface.

Rule: YAGNI (You Ain't Gonna Need It). Chỉ abstract khi có ≥2 impl thực tế hoặc chắc chắn cần thay trong tương lai gần.

Trap 2 — Abstraction không có use case

Code legacy đầy BaseFooService, AbstractBarHandler, IBazManager mà không có code thực sự polymorphic. Abstract được thêm "cho đúng pattern" — giờ là dead weight.

Rule: mỗi abstraction phải trả lời được "code nào dùng abstraction này thay vì concrete?". Không trả lời được → xoá abstraction.

Trap 3 — Abstraction quá sớm (premature abstraction)

// Nghi "mot ngay nao do se co cac loai notification"
interface Notifier {
    void send(Notification n);
}

class EmailNotifier implements Notifier {
    void send(Notification n) { ... }
}

Hiện tại chỉ có email. SMS, push notification — tương lai có thể. Viết abstraction ngay — chi phí thêm class, nhưng chưa dùng polymorphism.

Tương lai khi thêm SMS, requirements có thể khác — abstraction hiện tại không phù hợp. Phải sửa abstraction + email impl + thêm SMS impl. Tốn hơn bắt đầu concrete + refactor khi thực sự cần.

Rule: rule of 3. Khi có 3 implementation (không phải 2) thường mới đủ rõ ràng để abstract.

Trap 4 — Abstraction không thay đổi được

Code dùng new EmailNotifier() trực tiếp trong 50 chỗ. "Abstraction" chỉ có trong class, nhưng caller hardcode → không thể thay. Abstraction giả tạo — tên interface nhưng coupling cụ thể.

Fix: dependency injection (Spring @Autowired, constructor inject). Caller nhận Notifier, không new trực tiếp.

6. Java cung cấp gì để hiện thực abstraction

4 công cụ chính (chi tiết ở bài 4, 5, 6 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.
  • Java 8+ có default method (logic trong interface).
  • Class có thể implement nhiều interface.
  • Không 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".

sealed class / sealed interface (Java 17+)

  • Giới hạn danh sách con được phép extend/implement.
  • Làm abstraction thành closed hierarchy — compiler biết hết subtype.
  • Enable pattern matching exhaustive (switch expression cover hết case).

Dùng cho: abstraction có số lượng con xác định (ADT — Algebraic Data Type style). Vd sealed interface Shape permits Circle, Square, Triangle.

record (Java 14+)

  • Immutable data class, concise syntax.
  • Không phải abstraction trực tiếp, nhưng hỗ trợ design với value object — common trong abstraction thiết kế functional.

Generic <T>

  • Abstraction trên kiểu — class/method tổng quát over types.
  • List<T>, Map<K, V>, Optional<T> — abstraction không tough-couple với type cụ thể.

Module 8 bài 1-3 đi sâu generic.

7. Case study — design "payment system"

Bài tập thực tế: design thanh toán với nhiều provider.

Phân tích

Thực thể cụ thể:

  • Credit card Stripe
  • Credit card PayPal
  • E-wallet MoMo
  • Bank transfer Vietcombank
  • Crypto payment Bitcoin

Use case: user chọn 1 phương thức → charge số tiền → nhận kết quả success/fail.

Abstraction

Điểm chung: charge(amount) → result. Đó là core abstraction.

interface PaymentProvider {
    PaymentResult charge(BigDecimal amount, PaymentContext context);
    RefundResult refund(String transactionId);
    String providerName();
}

record PaymentResult(boolean success, String transactionId, String errorCode) {}

Mỗi provider implement riêng:

class StripeProvider implements PaymentProvider {
    public PaymentResult charge(BigDecimal amount, PaymentContext ctx) {
        // HTTP call Stripe API
    }
}

class MoMoProvider implements PaymentProvider { ... }
class CryptoProvider implements PaymentProvider { ... }

Code caller — không biết provider cụ thể

class CheckoutService {
    private final Map<String, PaymentProvider> providers;   // injected

    public void pay(String providerId, BigDecimal amount) {
        PaymentProvider p = providers.get(providerId);
        PaymentResult r = p.charge(amount, buildContext());
        if (r.success()) markOrderPaid(r.transactionId());
        else handleFailure(r.errorCode());
    }
}

Thêm provider Stripe, MoMo, Bitcoin — CheckoutService không đổi 1 dòng. Đây là power of abstraction.

Open/Closed Principle

Pattern trên là ví dụ của OCP (Open for extension, Closed for modification — SOLID):

  • Open for extension: thêm PaymentProvider mới bằng class mới.
  • Closed for modification: CheckoutService không sửa khi thêm provider.

Abstraction đúng → OCP tự nhiên. Abstraction sai → mỗi lần thêm feature phải sửa code cũ → fragile.

8. Pitfall tổng hợp

Nhầm 1: Abstraction vô nghĩa cho 1 implementation.

interface UserRepository { User findById(long id); }
class UserRepositoryImpl implements UserRepository { ... }   // Chi co 1 impl

✅ Concrete class trước. Abstract khi có nhu cầu thực sự.

Nhầm 2: Abstraction quá sớm.

// Nghi "tuong lai se co nhieu loai"
interface Foo { ... }
class FooV1 implements Foo { ... }

✅ Rule of 3 — chờ đến khi có ≥2-3 impl thực tế.

Nhầm 3: God abstraction — 1 interface chứa mọi method.

interface Document {
    void print();
    void scan();
    void fax();
    void email();
}
// Impl "BasicPrinter" phai throw UnsupportedOperationException cho scan/fax

✅ Interface Segregation: tách Printable, Scannable, Faxable, Emailable. Class implement đúng interface.

Nhầm 4: Abstraction leak — method lộ chi tiết impl.

interface Cache {
    Map<String, Object> getInternalMap();   // Lo Map - caller depend implementation
}

✅ Method chỉ expose behavior: get(key), put(key, value). Đổi từ Map sang Redis không break API.

Nhầm 5: Abstraction không test được.

class Service {
    EmailSender sender = new EmailSender();   // hardcoded
}

✅ Dependency injection: Service(EmailSender sender). Test inject mock.

9. 📚 Deep Dive

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

Sách:

  • "Effective Java" - Joshua Bloch (Item 18-22) — interface vs abstract class, favor composition, design for inheritance.
  • "Clean Code" - Robert C. Martin Ch.6 Objects and Data Structures, Ch.10 Classes — abstraction principles.
  • "Design Patterns" - GoF — 23 pattern đều là case study về abstraction.
  • "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?". Nếu lung túng — xoá abstraction, viết concrete trước, abstract sau khi rõ.

10. 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.
  • Over-engineering trap: abstract cho 1 impl, abstraction không use case, premature abstraction, god abstraction. Rule of 3 — chờ có 3 impl thực sự mới abstract.
  • Java tools: abstract class, interface, sealed class/interface (Java 17+), generic <T>.
  • Abstraction đúng → Open/Closed Principle tự nhiên, codebase mở rộng được.

11. 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 → áp dụng sai:

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

Thiết kế OOP tốt bắt đầu bằng abstraction tốt. Abstraction sai → 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
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.

Q3
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.

Q4
Vì sao 'premature abstraction' tệ hơn 'không có abstraction'?

Premature abstraction = abstract trước khi có đủ thông tin.

Hệ quả:

  1. Abstraction dựa trên đoán: chỉ có 1 impl hiện tại. Abstract dựa trên "có thể trong tương lai cần X". Khi thực sự thêm impl 2, requirements thường khác với dự đoán → abstraction không phù hợp → refactor.
  2. Chi phí upfront: thêm class/file, indirection, boilerplate. Không có benefit ngay.
  3. Coupling ngược: abstraction sai lock design → khó thay đổi. Concrete code dễ refactor hơn abstraction sai.
  4. Fake abstraction: 1 interface + 1 impl là "code ceremony" không có giá trị. Reader đọc 2 file thay 1, không thêm semantic.

Không abstract: khi cần thêm impl thứ 2, refactor concrete class thành abstraction. Nhiều IDE có tool "Extract Interface" — 2 phút xong. Lần này abstraction dựa trên 2 case thực tế, chính xác hơn.

Rule of 3 (Martin Fowler): không abstract cho đến khi có 3 case tương tự. 2 case có thể coincidence, 3 case là pattern.

Q5
Ví dụ abstraction leak là gì, và cách tránh?

Abstraction leak = abstraction (interface/class) expose detail implementation bên dưới — caller coupled vào impl cụ thể.

Ví dụ tệ:

interface Cache {
  HashMap<String, Object> getInternalMap();   // Lo HashMap
}

Caller dùng getInternalMap() → phụ thuộc HashMap. Đổi sang Redis/Caffeine → phải thay tất cả caller.

Ví dụ tốt:

interface Cache {
  Optional<Object> get(String key);
  void put(String key, Object value);
  void evict(String key);
}

Chỉ expose behavior. Đổi HashMap → ConcurrentHashMap → Redis → caller không biết, không cần sửa.

Cách tránh:

  • Method trả interface hoặc simple type, không trả implementation cụ thể.
  • Không expose internal collection — dùng Collections.unmodifiableList hoặc copy defensive.
  • Không method có tên kiểu getInternalX, getRawY — signal leak intent.
  • Test abstraction: code 2 impl khác hẳn nhau (vd in-memory + Redis). Nếu test pass cả 2, abstraction clean. Nếu fail — đâu đó leak.

Joshua Bloch Item 15: "Minimize the accessibility of classes and members" — áp dụng cho abstraction: expose ít nhất có thể để tương tác.

Bài tiếp theo: extends và super — kế thừa từ class cha

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