OOP: từ object đến thiết kế dễ thay đổi
OOP không phải chỉ là gom field và method vào class. Mục tiêu là đặt state cùng behavior bảo vệ state đó, tạo boundary rõ và cho phép thay đổi implementation mà ít ảnh hưởng client.
1. Object, class và invariant
Class là định nghĩa kiểu; object là instance có identity, state và behavior. Một class tốt không chỉ chứa dữ liệu mà còn giữ invariant: điều kiện phải luôn đúng.
Ví dụ nguồn · BankAccount · minh họa guard; chưa có luồng nạp tiền hoặc số dư ban đầu.
public final class BankAccount {
private long balance;
public void withdraw(long amount) {
if (amount <= 0 || amount > balance) {
throw new IllegalArgumentException("Invalid amount");
}
balance -= amount;
}
}
Nếu để balance public hoặc chỉ sinh getter/setter vô điều kiện, caller có thể tạo state sai. Đây là lý do encapsulation quan trọng hơn việc “field phải private” một cách máy móc.
Bổ sung · ví dụ thiết kế và kiểm chứng
Biến invariant thành điều kiện kiểm tra được
Với ví dụ này, viết rõ balance ≥ 0, đầu vào hợp lệ là 0 < amount ≤ balance, và một lần rút bị từ chối phải giữ nguyên số dư. Constructor phải tạo state hợp lệ; mọi đường cập nhật sau đó phải giữ cùng quy tắc. Đó là một contract có thể test, không chỉ là quy ước đặt tên field.
balance khởi tạo mặc định bằng 0, chưa có constructor nhận số dư hay thao tác nạp tiền. Vì vậy không có lần rút dương nào thành công ngay sau new BankAccount(). Đoạn này minh họa guard, không phải ứng dụng tài khoản hoàn chỉnh.Bài tập bổ sung · lưu thành BankAccountCheck.java · chạy độc lập, không cần thư viện.
public class BankAccountCheck {
static final class Account {
private long balance;
Account(long initialBalance) {
if (initialBalance < 0) {
throw new IllegalArgumentException("Negative balance");
}
balance = initialBalance;
}
void withdraw(long amount) {
if (amount <= 0 || amount > balance) {
throw new IllegalArgumentException("Invalid amount");
}
balance -= amount;
}
long balance() { return balance; }
}
static void rejected(Account account, long amount) {
long before = account.balance();
try {
account.withdraw(amount);
throw new AssertionError("Expected rejection");
} catch (IllegalArgumentException expected) {
if (account.balance() != before) {
throw new AssertionError("State changed after rejection");
}
}
}
public static void main(String[] args) {
Account account = new Account(100);
rejected(account, 0);
rejected(account, -1);
rejected(account, 101);
account.withdraw(100);
if (account.balance() != 0) {
throw new AssertionError("Wrong balance");
}
rejected(account, 1);
System.out.println("PASS: invariant and rejected transitions");
}
}Acceptance: ba đầu vào 0, −1, 101 bị từ chối khi số dư là 100; rút đúng 100 thành công; rút thêm 1 thất bại; mọi lần thất bại đều không đổi state. Output: PASS: invariant and rejected transitions. Thử bỏ guard rồi chạy lại để thấy test bắt được lỗi.
private không làm thao tác check-then-update trở thành atomic. Khi chia sẻ giữa threads, cần bảo vệ toàn bộ vùng kiểm tra và cập nhật bằng một cơ chế đồng bộ phù hợp; khi có persistence/nhiều process, cần thiết kế transaction/concurrency riêng. Không suy ra bảo đảm tài chính hay tính bền vững dữ liệu từ bài tập này. [O9]2. Bốn trụ cột và điểm hay nhầm
| Khái niệm | Câu hỏi nó trả lời | Ví dụ Java |
|---|---|---|
| Encapsulation | Ai được phép thấy/sửa state, và invariant được bảo vệ ở đâu? | private field + method nghiệp vụ như withdraw. |
| Abstraction | Client cần biết contract nào và có thể bỏ qua chi tiết nào? | PaymentGateway.charge() che HTTP, retry, vendor SDK. |
| Inheritance | Kiểu con có thật sự là một kiểu cha và giữ nguyên contract không? | CardPayment implements PaymentMethod. |
| Polymorphism | Cùng một lời gọi chạy implementation phù hợp với object runtime nào? | gateway.charge() dispatch tới Stripe/PayPal adapter. |
Bổ sung · phân biệt contract và implementation
Một boundary phải nói cả cách thất bại
Trong ví dụ PaymentGateway.charge(), abstraction cho client một contract; encapsulation bảo vệ cách adapter giữ state và thông tin cấu hình. Đề xuất khi thiết kế contract: ghi rõ đầu vào, kết quả thành công, lỗi nghiệp vụ, timeout và trách nhiệm retry. Ẩn vendor SDK không có nghĩa là giấu luôn mọi kết quả thất bại.
Từ CardPayment implements PaymentMethod, hãy đọc là subtyping qua interface: một kiểu thực thi contract của kiểu khác, không phải kế thừa mutable state từ một superclass. Java tách hai việc này; bảng bốn trụ cột là mental model, không phải bốn keyword tương ứng một-một. [O1] [O2]
Tự kiểm: vẽ một caller và hai adapter. Đổi adapter mà không sửa caller; nêu một invariant nội bộ và một điều kiện lỗi nhìn thấy qua contract. Đây là evidence cụ thể cho cả abstraction lẫn encapsulation.
3. Overriding, overloading và dynamic dispatch
- Overriding: subclass cung cấp lại instance method cùng signature; method được chọn theo object runtime.
- Overloading: nhiều method cùng tên nhưng parameter list khác; compiler chọn theo static types tại compile time.
static,privatevà constructor không tham gia polymorphic overriding như instance method thông thường.
Ví dụ nguồn · trích đoạn trong method; giả định đã có PaymentMethod, CardPayment và biến order.
PaymentMethod method = new CardPayment();
method.pay(order); // Runtime dispatch gọi CardPayment.pay(...)Bổ sung · đã đối chiếu tài liệu
Hai bước độc lập: chọn signature, rồi chọn implementation
Bài tập bổ sung · DispatchDemo.java · dự đoán hai dòng output trước khi chạy.
public class DispatchDemo {
static class Base {
String pay(Object order) { return "Base/Object"; }
}
static class Card extends Base {
@Override
String pay(Object order) { return "Card/Object"; }
String pay(String order) { return "Card/String"; }
}
public static void main(String[] args) {
Base method = new Card();
System.out.println(method.pay("order")); // Card/Object
Card card = new Card();
System.out.println(card.pay("order")); // Card/String
}
}Ở lời gọi đầu, static type Base khiến compiler chọn pay(Object); object thực là Card nên runtime chạy override của signature đó. Lời gọi thứ hai có static type Card, nên overload pay(String) được chọn. Runtime không quay lại chọn overload theo kiểu thật của đối số. [O4]
Không thể overload chỉ bằng kiểu trả về. static method có thể bị hide, không override; private method và constructor không được kế thừa để override. Dùng @Override để compiler phát hiện việc vô tình tạo overload thay vì override. [O1] [O2]
4. “Is-a” và “has-a”
Inheritance dùng cho quan hệ is-a: mọi object con phải thay được object cha mà không phá kỳ vọng của caller. Composition dùng cho has-a/uses-a: object nhận collaborator qua constructor và ủy quyền công việc.
Ví dụ nguồn · CheckoutService · cần định nghĩa PaymentGateway của ứng dụng để compile.
public final class CheckoutService {
private final PaymentGateway gateway;
public CheckoutService(PaymentGateway gateway) {
this.gateway = gateway;
}
}
Ưu tiên composition khi mục tiêu chỉ là tái sử dụng behavior hoặc thay implementation. Inheritance làm subclass phụ thuộc state/lifecycle của superclass và dễ tạo “fragile base class”.
instanceof, bỏ qua method, đổi precondition mạnh hơn hoặc trả kết quả trái contract cho một subtype, hierarchy có thể sai.Bổ sung · quy tắc chẩn đoán và trade-off
Dùng substitutability để quyết định, không chỉ nhìn tên class
Một phép thử tự biên soạn: client nhận PaymentMethod và đã được phép gọi pay(order) với một order hợp lệ. Một subtype bất ngờ đòi client downcast trước, hoặc luôn ném lỗi cho thao tác mà base contract cam kết hỗ trợ, là tín hiệu cần xem lại hierarchy. Không tăng precondition và không làm yếu postcondition mà caller đang dựa vào.
instanceof chưa đủ chứng minh vi phạm LSP. Một pattern match ở boundary hoặc trên một sealed hierarchy có chủ đích có thể hoàn toàn hợp lệ. Câu hỏi chẩn đoán vẫn là: caller có bị phá contract khi thay subtype hay không?Trong CheckoutService, constructor injection cho phép truyền adapter thật hoặc test double. Đề xuất thêm kiểm tra non-null cho dependency và kiểm thử cùng một contract trên các adapter. Trade-off: composition thêm bước ủy quyền và cấu hình; inheritance vẫn phù hợp khi quan hệ is-a, extension points và lifecycle thực sự ổn định.
Evidence: viết một bộ contract tests chạy cho ít nhất hai implementation; cùng đầu vào hợp lệ phải đáp ứng cùng postcondition. Không chỉ test từng concrete class bằng bộ giả định riêng.
5. Association, aggregation và composition
| Quan hệ object | Ý nghĩa thực dụng | Lifecycle |
|---|---|---|
| Association | Một object biết/dùng object khác. | Không nói ownership. |
| Aggregation | Whole chứa part nhưng part có thể sống độc lập. | Lifecycle tương đối độc lập. |
| Composition | Whole sở hữu part như một phần invariant. | Part thường sinh/chết cùng whole. |
Trong code thực tế, hãy nói rõ ownership, mutability và lifecycle. Quan hệ object không tự động quyết định cardinality hay cascade trong database.
Bổ sung · làm rõ phạm vi thuật ngữ
Ownership là một quyết định thiết kế, không phải hiệu ứng của cú pháp
“Composition” ở mục 4 được dùng theo nghĩa rộng: ghép behavior bằng collaborator. “Composition” trong bảng quan hệ object nhấn mạnh ownership/lifecycle. Chỉ nhìn một field hay constructor injection chưa đủ kết luận quan hệ lifecycle: gateway có thể được nhiều service dùng chung.
Khi review, ghi lại ai tạo part, ai được sửa nó, ai chịu trách nhiệm kết thúc sử dụng/đóng resource, và có alias nào giữ part ngoài whole hay không. final khóa việc gán lại reference, không tự đóng băng object được tham chiếu. [O1]
6. Interface, abstract class và kiểu Java hiện đại
- Dùng interface cho capability/boundary có nhiều implementation không cần chung mutable state.
- Dùng abstract class khi một family thật sự chia sẻ state, constructor và protected implementation.
- Dùng sealed class/interface khi tập subtype đóng và compiler nên kiểm tra exhaustiveness.
- Dùng record cho value carrier bất biến nông; record không tự làm mutable component trở nên immutable.
Bổ sung · đã đối chiếu tài liệu
Chọn kiểu theo điều cần bảo vệ
| Lựa chọn | Phù hợp khi | Chi phí / điều không được suy ra |
|---|---|---|
| Interface | Cần contract/capability với nhiều implementation. | Không có instance state của interface; default/static/private methods không biến nó thành base class có constructor. |
| Abstract class | Một family thật sự chia sẻ state và construction. | Subclass gắn với lifecycle của base class; Java chỉ cho một direct superclass. |
| Sealed type | Muốn kiểm soát các direct subtype của mô hình. | Nhánh non-sealed có thể mở lại khả năng mở rộng; không mặc định mọi leaf đều bị đóng. |
| Record | Muốn biểu diễn value carrier bằng components. | Bất biến nông; component mutable vẫn cần ownership/defensive copy phù hợp. |
Bảng này triển khai các lựa chọn ở phần nguồn; cú pháp và giới hạn class/interface theo JLS. [O1] [O2]
instanceof là tính năng chính thức từ Java 16; sealed types từ Java 17; pattern matching cho switch từ Java 21. Ví dụ dưới dùng JDK 21 và không cần --enable-preview. Đây là baseline của bài, không phải khẳng định JDK mới nhất. [O6]Bài tập bổ sung · ValueTypesDemo.java · defensive snapshot và exhaustive switch.
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class ValueTypesDemo {
record Labels(List<String> values) {
Labels {
values = List.copyOf(values);
}
}
sealed interface PaymentResult permits Approved, Declined {}
record Approved(String receiptId) implements PaymentResult {}
record Declined(String reason) implements PaymentResult {}
static String describe(PaymentResult result) {
return switch (Objects.requireNonNull(result)) {
case Approved a -> "approved:" + a.receiptId();
case Declined d -> "declined:" + d.reason();
};
}
public static void main(String[] args) {
var original = new ArrayList<>(List.of("java"));
var labels = new Labels(original);
original.add("changed");
if (!labels.values().equals(List.of("java"))) {
throw new AssertionError("Alias leaked");
}
try {
labels.values().add("oops");
throw new AssertionError("Expected unmodifiable list");
} catch (UnsupportedOperationException expected) {
// The public accessor does not expose a mutable list.
}
System.out.println(labels.values());
System.out.println(describe(new Approved("R1")));
System.out.println(describe(new Declined("blocked")));
}
}Expected output: lần lượt là [java], approved:R1, declined:blocked. List.copyOf tạo kết quả không phản ánh những lần sửa cấu trúc list đầu vào về sau, không cho phép sửa qua accessor và từ chối null list/null elements. Đây không phải deep copy; bài dùng String immutable làm phần tử. Với component là một mutable object, cần quyết định copy đến mức nào. [O7]
Thử làm hỏng: bỏ List.copyOf để thấy alias leak; thêm một subtype mới vào sealed model và quan sát yêu cầu cập nhật switch. Objects.requireNonNull ở đây chủ động từ chối null; exhaustiveness theo subtype không thay thế chính sách null. [O4] [O6]
Liên hệ equality: Object.equals mặc định kiểm identity; value type cần contract equality phù hợp. Khi tự override equals, phải giữ consistency với hashCode. Đừng dùng mutable state đang thay đổi làm nền tảng cho key trong hash collection. [O8]
7. SOLID như công cụ chẩn đoán
| Nguyên tắc | Dấu hiệu vi phạm | Hướng sửa thường gặp |
|---|---|---|
| SRP | Một class đổi vì nhiều nhóm lý do không liên quan. | Tách policy, persistence, formatting hoặc integration boundary. |
| OCP | Thêm loại mới phải sửa chuỗi if/switch rải rác. | Đưa variation sau interface/strategy khi variation là thật. |
| LSP | Subtype không giữ contract của base type. | Sửa hierarchy, contract hoặc chuyển sang composition. |
| ISP | Client phụ thuộc method nó không dùng. | Tách interface theo capability của consumer. |
| DIP | Business policy phụ thuộc trực tiếp framework/vendor detail. | Dependency hướng vào abstraction thuộc core; adapter implement ở ngoài. |
SOLID không yêu cầu mỗi class chỉ có một method hay mọi dependency đều cần interface. Chỉ tạo abstraction khi nó bảo vệ một boundary hoặc variation có ý nghĩa.
Bổ sung · phân tích thiết kế tự biên soạn
Từ một nguyên tắc đến bằng chứng thay đổi
Đề xuất cách dùng bảng SOLID: chọn một thay đổi đã xảy ra, liệt kê những nơi phải sửa, rồi xác định dependency nào khiến thay đổi lan rộng. Ví dụ đổi vendor thanh toán: ưu tiên giữ business policy và contract ổn định, để vendor mapping nằm trong adapter. Nếu chỉ thêm một lớp chuyển tiếp không bảo vệ được gì, abstraction đó chưa có lý do tồn tại.
DIP là định hướng dependency vào abstraction phù hợp; dependency injection chỉ là cách cung cấp object cộng tác. Một constructor nhận trực tiếp vendor SDK vẫn có injection, nhưng business policy có thể còn phụ thuộc vendor. Đây là phân tích thiết kế, không phải lỗi mà compiler tự bắt.
Evidence đề xuất: lưu diff của một thay đổi thật, contract tests trước/sau và một đoạn giải thích vì sao tách hoặc không tách boundary. Số interface không phải thước đo chất lượng thiết kế.
8. Checklist trả lời phỏng vấn
- Giải thích OOP bằng invariant, contract và substitutability thay vì chỉ đọc 4 định nghĩa.
- Phân biệt abstraction với encapsulation bằng một ví dụ.
- Phân biệt overriding/runtime dispatch với overloading/compile-time selection.
- Chứng minh lúc nào inheritance đúng; giải thích vì sao composition thường ít coupling hơn.
- Nêu trade-off của interface, abstract class, record và sealed type.
- Áp dụng SOLID theo dấu hiệu thay đổi thực tế, không tạo interface/class thừa.
Bổ sung · interview evidence
Biến 6 mục checklist thành bài trả lời có bằng chứng
| Mục nguồn | Bằng chứng tối thiểu để tự đánh giá |
|---|---|
| 1. Invariant / contract / substitutability | Nêu một invariant; chỉ ra guard, postcondition và test cho cả thành công lẫn thất bại. |
| 2. Abstraction ≠ encapsulation | Dùng cùng một ví dụ gateway để tách contract nhìn từ caller và state/chi tiết bên trong. |
| 3. Overriding ≠ overloading | Dự đoán đúng hai dòng DispatchDemo và giải thích static type ở bước chọn signature. |
| 4. Inheritance và composition | Đưa một case is-a hợp lệ, một case chỉ uses-a và mô tả contract test dùng chung. |
| 5. Interface / abstract / record / sealed | Nêu một trade-off mỗi loại; chạy thử alias leak và chỉ ra baseline Java của switch. |
| 6. SOLID không hình thức | Chỉ ra một diff hoặc tình huống thay đổi và giải thích boundary có ý nghĩa; không đo bằng số class. |
Lệnh chạy các chương trình bổ sung sau khi lưu mỗi block vào đúng tên file.
javac --release 21 BankAccountCheck.java DispatchDemo.java ValueTypesDemo.java
java BankAccountCheck
java DispatchDemo
java ValueTypesDemoAcceptance của buổi học: cả ba chương trình chạy đúng output đã ghi; người học giải thích được các lỗi khi bỏ guard hoặc bỏ defensive snapshot. Lưu source thử nghiệm, phiên bản java -version, stdout và câu trả lời cho 6 mục. Đây là bài kiểm chứng trong chuyên đề, không cộng vào tổng lab của Part 01.
Tài liệu tham khảo & phạm vi phiên bản
[O1] JLS 8 · Classes — Class, inheritance, overriding, constructor, record và sealed class. Bản Java SE 21.
[O2] JLS 9 · Interfaces — Interface, default/static/private methods và quan hệ implementation. Bản Java SE 21.
[O3] Oracle Java Object-Oriented Concepts — Giữ reference nguồn cho khái niệm nền. Tutorial này hướng tới JDK 8, không dùng để xác định mốc record/sealed/pattern switch.
[O4] JLS 15.12 · Method Invocation — Chọn method tại compile time và xác định method chạy tại runtime; chương 15 cũng quy định switch expressions.
[O5] JLS 12.5 · Creation of New Class Instances — Trình tự khởi tạo và dynamic dispatch trong construction.
[O6] Oracle · Java Language Changes — Mốc chính thức của record, sealed types và pattern matching cho switch.
[O7] Java 21 API · List.copyOf — Unmodifiable snapshot, không phản ánh thay đổi cấu trúc collection đầu vào và giới hạn null.
[O8] Java 21 API · Object.equals / hashCode — Identity mặc định và contract equality/hash code.
[O9] JLS 17 · Threads and Locks — Phân biệt encapsulation với đồng bộ và visibility giữa threads.