Kết hợp và kế thừa
Thuộc phần Lập trình hướng đối tượng trong hành trình Java của Coddy. Bài học 47 trên 87.
Kế thừa tạo ra quan hệ "là một": Dog là một Animal. Nhưng nếu một lớp cần chức năng từ thứ mà nó không phải thì sao? Đây là lúc kết hợp phát huy tác dụng, tạo ra quan hệ "có một" thay thế.
Với kỹ thuật kết hợp đối tượng, một lớp chứa các đối tượng của những lớp khác làm thuộc tính thay vì kế thừa chúng:
// Cách dùng kế thừa (inheritance): Car LÀ một Engine (không hợp lý)
class Car extends Engine { }
// Cách dùng thành phần (composition): Car CÓ một Engine (hợp lý)
class Car {
private Engine engine;
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.ignite();
}
}Composition mang lại một số lợi ích. Bạn có thể thay đổi đối tượng được kết hợp trong khi chương trình đang chạy, chẳng hạn như đổi động cơ.
Bạn cũng có thể kết hợp hành vi từ nhiều lớp (class), điều mà kế thừa (inheritance) không cho phép trong Java. Lớp kết hợp chỉ cung cấp những phương thức (method) mà nó chọn, giúp tăng tính đóng gói (encapsulation).
Quy tắc thực tế: hãy dùng tính kế thừa (inheritance) khi thực sự có mối quan hệ “là một” và lớp con đúng là phiên bản chuyên biệt của lớp cha.
Hãy dùng cách kết hợp khi bạn cần sử dụng chức năng của một lớp khác mà không cần thuộc kiểu đó. Nhiều nhà phát triển giàu kinh nghiệm tuân theo nguyên tắc: "ưu tiên kết hợp hơn kế thừa" vì cách này giúp code linh hoạt và dễ bảo trì hơn.
Thử thách
DễHãy xây dựng một hệ thống máy tính bằng cách sử dụng composition để minh họa mối quan hệ “có”. Thay vì để Computer kế thừa các thành phần của nó (điều này không hợp lý), máy tính của bạn sẽ chứa các bộ phận cần thiết để hoạt động.
Bạn sẽ tổ chức code trong ba tệp:
Processor.java: Tạo một lớp đại diện cho CPU. Một Processor có hai trường riêng tư:brand(String) vàspeedGHz(double). Thêm hàm khởi tạo để khởi tạo cả hai trường và các phương thức getter cho từng trường. Thêm phương thứcprocess()trả về một String:[brand] processing at [speedGHz] GHzMemory.java: Tạo một lớp đại diện cho RAM. Một Memory có hai trường riêng tư:type(String, chẳng hạn như "DDR4" hoặc "DDR5") vàsizeGB(int). Thêm hàm khởi tạo, các getter và phương thứcload()trả về:Loading [sizeGB]GB [type] memoryComputer.java: Đây là nơi composition phát huy tác dụng! Lớp Computer của bạn cần có một Processor và một Memory làm các trường riêng tư. Lớp này không kế thừa chúng mà chứa chúng. Thêm hàm khởi tạo nhận cả hai thành phần. Thêm phương thứcboot()trả về một String nhiều dòng, thể hiện máy tính khởi động bằng cách sử dụng các thành phần của nó:Đồng thời thêm phương thứcBooting computer... [processor.process() result] [memory.load() result] System ready!getSpecs()trả về:Specs: [processor brand] CPU, [memory sizeGB]GB [memory type]Main.java: Hãy khởi động hệ thống bạn đã kết hợp! Bạn sẽ nhận bốn đầu vào: hãng bộ xử lý (String), tốc độ bộ xử lý (double), loại bộ nhớ (String) và dung lượng bộ nhớ (int).Tạo Processor và Memory với các giá trị này, sau đó kết hợp chúng vào một Computer. In ra kết quả gọi
boot(), rồi in ra kết quả gọigetSpecs().
Bạn sẽ nhận lần lượt bốn đầu vào: hãng bộ xử lý, tốc độ bộ xử lý (GHz), loại bộ nhớ và dung lượng bộ nhớ (GB).
Hãy chú ý cách Computer giao việc cho các thành phần thay vì cố gắng trở thành chúng. Đó chính là sức mạnh của composition! Computer có thể sử dụng bất kỳ Processor hoặc Memory nào bạn cung cấp, giúp thiết kế linh hoạt và thực tế.
Tự làm thử
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// Đọc các giá trị đầu vào
String processorBrand = scanner.nextLine();
double processorSpeed = Double.parseDouble(scanner.nextLine());
String memoryType = scanner.nextLine();
int memorySize = Integer.parseInt(scanner.nextLine());
// TODO: Tạo Processor với thương hiệu và tốc độ
// TODO: Tạo Memory với loại và dung lượng
// TODO: Tạo Computer bằng cách kết hợp Processor và Memory
// TODO: In ra kết quả của boot()
// TODO: In ra kết quả của getSpecs()
}
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Kiến thức cơ bản về lập trình hướng đối tượng
Tệp bên ngoàiGiới thiệu về lập trình hướng đối tượngLớp và đối tượngTừ khóa thisPhương thứcTrường (thuộc tính)Phương thức khởi tạoNạp chồng phương thức khởi tạoÔn tập: Máy tính đơn giản4Kế thừa
Kế thừa cơ bản (extends)Từ khóa superGhi đè phương thức (@Override)Chuỗi gọi hàm khởi tạoLớp ObjectKế thừa đơn và đa cấpVì sao không hỗ trợ kế thừa nhiều lớpÔn tập: Phân cấp lớp Employee7Phương thức đặc biệt và lớp Object
Phương thức toString()equals() và hashCode()Phương thức clone()compareTo() và ComparableGiao diện ComparatorÔn tập: sắp xếp tùy chỉnh2Bộ điều chỉnh quyền truy cập và đóng gói
Tổng quan về các mức truy cậpPhương thức getter và setterẨn thông tinTừ khóa finalÔn tập: Trình quản lý tài khoản ngân hàng5Tính đa hình
Kiến thức cơ bản về nạp chồng phương thứcGhi đè phương thức (thời gian chạy)Chuyển kiểu lên và chuyển kiểu xuốngToán tử instanceofLớp và phương thức trừu tượngÔn tập: máy tính hình học8Khái niệm OOP nâng cao
Kết hợp và kế thừaKết tập và hợp thànhLớp bên trong, lớp lồng nhau và lớp ẩn danhKiểu enum và phương thức enumBản ghi (Java 16+)Lớp sealed (Java 17+)3Thuộc tính lớp và thành viên tĩnh
Biến của instance và biến tĩnhPhương thức tĩnhKhối tĩnhHằng số (static final)Ôn tập: bộ đếm và tiện ích6Giao diện và lớp trừu tượng
Giới thiệu về giao diệnTriển khai giao diệnTriển khai nhiều giao diệnPhương thức mặc định và tĩnh trong giao diệnLớp trừu tượng và giao diệnGiao diện hàmÔn tập: hệ thống thanh toán9Kiểu tổng quát
Giới thiệu về kiểu tổng quátLớp tổng quátPhương thức tổng quátTham số kiểu có giới hạnKý tự đại diện (?, extends, super)Ôn tập: vùng chứa tổng quát12Mẫu thiết kế, phần 2
Mẫu CommandMẫu AdapterMẫu DecoratorMẫu Template MethodMẫu StateMẫu CompositeMẫu IteratorTự luyện tập: Trình biên dịch Java online