Skip to content

Java OOP Foundation: Tư Duy Hướng Đối Tượng Cho Junior Developer

By Nhân Nguyễn on Apr 28, 2026

Java OOP Foundation: Tư Duy Hướng Đối Tượng Cho Junior Developer

Chào bạn. Nếu bạn đang là một Java developer với 0–2 năm kinh nghiệm, hoặc một senior đang muốn dạy lại junior của mình, thì bài viết này là dành cho bạn. Ở đây, chúng ta không bàn về syntax – thứ mà bất kỳ cuốn sách Java cơ bản nào cũng có. Chúng ta sẽ đi sâu vào tư duy hướng đối tượng. Đích đến là: khi nhìn vào một đoạn code, bạn không chỉ thấy code chạy đúng output, mà còn thấy được đâu là một thiết kế đẹp, dễ bảo trì, và đâu là “bom nổ chậm” được ngụy trang dưới lớp áo class.

“Mọi thứ Spring, JPA, Hibernate sẽ chỉ là ‘thuộc lòng API’ nếu bạn không có nền tảng OOP vững chắc.”

Chào bạn. Nếu bạn đang là một Java developer với 0–2 năm kinh nghiệm, hoặc một senior đang muốn dạy lại junior của mình, thì bài viết này là dành cho bạn. Ở đây, chúng ta không bàn về syntax – thứ mà bất kỳ cuốn sách Java cơ bản nào cũng có. Chúng ta sẽ đi sâu vào tư duy hướng đối tượng. Đích đến là: khi nhìn vào một đoạn code, bạn không chỉ thấy code chạy đúng output, mà còn thấy được đâu là một thiết kế đẹp, dễ bảo trì, và đâu là “bom nổ chậm” được ngụy trang dưới lớp áo class.

Trong suốt hơn một thập kỷ review code, tôi nhận thấy 90% bug khó tìm của junior bắt nguồn từ việc không thấm OOP: class chỉ là túi đựng method, field public để tiện truy cập, kế thừa chỉ để “xài ké code”, và những service class với hàng chục static method – thứ mà tôi gọi là procedural code đội lốt OOP. Hậu quả là state bị băm nát, bug không lần ra nguồn gốc, và mỗi lần sửa code là một lần cầu nguyện.

Bài viết này sẽ dẫn dắt bạn qua một lộ trình thực chiến: từ việc nhận diện “code đội lốt” đến làm chủ 4 trụ cột OOP, thấm nhuần các nguyên lý SOLID và tránh những anti-pattern kinh điển. Tất cả được minh họa bằng những ví dụ thực tế mà chính bạn có thể gặp trong các dự án Java, đặc biệt là trong lĩnh vực banking/fintech.


1. Bức Tranh Thực Tế: Khi “Code Chạy Được” Vẫn Bị Senior Reject

Hãy bắt đầu bằng một tình huống quen thuộc. Một junior viết một service chuyển tiền cho ứng dụng ngân hàng. Code cho ra đúng output, tester cũng pass, nhưng senior review xong lắc đầu ngay lập tức. Đây là code của bạn ấy:

// ❌ Code junior viết — chạy được, "đúng output", nhưng senior reject ngay
public class TransferService {
    public static double balance1 = 0;
    public static double balance2 = 0;

    public static void deposit1(double amount) {
        balance1 += amount;
    }

    public static void withdraw1(double amount) {
        balance1 -= amount;
    }

    public static void deposit2(double amount) {
        balance2 += amount;
    }

    public static void withdraw2(double amount) {
        balance2 -= amount;
    }

    public static void transfer(double amount) {
        balance1 -= amount;
        balance2 += amount;
    }

    public static void main(String[] args) {
        deposit1(1000);
        transfer(300);
        System.out.println("A: " + balance1 + ", B: " + balance2);
    }
}

Nhìn qua, output vẫn đúng, nhưng bạn hãy đọc kỹ những lời phê trên PR sau đây:

  1. Đây là procedural code đội lốt class. Toàn bộ field và method đều static. Hãy tách method thành hành vi của một đối tượng Account thực sự.
  2. balance1, balance2public static → ai cũng có thể thay đổi từ bất kỳ đâu. Không có cơ chế đảm bảo tính toàn vẹn (invariant). Nếu ai đó set balance1 = -1000 thì sao? Encapsulation đâu?
  3. Dùng double cho tiền tệ là sai hoàn toàn. Trong banking, chúng ta dùng BigDecimal.
  4. Muốn thêm account thứ 3, bạn sẽ phải copy-paste deposit/withdraw? Hãy dùng đối tượng.
  5. Transfer không có tính chất nguyên tố (atomic). Nếu chương trình crash giữa dòng balance1 -= amountbalance2 += amount, tiền sẽ biến mất. Đây là banking thực sự đấy.
  6. Test thế nào? Toàn static method thì rất khó mock. Hãy áp dụng Dependency Injection.

