Java OO & Functional/Nhận diện abstraction sai — over-engineering trap và cách sửa
2/38
Bài 2 / 38~13 phútKế thừa & Đa hìnhMiễn phí lượt xem

Nhận diện abstraction sai — over-engineering trap và cách sửa

4 trap over-engineering kinh điển: interface 1 implementation, abstraction không use case, premature abstraction, abstraction giả tạo. Rule of 3, YAGNI, case study payment system minh hoạ abstraction đúng và Open/Closed Principle.

TL;DR: Abstraction mạnh, nhưng abstraction sai tệ hơn không có abstraction — nó khoá design vào dự đoán sai và biến code thành ceremony. Bài này dạy cách nhận diện 4 trap over-engineering: interface chỉ có 1 implementation, abstraction không ai dùng polymorphic, premature abstraction (abstract trước khi đủ thông tin), và abstraction giả tạo (caller vẫn hardcode concrete). Công cụ phòng thủ: YAGNIrule of 3 — chờ 3 case thực tế mới abstract. Phần cuối là case study payment system: abstraction đúng trông như thế nào, và vì sao nó tự nhiên đạt Open/Closed Principle. Kèm 5 pitfall tổng hợp với code sai/đúng đối chiếu.

Bài 01 dạy bạn quy trình 5 bước để tạo abstraction. Bài này dạy kỹ năng ngược lại — và quan trọng không kém: nhận ra khi nào abstraction là thừa, sai, hoặc quá sớm.

Codebase legacy nào cũng có vài chục BaseFooService, AbstractBarHandler, IBazManager mà không một dòng code nào dùng chúng polymorphic. Mỗi abstraction thừa là một file thêm để đọc, một tầng indirection để nhảy qua khi debug — chi phí trả hàng ngày, lợi ích không bao giờ đến. Học cách nhận diện chúng sớm rẻ hơn nhiều so với refactor sau 2 năm.

1. Khi nào KHÔNG trừu tượng — 4 trap over-engineering

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

Trap 1 — Interface 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 thì 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 nên 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.

Bốn trap trên không rời rạc — chúng là bốn câu hỏi phải trả lời theo đúng thứ tự này trước khi gõ interface:

Cây quyết định bốn câu hỏi trước khi thêm abstraction, mọi nhánh rẽ ngang đều dẫn về concrete class

Chỉ một nhánh duy nhất dẫn tới abstraction. Mặc định là concrete class — abstraction mới là thứ phải chứng minh.

2. Case study — design "payment system"

Để thấy abstraction đúng trông như thế nào (đối chiếu với 4 trap trên), xét 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.

Chú ý: ở đây có 5 implementation thực tế ngay từ đầu — vượt rule of 3, abstraction không hề premature.

Abstraction

Điểm chung: charge(amount) trả về 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.

Đối chiếu với 4 trap:

  • Có 5 implementation thật (không phạm trap 1, trap 3).
  • CheckoutService chính là code dùng abstraction polymorphic (không phạm trap 2).
  • Provider được inject qua Map, không new hardcode (không phạm trap 4).

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 thì OCP đến tự nhiên. Abstraction sai khiến mỗi lần thêm feature phải sửa code cũ — hệ thống fragile.

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

4. 📚 Deep Dive

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

Sách:

  • "Effective Java" - Joshua Bloch (Item 18-22) — interface vs abstract class, favor composition, design for inheritance.
  • "Design Patterns" - GoF — 23 pattern đều là case study về abstraction.

Essay:

Ghi chú: 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? Code nào sẽ dùng nó polymorphic?". Nếu lúng túng — xoá abstraction, viết concrete trước, abstract sau khi pattern rõ. IDE có "Extract Interface" — refactor sau chỉ mất vài phút, rẻ hơn nhiều so với sống chung abstraction sai.

5. Tóm tắt

  • Abstraction sai tệ hơn không có abstraction — nó khoá design vào dự đoán sai, thêm indirection vô ích.
  • 4 trap: (1) interface 1 implementation, (2) abstraction không ai dùng polymorphic, (3) premature abstraction, (4) abstraction giả tạo — caller vẫn new concrete.
  • YAGNI: không xây cho nhu cầu tưởng tượng. Rule of 3: 2 case có thể là trùng hợp, 3 case mới là pattern.
  • Test một abstraction: "code nào dùng nó thay vì concrete?" — không trả lời được thì xoá.
  • Abstraction giả tạo fix bằng dependency injection — caller nhận interface qua constructor, không tự new.
  • Case study payment: 5 provider thực tế + caller polymorphic + injection = abstraction đúng, OCP tự nhiên.
  • Refactor concrete thành abstraction khi cần (Extract Interface) rẻ hơn nhiều so với gỡ abstraction sai đã ăn sâu.

6. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao 'premature abstraction' tệ hơn 'không có abstraction'?
  2. Q2
    Interface chỉ có 1 implementation có luôn luôn là trap không? Khi nào nó hợp lý?
  3. Q3
    God abstraction (1 interface chứa mọi method) vi phạm nguyên tắc nào, gây hại gì, và sửa thế nào?
  4. Q4
    Ví dụ abstraction leak là gì, và cách tránh?
  5. Q5
    Trong case study payment, vì sao thêm provider mới không phải sửa CheckoutService? Điều kiện nào của abstraction làm được việc đó?

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

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

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

Bài tiếp theo

`extends` và `super` — kế thừa cơ bản