Kế thừa và Thành phần (Mối quan hệ Is-a so với Has-a) trong Java

1. Khái quát chung

Tính kế thừa và thành phần - cùng với tính trừu tượng, tính đóng gói và tính đa hình - là những nền tảng của lập trình hướng đối tượng (OOP).

Trong hướng dẫn này, chúng tôi sẽ đề cập đến những điều cơ bản về sự kế thừa và thành phần, và chúng tôi sẽ tập trung mạnh mẽ vào việc phát hiện sự khác biệt giữa hai loại mối quan hệ.

2. Kiến thức cơ bản về người thừa kế

Kế thừa là một cơ chế mạnh mẽ nhưng bị lạm dụng và lạm dụng.

Nói một cách đơn giản, với tính kế thừa, một lớp cơ sở (hay còn gọi là kiểu cơ sở) xác định trạng thái và hành vi chung cho một kiểu nhất định và cho phép các lớp con (hay còn gọi là kiểu con) cung cấp các phiên bản chuyên biệt của trạng thái và hành vi đó.

Để có ý tưởng rõ ràng về cách làm việc với kế thừa, hãy tạo một ví dụ đơn giản: một lớp cơ sở Person xác định các trường và phương thức chung cho một người, trong khi các lớp con WaitressActress cung cấp các triển khai phương thức chi tiết bổ sung.

Đây là lớp Người :