Sau khi refactor theo đúng tư duy OOP, code trở thành:

// ✅ Account là một entity thực sự, có state và hành vi
public class Account {
    private final String accountId;
    private BigDecimal balance;
    private final String currency;

    public Account(String accountId, BigDecimal initialBalance, String currency) {
        if (accountId == null || accountId.isBlank())
            throw new IllegalArgumentException("accountId required");
        if (initialBalance == null || initialBalance.signum() < 0)
            throw new IllegalArgumentException("balance must be >= 0");
        this.accountId = accountId;
        this.balance = initialBalance;
        this.currency = currency;
    }

    public void deposit(BigDecimal amount) {
        validatePositive(amount);
        this.balance = this.balance.add(amount);
    }

    public void withdraw(BigDecimal amount) {
        validatePositive(amount);
        if (this.balance.compareTo(amount) < 0)
            throw new InsufficientFundsException(accountId, amount, balance);
        this.balance = this.balance.subtract(amount);
    }

    private void validatePositive(BigDecimal amount) {
        if (amount == null || amount.signum() <= 0)
            throw new IllegalArgumentException("amount must be > 0");
    }

    public BigDecimal getBalance() { return balance; }
    public String getAccountId() { return accountId; }
    public String getCurrency() { return currency; }
}

// ✅ TransferService chỉ phối hợp hành vi, không giữ state
public class TransferService {
    public void transfer(Account from, Account to, BigDecimal amount) {
        if (!from.getCurrency().equals(to.getCurrency()))
            throw new CurrencyMismatchException(from, to);
        from.withdraw(amount);
        to.deposit(amount);
        // Atomicity sẽ được đảm bảo ở tầng @Transactional bên ngoài
    }
}

Sự khác biệt không chỉ nằm ở syntax, mà ở chính tư duy thiết kế:

Procedural (Junior)OOP (Senior)
Data và behavior tách rờiData + behavior gắn kết (Account.deposit())
State public, ai cũng sửa đượcState private, chỉ class tự kiểm soát
Invariant phải kiểm tra ở mọi callerInvariant được bảo vệ ngay trong class
Static method – khó test, khó mockInstance method – dễ inject, dễ test
Thêm account = copy-paste codeThêm account = new Account(...)

Bạn thấy đấy, một service tưởng chừng đơn giản nhưng đã phơi bày hầu hết các vấn đề cốt lõi của việc thiếu tư duy OOP. Vậy làm sao để code của bạn không rơi vào vết xe đổ đó? Hãy bắt đầu xây dựng mental model cho 4 trụ cột.


2. 4 Trụ Cột OOP – Hiểu Sâu Bằng Mental Model

Sách giáo khoa thường đưa ra định nghĩa: Encapsulation, Inheritance, Polymorphism, Abstraction. Nhưng nếu chúng ta chỉ dừng ở định nghĩa, bạn sẽ không bao giờ “thấm”. Hãy cùng xây dựng những hình ảnh trực quan để mỗi khi viết code, bạn tự động áp dụng đúng.

Trụ cột 1: Encapsulation – “Class là một con thú có boundary”

Mental model: Hãy hình dung mỗi object như một con thú trong chuồng. Field của nó (state) là nội tạng – private, ẩn bên trong. Method là cách thế giới bên ngoài tương tác với nó – public. Nếu bạn phơi bày nội tạng ra ngoài, bất kỳ ai cũng có thể “mổ xẻ” con thú, và nó sẽ dễ dàng “chết” (lỗi) mà bạn không kiểm soát được.

// ❌ Encapsulation bị phá hủy hoàn toàn
public class BankAccount {
    public BigDecimal balance;  // Ai cũng set được
    public String status;       // Có thể set "BANANA" một cách vô tội vạ
}

// Caller có thể làm:
account.balance = new BigDecimal("-9999999"); // Tài khoản âm không kiểm soát
account.status = "BANANA";                    // Trạng thái không hợp lệ

// ✅ Encapsulation đúng: bảo vệ state, chỉ cung cấp hành vi có ý nghĩa
public class BankAccount {
    private BigDecimal balance;
    private AccountStatus status;  // enum

    public void debit(BigDecimal amount) {
        if (status != AccountStatus.ACTIVE)
            throw new AccountNotActiveException(status);
        if (balance.compareTo(amount) < 0)
            throw new InsufficientFundsException();
        balance = balance.subtract(amount);
    }

    public BigDecimal getBalance() { return balance; }
}

3 quy tắc vàng của encapsulation:

  1. Field luôn là private, chỉ protected nếu thực sự cần cho subclass.
  2. Cung cấp method có ý nghĩa thay vì cho truy cập trực tiếp field.
  3. Luôn validate trong constructor và các method thay đổi state, để object tự đảm bảo nó luôn ở trạng thái hợp lệ.

Cảnh báo: Đừng biến encapsulation thành “vỏ bọc rỗng” bằng cách sinh getter/setter cho mọi field.

// ❌ JavaBean syndrome: getter/setter mọi thứ, encapsulation = 0
public class Order {
    private List<OrderItem> items;
    public List<OrderItem> getItems() { return items; }
    public void setItems(List<OrderItem> items) { this.items = items; }
}

// Caller có thể dễ dàng phá hỏng object:
order.getItems().clear(); // Items biến mất, Order không hề hay biết
order.setItems(null);     // NPE rình rập

// ✅ Cung cấp method có ý nghĩa, bảo vệ collection
public class Order {
    private final List<OrderItem> items = new ArrayList<>();

    public void addItem(OrderItem item) {
        validate(item);
        items.add(item);
    }

    public void removeItem(String itemId) { /* ... */ }

    public List<OrderItem> getItems() {
        return Collections.unmodifiableList(items); // Read-only view
    }
}

Trước khi viết một getter/setter, hãy tự hỏi: “Caller thực sự cần làm gì? Tôi có thể cung cấp một method có ý nghĩa thay vì trả về trực tiếp state không?”

Trụ cột 2: Inheritance – “is-a”, không phải “has code-a”

Mental model: Inheritance là mối quan hệ “con là một loại của cha” (Dog is-a Animal). Nó không phải là công cụ để bạn “xài chùa” code từ class khác. Đây là lỗi vô cùng phổ biến.

// ❌ Kế thừa chỉ để dùng ké method – sai mục đích
public class StringUtils { /* utility methods */ }
public class UserController extends StringUtils {  // ← UserController có phải StringUtils không? Không!
    public ResponseEntity<User> getUser() {
        String trimmed = trim(input); // "tiện" gọi method từ cha
    }
}

// ✅ Dùng Composition: UserController SỬ DỤNG StringUtils, không phải LÀ StringUtils
public class UserController {
    public ResponseEntity<User> getUser() {
        String trimmed = StringUtils.trim(input);
    }
}

Nguyên lý Liskov Substitution (LSP), một trong 5 nguyên lý SOLID, tuyên bố: Subclass phải có thể thay thế cha ở mọi nơi mà cha xuất hiện, và không được phá vỡ hành vi được kỳ vọng. Hãy xem ví dụ kinh điển về Square và Rectangle:

// ❌ Vi phạm LSP: Square không thể thay thế Rectangle
public class Rectangle {
    protected int width, height;
    public void setWidth(int w) { this.width = w; }
    public void setHeight(int h) { this.height = h; }
    public int area() { return width * height; }
}

public class Square extends Rectangle {
    @Override
    public void setWidth(int w) { this.width = w; this.height = w; }
    @Override
    public void setHeight(int h) { this.width = h; this.height = h; }
}

// Code của caller chỉ biết đến Rectangle:
Rectangle r = new Square();  // Polymorphism – ổn không?
r.setWidth(5);
r.setHeight(10);
assert r.area() == 50;  // ❌ FAIL! Square ép area = 100

Bài học: Nếu một subclass cần override và thay đổi hành vi của parent theo hướng “ép buộc” khác đi, đó là dấu hiệu cho thấy mối quan hệ “is-a” không thực sự đúng. Hãy cân nhắc tách chúng ra thành các interface riêng.

Inheritance vs Composition – quy tắc vàng từ Effective Java:

“Favor composition over inheritance.” – Joshua Bloch