public class Person { private final String name; // other fields, standard constructors, getters }

Và đây là các lớp con:

public class Waitress extends Person { public String serveStarter(String starter) { return "Serving a " + starter; } // additional methods/constructors } 
public class Actress extends Person { public String readScript(String movie) { return "Reading the script of " + movie; } // additional methods/constructors }

Ngoài ra, hãy tạo một bài kiểm tra đơn vị để xác minh rằng các trường hợp của các lớp Nhân viên phục vụNữ diễn viên cũng là các trường hợp của Người , do đó cho thấy rằng điều kiện “là một” được đáp ứng ở cấp loại:

@Test public void givenWaitressInstance_whenCheckedType_thenIsInstanceOfPerson() { assertThat(new Waitress("Mary", "[email protected]", 22)) .isInstanceOf(Person.class); } @Test public void givenActressInstance_whenCheckedType_thenIsInstanceOfPerson() { assertThat(new Actress("Susan", "[email protected]", 30)) .isInstanceOf(Person.class); }

Điều quan trọng cần nhấn mạnh ở đây là khía cạnh ngữ nghĩa của sự kế thừa . Bên cạnh việc sử dụng lại việc triển khai lớp Person , chúng tôi đã tạo ra một mối quan hệ “is-a” được xác định rõ ràng giữa kiểu cơ sở Person và các kiểu phụ là WaitressActress . Các nữ hầu bàn và nữ diễn viên thực sự là những người.

Điều này có thể khiến chúng ta đặt câu hỏi: trong những trường hợp sử dụng nào thì kế thừa là cách tiếp cận đúng đắn để thực hiện?

Nếu các kiểu con đáp ứng điều kiện “is-a” và chủ yếu cung cấp chức năng bổ sung sâu hơn trong hệ thống phân cấp các lớp, thì kế thừa là cách để đi.

Tất nhiên, việc ghi đè phương thức được phép miễn là các phương thức được ghi đè vẫn bảo toàn khả năng thay thế kiểu cơ sở / kiểu con được khuyến khích bởi Nguyên tắc thay thế Liskov.

Ngoài ra, chúng ta nên nhớ rằng các kiểu con kế thừa API của kiểu cơ sở , một số trường hợp có thể là quá mức cần thiết hoặc đơn thuần là không mong muốn.

Nếu không, chúng ta nên sử dụng bố cục thay thế.

3. Kế thừa trong các mẫu thiết kế

Mặc dù sự đồng thuận là chúng ta nên ưu tiên thành phần hơn kế thừa bất cứ khi nào có thể, có một số trường hợp sử dụng điển hình mà kế thừa có vị trí của nó.

3.1. Mẫu siêu kiểu lớp

Trong trường hợp này, chúng tôi sử dụng kế thừa để di chuyển mã chung đến một lớp cơ sở (siêu kiểu), trên cơ sở mỗi lớp .

Đây là cách triển khai cơ bản của mẫu này trong lớp miền:

public class Entity { protected long id; // setters } 
public class User extends Entity { // additional fields and methods } 

Chúng ta có thể áp dụng cách tiếp cận tương tự cho các lớp khác trong hệ thống, chẳng hạn như lớp dịch vụ và lớp bền bỉ.

3.2. Mẫu phương pháp mẫu

Trong mẫu phương thức khuôn mẫu, chúng ta có thể sử dụng một lớp cơ sở để xác định các phần bất biến của một thuật toán và sau đó triển khai các phần biến thể trong các lớp con :

public abstract class ComputerBuilder { public final Computer buildComputer() { addProcessor(); addMemory(); } public abstract void addProcessor(); public abstract void addMemory(); } 
public class StandardComputerBuilder extends ComputerBuilder { @Override public void addProcessor() { // method implementation } @Override public void addMemory() { // method implementation } }

4. Kiến thức cơ bản về thành phần

Thành phần là một cơ chế khác do OOP cung cấp để sử dụng lại việc triển khai.

Tóm lại, bố cục cho phép chúng ta mô hình hóa các đối tượng được tạo thành từ các đối tượng khác , do đó xác định mối quan hệ “có-một” giữa chúng.

Hơn nữa, bố cục là hình thức liên kết mạnh nhất , có nghĩa là (các) đối tượng được tạo thành hoặc được chứa bởi một đối tượng cũng bị phá hủy khi đối tượng đó bị phá hủy .

Để hiểu rõ hơn về cách thức hoạt động của bố cục, hãy giả sử rằng chúng ta cần làm việc với các đối tượng đại diện cho máy tính .

Máy tính bao gồm các bộ phận khác nhau, bao gồm bộ vi xử lý, bộ nhớ, card âm thanh, v.v., vì vậy chúng ta có thể lập mô hình cả máy tính và từng bộ phận của nó thành các lớp riêng lẻ.

Đây là cách triển khai đơn giản của lớp Máy tính có thể trông như thế nào :

public class Computer { private Processor processor; private Memory memory; private SoundCard soundCard; // standard getters/setters/constructors public Optional getSoundCard() { return Optional.ofNullable(soundCard); } }

Các lớp sau mô hình hóa bộ vi xử lý, bộ nhớ và card âm thanh (các giao diện được bỏ qua vì mục đích ngắn gọn):

public class StandardProcessor implements Processor { private String model; // standard getters/setters }
public class StandardMemory implements Memory { private String brand; private String size; // standard constructors, getters, toString } 
public class StandardSoundCard implements SoundCard { private String brand; // standard constructors, getters, toString } 

Thật dễ dàng để hiểu động cơ đằng sau việc thúc đẩy bố cục thay vì kế thừa. Trong mọi tình huống có thể thiết lập mối quan hệ “có-một” chính xác về mặt ngữ nghĩa giữa một lớp nhất định và những lớp khác, bố cục là lựa chọn đúng đắn để thực hiện.

Trong ví dụ trên, Máy tính đáp ứng điều kiện “has-a” với các lớp mô hình hóa các bộ phận của nó.

Cũng cần lưu ý rằng trong trường hợp này, đối tượng Máy tính được chứa có quyền sở hữu các đối tượng được chứa nếu và chỉ khi các đối tượng đó không thể được sử dụng lại trong một đối tượng Máy tính khác . Nếu họ có thể, chúng tôi sẽ sử dụng tổng hợp, thay vì thành phần, trong đó quyền sở hữu không được ngụ ý.

5. Thành phần không có trừu tượng

Ngoài ra, chúng ta có thể xác định mối quan hệ thành phần bằng cách mã hóa cứng các phần phụ thuộc của lớp Máy tính , thay vì khai báo chúng trong hàm tạo:

public class Computer { private StandardProcessor processor = new StandardProcessor("Intel I3"); private StandardMemory memory = new StandardMemory("Kingston", "1TB"); // additional fields / methods }

Tất nhiên, đây sẽ là một thiết kế cứng nhắc, được kết hợp chặt chẽ, vì chúng tôi sẽ làm cho Máy tính phụ thuộc mạnh mẽ vào các triển khai cụ thể của Bộ xử lýBộ nhớ .

Chúng tôi sẽ không tận dụng mức độ trừu tượng được cung cấp bởi các giao diện và sự chèn ép phụ thuộc.

Với thiết kế ban đầu dựa trên các giao diện, chúng tôi nhận được một thiết kế được ghép nối lỏng lẻo, cũng dễ kiểm tra hơn.

6. Kết luận

Trong bài viết này, chúng tôi đã tìm hiểu các nguyên tắc cơ bản về kế thừa và cấu tạo trong Java, đồng thời chúng tôi khám phá sâu về sự khác biệt giữa hai loại mối quan hệ (“is-a” so với “has-a”).

Như mọi khi, tất cả các mẫu mã hiển thị trong hướng dẫn này đều có sẵn trên GitHub.