// ❌ Kế thừa HashSet: fragile, coupling chặt
public class CountingHashSet<E> extends HashSet<E> {
    private int addCount = 0;

    @Override public boolean add(E e) { addCount++; return super.add(e); }
    @Override public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c); // BUG! HashSet.addAll() gọi add() bên trong → đếm 2 lần
    }
}

// ✅ Composition: wrap, không phụ thuộc internal behavior
public class CountingSet<E> {
    private final Set<E> set;
    private int addCount = 0;

    public CountingSet(Set<E> set) { this.set = set; }
    public boolean add(E e) { addCount++; return set.add(e); }
    public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return set.addAll(c); }
    // Không đụng hàng với internal của HashSet
}

Khi nào dùng Inheritance?

  • Thực sự là “is-a” (Dog is-a Animal, EmailNotification is-a Notification)
  • Subclass không phá vỡ bất biến của parent.
  • Bạn không thể đạt được bằng composition (rất hiếm).

Khi nào dùng Composition?
Hầu hết thời gian (90% code application): “has-a”, “uses-a”, cần flexibility để đổi implementation khi chạy, hoặc đơn giản là code reuse mà không cần ràng buộc “is-a”.

Trụ cột 3: Polymorphism – “Cùng một API, khác behavior”

Mental model: Polymorphism cho phép bạn viết code một lần, nhưng làm việc với nhiều loại đối tượng khác nhau mà không cần biết cụ thể chúng là loại gì.

// Interface định nghĩa "hợp đồng"
public interface NotificationChannel {
    void send(String userId, String message);
}

// 3 implementation với hành vi riêng biệt
public class EmailChannel implements NotificationChannel {
    public void send(String userId, String message) { /* Gửi email */ }
}

public class SmsChannel implements NotificationChannel {
    public void send(String userId, String message) { /* Gửi SMS */ }
}

public class PushChannel implements NotificationChannel {
    public void send(String userId, String message) { /* Gửi push notification */ }
}

// Caller chỉ làm việc với NotificationChannel – không hề biết Email hay SMS
public class NotificationService {
    private final List<NotificationChannel> channels;

    public NotificationService(List<NotificationChannel> channels) {
        this.channels = channels;
    }

    public void notifyAll(String userId, String message) {
        for (NotificationChannel channel : channels) {
            channel.send(userId, message); // Đa hình!
        }
    }
}

Lợi ích thực tiễn:

  • Open-Closed Principle (O trong SOLID): thêm WhatsAppChannel mới, bạn không cần sửa NotificationService, chỉ thêm class mới.
  • Test cực dễ: mock NotificationChannel để kiểm tra thay vì phải kết nối email server thật.
  • Strategy pattern được hiện thực tự nhiên: bạn có thể thay đổi behavior lúc runtime bằng cách inject implementation khác.

Anti-pattern cần tránh: instanceof và cast thủ công

// ❌ Giết chết đa hình bằng type checking
public void send(NotificationChannel channel, ...) {
    if (channel instanceof EmailChannel) {
        ((EmailChannel) channel).send(...);
    } else if (channel instanceof SmsChannel) {
        ((SmsChannel) channel).send(...);
    }
    // Thêm kênh mới → phải sửa method này, vi phạm Open-Closed
}

// ✅ Đa hình – hành vi nằm trong chính interface
public interface NotificationChannel {
    void send(String userId, String message);
    void markSent();  // Mỗi implementation tự xử lý
}

Trụ cột 4: Abstraction – “Che giấu phức tạp, phơi bày ý định”

Mental model: Abstraction là nghệ thuật khiến caller chỉ nhìn thấy WHAT (mục đích), không cần biết HOW (cách thực hiện). Một class tốt là một class ẩn đi mọi chi tiết rối rắm và cung cấp một interface rõ ràng, đầy ý nghĩa.

// ❌ Caller buộc phải biết quá nhiều chi tiết – leak abstraction
public class TransferService {
    public void transfer(String fromId, String toId, BigDecimal amount,
                          Connection conn, TransactionTemplate tx,
                          Logger log, AuditLogger audit) {
        tx.execute(status -> {
            try {
                conn.prepareStatement("UPDATE accounts SET balance = ...");
                // 50 dòng code DB lộn xộn
            } catch (SQLException e) { /* ... */ }
        });
    }
}

// ✅ Abstraction đúng: chỉ cần một request object
public class TransferService {
    public void transfer(TransferRequest request) {
        // Mọi thứ DB, transaction, log ẩn bên trong
    }
}

// Caller nhìn vào:
transferService.transfer(new TransferRequest(fromId, toId, amount));

Quy tắc cho một abstraction tốt:

  1. Tên method miêu tả Ý ĐỊNH (transfer), không phải cách thực hiện (executeUpdateAndCommit).
  2. Số lượng parameter ít (≤4). Nhiều hơn hãy wrap thành object.
  3. Ẩn internal state: caller không được biết dữ liệu lưu thế nào.
  4. Thất bại ở mức phù hợp: không leak SQLException ra ngoài, hãy wrap thành TransferFailedException.

3. Những Mảnh Ghép Quan Trọng: this, super, static, final và Hơn Thế Nữa

Để vận dụng 4 trụ cột trên, bạn phải hiểu rõ vai trò của từng keyword. Dưới đây là cách chúng phối hợp trong một thiết kế thực tế.

public class Account {
    private static final BigDecimal MIN_BALANCE = BigDecimal.ZERO; // Constant

    private final String accountId;  // Không đổi sau khi khởi tạo
    private BigDecimal balance;

    public Account(String accountId, BigDecimal balance) {
        this.accountId = accountId;  // this.field = parameter (phân biệt)
        this.balance = balance;
    }

    // Constructor chaining
    public Account(String accountId) {
        this(accountId, BigDecimal.ZERO);
    }

    // Static factory method
    public static Account empty(String id) {
        return new Account(id);
    }
}

public class SavingsAccount extends Account {
    private BigDecimal interestRate;

    public SavingsAccount(String id, BigDecimal balance, BigDecimal rate) {
        super(id, balance);          // Gọi constructor parent, phải là dòng đầu
        this.interestRate = rate;
    }

    @Override
    public void deposit(BigDecimal amount) {
        super.deposit(amount);       // Gọi hành vi cha
        // Thêm logic tính lãi...
    }
}

Tóm lược:

KeywordÁp dụngÝ nghĩa
thisnon-static methodReference đến instance hiện tại
supersubclassReference đến parent class
staticfield/methodThuộc về class, không thuộc instance
final (field)fieldGiá trị không đổi sau khi gán; nếu là reference thì reference không đổi
final (method)methodCấm override
final (class)classCấm kế thừa (ví dụ: String)
static finalfieldConstant, viết UPPER_CASE

Abstract Class vs Interface: Khi Nào Dùng Cái Nào?

Đây là câu hỏi phỏng vấn muôn thuở. Junior thường học thuộc lòng, nhưng hãy hiểu qua thiết kế:

// Abstract class: có state + shared behavior, là "is-a"
public abstract class BankAccount {
    protected BigDecimal balance;
    protected String accountId;

    public BigDecimal getBalance() { return balance; } // Shared concrete method
    public abstract BigDecimal calculateInterest();    // Subclass bắt buộc override
}

public class SavingsAccount extends BankAccount {
    public BigDecimal calculateInterest() { return balance.multiply(new BigDecimal("0.05")); }
}

// Interface: contract thuần túy, có thể implement nhiều
public interface Auditable { AuditLog getAuditLog(); }
public interface Encryptable { void encrypt(); void decrypt(); }

// Một class vừa là BankAccount, vừa có khả năng Auditable và Encryptable
public class SecureSavingsAccount extends BankAccount implements Auditable, Encryptable {
    // ...
}

Ma trận quyết định:

Tiêu chíAbstract ClassInterface
State (field)Chỉ static field (Java 8+)
ConstructorKhông
Method bodyChỉ default/static (Java 8+)
Multiple inheritanceKhông (Java cấm)Implement nhiều interface
Mục đích“is-a” + chia sẻ codeContract / năng lực
Ví dụ bankingBankAccount cho Savings/CheckingAuditable, Encryptable, Notifiable

Lời khuyên: Default hãy bắt đầu với interface. Khi bạn thực sự cần chia sẻ state và hành vi chung (có constructor, field) – hãy dùng abstract class. Interface linh hoạt hơn nhiều vì một class có thể implement nhiều interface.

Constructor & Initialization Order

Hiểu thứ tự khởi tạo giúp bạn tránh những bug null pointer oái oăm khi kế thừa:

public class Parent {
    private int x = 10;
    public Parent() { System.out.println("Parent constructor"); }
    { System.out.println("Parent instance initializer"); }
    static { System.out.println("Parent static block"); }
}

public class Child extends Parent {
    private int y = 20;
    public Child() {
        super(); // Ngầm định nếu không viết
        System.out.println("Child constructor");
    }
    { System.out.println("Child instance initializer"); }
    static { System.out.println("Child static block"); }
}

// Kết quả khi new Child():
// 1. Parent static block
// 2. Child static block
// 3. Parent instance initializer
// 4. Parent constructor
// 5. Child instance initializer
// 6. Child constructor

Quy tắc nhớ: static (parent → child) → instance init (parent) → constructor (parent) → instance init (child) → constructor (child).

Access Modifier – 4 Cấp Độ Kiểm Soát

Same classSame packageSubclassAnywhere
private
(default)
protected
public

Nguyên tắc thực chiến: Luôn bắt đầu với private. Chỉ mở rộng lên protected hoặc public khi có lý do thực sự. Điều này giúp giảm thiểu phạm vi ảnh hưởng khi thay đổi code.

Inner Class & Static Nested Class

public class Outer {
    private int outerField = 10;

    public class Inner {               // Gắn với instance Outer, truy cập được outerField
        public void show() { System.out.println(outerField); }
    }

    public static class StaticNested { // Không gắn instance, không truy cập outerField
        public void show() { /* System.out.println(outerField); // Compile error! */ }
    }
}

// Cách khởi tạo:
Outer.Inner inner = new Outer().new Inner();               // Cần outer instance
Outer.StaticNested nested = new Outer.StaticNested();      // Không cần outer

Trong thực tế, static nested class rất hữu dụng cho Builder pattern hay các Holder. Inner class ít dùng hơn và dễ gây memory leak vì nó giữ reference đến outer object. Java 8+ khuyến khích dùng lambda thay cho anonymous inner class khi làm việc với functional interface.


4. SOLID Cho Junior: 3 Nguyên Lý Đầu Là Đủ

Bạn không cần phải trở thành guru về 5 nguyên lý SOLID ngay từ năm đầu, nhưng hãy nắm chắc 3 nguyên lý đầu tiên. Chúng sẽ là kim chỉ nam cho mọi quyết định thiết kế của bạn.

S — Single Responsibility Principle (SRP)

Mỗi class chỉ có một lý do để thay đổi.

// ❌ Class ôm đồm 3 trách nhiệm
public class User {
    private String email;
    public void save() { /* DB code */ }              // Persistence
    public void sendWelcomeEmail() { /* SMTP */ }    // Communication
    public boolean validate() { /* validation */ }   // Business rule
}
// Thay DB → sửa; đổi SMTP → sửa; đổi validate rule → sửa. Một class, ba lý do thay đổi.

// ✅ Tách bạch
public class User { /* chỉ data */ }
public class UserRepository { public void save(User u) { } }
public class EmailService { public void sendWelcomeEmail(User u) { } }
public class UserValidator { public boolean validate(User u) { } }

O — Open-Closed Principle (OCP)

Mở để mở rộng, đóng để chỉnh sửa.

// ❌ Thêm channel phải if-else
public class NotificationService {
    public void notify(String type, String msg) {
        if (type.equals("EMAIL")) sendEmail(msg);
        else if (type.equals("SMS")) sendSms(msg);
        // Thêm PUSH → sửa class này
    }
}

// ✅ Dùng đa hình: thêm channel mới = thêm class mới
public interface NotificationChannel { void send(String msg); }
public class EmailChannel implements NotificationChannel { /* ... */ }
public class PushChannel implements NotificationChannel { /* ... */ }

L — Liskov Substitution Principle (LSP)

Subclass thay thế được parent mà không gây lỗi hành vi mong đợi.

(Ví dụ Square/Rectangle ở trên.)
Hãy luôn kiểm tra: nếu một phương thức nhận kiểu cha, và bạn truyền vào instance của con, chương trình có chạy đúng không? Nếu không, kế thừa đó đang có vấn đề.

I — Interface Segregation Principle (ISP)

Interface béo nên tách thành nhiều interface nhỏ.

// ❌ Interface quá to
public interface Worker { void work(); void eat(); void sleep(); }
public class Robot implements Worker {
    // Robot không ăn, không ngủ → throw UnsupportedOperationException – lỗi thiết kế
}

// ✅ Tách nhỏ
public interface Workable { void work(); }
public interface Eatable { void eat(); }
public class Robot implements Workable { public void work() { } }
public class Human implements Workable, Eatable, Sleepable { /* ... */ }

D — Dependency Inversion Principle (DIP)

Phụ thuộc vào abstraction, không phụ thuộc vào concrete.

// ❌ UserService phụ thuộc trực tiếp MySqlUserRepository
public class UserService {
    private MySqlUserRepository repo = new MySqlUserRepository();
}

// ✅ Phụ thuộc interface, inject từ ngoài (Spring DI chính là đây)
public class UserService {
    private final UserRepository repo; // interface
    public UserService(UserRepository repo) { this.repo = repo; }
}

Khi sang Spring, bạn sẽ thấy @Autowired trên constructor chính là hiện thực của DIP, giúp code dễ dàng thay đổi data source mà không cần sửa logic business.


5. Anti-Patterns Spring & General – Những Cái Bẫy Cần Tránh

God Class

Một class dài 2000 dòng, làm đủ thứ từ tạo user đến export CSV.
Giải pháp: Tách theo SRP – mỗi class một trách nhiệm.
“Nếu bạn không thể mô tả chức năng của class trong một câu không có chữ ‘và’, class đó đang làm quá nhiều việc.”

Anemic Domain Model

Entity chỉ có getter/setter, mọi logic dồn hết vào Service. Đây là kiểu code rất phổ biến trong các dự án Spring Boot khi developer quen “để entity rỗng, Service xử lý”.
Giải pháp: Đưa behavior trở lại entity (Rich Domain Model), chẳng hạn Order.cancel() thay vì orderService.cancel(order). Logic nghiệp vụ sẽ không bị phân tán khắp nơi.

// ❌ Anemic
public class Order { private OrderStatus status; /* getter/setter */ }
public class OrderService {
    public void cancel(Order order) {
        if (order.getStatus() != PENDING) throw ...;
        order.setStatus(CANCELLED);
    }
}

// ✅ Rich
public class Order {
    private OrderStatus status;
    public void cancel() {
        if (status != PENDING) throw new InvalidOrderStateException(...);
        status = CANCELLED;
    }
}

Constructor Quá Nhiều Tham Số

Khi bạn thấy constructor 10 tham số, đó là lúc cần Builder pattern:

Order order = Order.builder()
    .id(123L).userId("user1")
    .total(BigDecimal.valueOf(100)).currency("VND")
    .build();

Cyclic Dependency

A phụ thuộc B, B phụ thuộc A → vòng lặp khởi tạo, khó test.
Giải pháp: Dùng interface trung gian hoặc event để phá vỡ phụ thuộc vòng. Spring Boot sẽ báo lỗi nếu bạn vô tình tạo ra vòng lặp constructor injection.

Thiếu equals/hashCode Khi Dùng Map

Khi dùng custom object làm key cho HashMap, nhớ override equals()hashCode(). Nếu không, map sẽ không tìm thấy object dù giá trị giống hệt. Java 16 trở lên, record giúp bạn giải quyết vấn đề này một cách tự động:

record UserId(String value) { }  // equals/hashCode/toString tự sinh

6. Thực Hành – Từ Refactoring Đến Thiết Kế

Lý thuyết là vậy, nhưng bạn chỉ thực sự “ngộ” khi tự tay mổ xẻ code. Dưới đây là ba bài tập, từ dễ đến khó, giúp bạn luyện tập tư duy OOP.

Exercise 1: Refactor Procedural → OOP (Easy)

Code gốc quản lý thư viện bằng mảng static.
Yêu cầu: Tạo class Book (encapsulation), Library (quản lý collection). Thêm validation: không borrow sách đã borrowed, không thêm duplicate ISBN. Sử dụng Optional cho find method.

Exercise 2: Áp Dụng Polymorphism (Medium)

Thiết kế hệ thống tính phí giao dịch ngân hàng:

  • DomesticTransfer: phí 5,000 VND
  • InternationalTransfer: 0.5% amount, tối thiểu 50,000 VND
  • InternalTransfer: miễn phí
  • Thêm CryptoTransfer (phí 1% + 100,000) mà không sửa code cũ.

Gợi ý: Interface Transferable với BigDecimal calculateFee(). Service TransferProcessor chỉ biết interface.

Exercise 3: SOLID Refactoring (Hard)

Class ReportGenerator gồm một method generate với if-else theo loại báo cáo. Bên trong chứa code sinh PDF/Excel/CSV, lưu file, gửi mail, log.
Yêu cầu: Áp dụng SRP tách Generator, Exporter, Notifier, Logger. Áp dụng OCP để thêm format JSON không sửa code cũ. Dùng DIP – mọi dependency được inject, không new trực tiếp.

Hãy cố gắng hoàn thành 3 bài tập này. Đặt code lên GitHub để theo dõi tiến bộ của chính mình.


7. Chuẩn Bị Cho Phỏng Vấn – “Bắn” OOP Từ Surface Đến Architecture

Tầng Surface (Junior hay gặp)

Q: 4 trụ cột OOP là gì?
A: Encapsulation: đóng gói state, cung cấp method có ý nghĩa, tự validate. Inheritance: “is-a”, chia sẻ hành vi. Polymorphism: cùng interface, hành vi khác nhau lúc runtime. Abstraction: ẩn chi tiết, chỉ phơi bày ý định. Bốn trụ cột giúp code dễ mở rộng, dễ test và bảo trì.

Q: Abstract class vs Interface?
A: Abstract class có state (field), constructor, concrete method – dùng cho “is-a” và chia sẻ code. Interface là contract thuần túy, một class có thể implement nhiều interface, từ Java 8 có default method. Default: chọn interface, dùng abstract class khi cần state chung.

Tầng Deep Dive (Mid-level)

Q: Vì sao “Favor composition over inheritance”?
A: 1) Fragile base class – subclass phụ thuộc internal parent. 2) Tight coupling – kế thừa cố định lúc compile, composition linh hoạt runtime. 3) Java chỉ cho extends một class – composition cho phép “use” nhiều thành phần. 90% trường hợp app dùng composition.

Q: Cho ví dụ vi phạm LSP và cách sửa.
A: Square extends Rectangle. setWidth(5); setHeight(10); area() trả về 100 thay vì 50. Sửa: cả hai implement interface Shape với area(), không kế thừa nhau.

Tầng Architecture (Senior signal)

Q: Tại sao Spring khuyến khích Constructor Injection thay vì Field Injection?
A: 1) Immutable – field final. 2) Required dependency rõ ràng qua constructor signature. 3) Test dễ – không cần Spring context. 4) Circular dependency compile-time error thay vì runtime. Đây chính là DIP trong thực tế.


8. Checklist Thành Thạo – Trước Khi Bước Tiếp

☑ Mọi field đều private; validate trong constructor và setter.
☑ Phân biệt được “is-a” vs “has-a”; cân nhắc composition trước khi dùng extends.
☑ Đã tự tay viết code với interface và nhiều implementation.
☑ Chọn đúng giữa abstract class và interface qua 5 case study.
☑ Giải thích được SRP, OCP, LSP với ví dụ thực tế.
☑ Đoán đúng output của static block + instance init + super().
☑ Override equals/hashCode đúng; hiểu record tự động làm điều này.
☑ Hoàn thành ít nhất 2 trong 3 bài tập refactor.

Khi bạn có thể tự tin đánh dấu hết các mục trên, chúc mừng – bạn đã sẵn sàng tiến vào thế giới Exception & Logging (#49), và từ đó, Spring Boot sẽ không còn là một bộ API ma thuật nữa, mà là những công cụ được xây dựng trên chính nền tảng OOP vững chắc mà bạn đang có.


Tự Học Tiếp

Sách (đọc theo thứ tự):

  1. Head First Java (Kathy Sierra) – Nhập môn đầy hứng khởi.
  2. Effective Java (Joshua Bloch) – 30 điều đầu tiên cho năm đầu, toàn bộ 90 điều cho năm 2-3.
  3. Clean Code (Robert C. Martin) – Chương 6, 10, 11 về OOP.

Practice thêm:

Lời cuối: Khi bạn đọc code của Spring, Hibernate, hãy tự đặt câu hỏi: “Pattern OOP nào đang được sử dụng ở đây? Tại sao họ lại thiết kế như vậy?” Vẽ sơ đồ class trên giấy. Tư duy phản biện từng dòng code chính là cách bạn đi từ junior lên senior.

Hãy kết nối

Nếu bạn quan tâm tới việc hợp tác, có câu hỏi về bài viết, hay chỉ đơn giản muốn chuyện trò về backend — cứ ping mình nhé.