Composition vs Inheritance là một trong những câu hỏi phỏng vấn thường gặp. Bạn hẳn cũng từng nghe lời khuyên nên ưu tiên dùng Composition thay vì Inheritance.

Composition vs Inheritance
Cả Composition và Inheritance đều là các khái niệm trong lập trình hướng đối tượng. Chúng không gắn chặt với một ngôn ngữ lập trình cụ thể nào như Java. Trước khi đi vào so sánh việc sử dụng Composition so với Inheritance trong code, hãy cùng điểm qua định nghĩa ngắn gọn của từng khái niệm nhanh về chúng.
Composition
Composition là một kỹ thuật thiết kế trong lập trình hướng đối tượng để triển khai mối quan hệ has-a (có-một) giữa các đối tượng. Trong Java, Composition được thực hiện bằng cách sử dụng các biến instance của những object khác. Ví dụ: một Person có một Job (a Person has a Job) có thể được triển khai như sau trong lập trình hướng đối tượng với Java.
package com.journaldev.composition;
public class Job {
// variables, methods etc.
}
package com.journaldev.composition;
public class Person {
//composition has-a relationship
private Job job;
//variables, methods, constructors etc. object-oriented
Inheritance là một kỹ thuật thiết kế trong lập trình hướng đối tượng để triển khai mối quan hệ is-a (là-một) giữa các đối tượng. Trong Java, Inheritance được thực hiện bằng cách sử dụng từ khóa extends. Ví dụ: mối quan hệ Cat is an Animal (Mèo là một Loài động vật) trong lập trình Java sẽ được triển khai như sau.
package com.journaldev.inheritance;
public class Animal {
// variables, methods etc.
}
package com.journaldev.inheritance;
public class Cat extends Animal{
}
Composition over Inheritance
Cả Composition và Inheritance đều giúp tái sử dụng code nhưng theo những cách tiếp cận khác nhau. Vậy nên chọn cách nào? Làm thế nào để so sánh giữa Composition và Inheritance? Hẳn bạn đã từng nghe lời khuyên trong lập trình là nên ưu tiên Composition hơn Inheritance. Hãy cùng xem một số lý do sẽ giúp bạn lựa chọn giữa hai kỹ thuật này.
- Inheritance tạo ra sự tightly coupled (gắn kết chặt chẽ), trong khi Composition mang tính loosely coupled (gắn kết lỏng lẻo, linh hoạt hơn). Giả sử chúng ta có các class bên dưới sử dụng Inheritance:
package com.journaldev.java.examples;
public class ClassA {
public void foo(){
}
}
class ClassB extends ClassA{
public void bar(){
}
}
Để đơn giản, chúng ta đặt cả superclass và subclass trong cùng một package. Nhưng trên thực tế, chúng thường nằm ở các cơ sở mã khác nhau. Có thể có nhiều class mở rộng từ superclass ClassA. Một ví dụ rất phổ biến cho tình huống này là việc kế thừa từ class Exception. Bây giờ, giả sử phần triển khai (implementation) của ClassA được thay đổi như sau: một method mới là bar() được thêm vào.
package com.journaldev.java.examples;
public class ClassA {
public void foo(){
}
public int bar(){
return 0;
}
}
Ngay khi bạn bắt đầu sử dụng bản triển khai mới của ClassA, bạn sẽ gặp compile-time error trong ClassB với thông báo: The return type is incompatible with ClassA.bar() (kiểu trả về không tương thích với ClassA.bar()). Giải pháp là phải thay đổi method bar() ở superclass hoặc subclass để chúng tương thích với nhau. Tuy nhiên, nếu bạn sử dụng Composition thay vì Inheritance, bạn sẽ không bao giờ gặp vấn đề này. Một ví dụ đơn giản về cách triển khai ClassB bằng Composition có thể như sau:
class ClassB{
ClassA classA = new ClassA();
public void bar(){
classA.foo();
classA.bar();
}
}
- Trong Inheritance, không có cơ chế kiểm soát truy cập, trong khi với composition, chúng ta có thể giới hạn quyền truy cập. Khi sử dụng Inheritance, tất cả các method của superclass đều được “phơi bày” cho những class khác có quyền truy cập vào subclass. Vì vậy, nếu trong superclass xuất hiện method mới hoặc có lỗ hổng bảo mật, thì subclass cũng sẽ trở nên dễ bị tấn công. Ngược lại, trong Composition, chúng ta có thể chọn những method nào sẽ được sử dụng. Điều này giúp Composition an toàn hơn so với Inheritance. Ví dụ: trong ClassB, chúng ta có thể chỉ cung cấp (expose) method foo() của ClassA cho các class khác bằng đoạn code như sau:
class ClassB {
ClassA classA = new ClassA();
public void foo(){
classA.foo();
}
public void bar(){
}
}
Đây chính là một trong những ưu điểm lớn nhất của Composition so với Inheritance.
- Composition mang lại sự linh hoạt trong việc gọi các method, điều này đặc biệt hữu ích trong trường hợp có nhiều subclass. Ví dụ: giả sử chúng ta có tình huống sử dụng Inheritance như dưới đây:
abstract class Abs {
abstract void foo();
}
public class ClassA extends Abs{
public void foo(){
}
}
class ClassB extends Abs{
public void foo(){
}
}
class Test {
ClassA a = new ClassA();
ClassB b = new ClassB();
public void test(){
a.foo();
b.foo();
}
}
Vậy nếu có thêm nhiều subclass nữa thì sao? Liệu Composition có làm code trở nên rườm rà vì phải tạo một instance riêng cho từng subclass không? Câu trả lời là không, chúng ta hoàn toàn có thể viết lại class Test như ví dụ dưới đây:
class Test {
Abs obj = null;
Test1(Abs o){
this.obj = o;
}
public void foo(){
this.obj.foo();
}
}
Điều này mang lại cho bạn sự linh hoạt để sử dụng bất kỳ subclass nào, tùy thuộc vào object được truyền vào trong constructor.
- Một lợi ích nữa của Composition so với Inheritance là về phạm vi kiểm thử. Với Composition, việc viết Unit Test trở nên dễ dàng hơn, vì chúng ta biết rõ mình đang sử dụng những method nào từ class khác. Do đó, ta có thể mock các method đó để kiểm thử. Trong khi đó, với Inheritance, ta phụ thuộc rất nhiều vào superclass và không biết chắc sẽ cần dùng đến method nào từ nó. Vì vậy, ta buộc phải kiểm tra tất cả các method của superclass. Đây là khối lượng công việc dư thừa và không cần thiết, vốn chỉ phát sinh vì sử dụng Inheritance.
Kết luận về Composition vs Inheritance. Bạn đã có đủ lý do để lựa chọn Composition thay vì Inheritance. Hãy chỉ sử dụng Inheritance khi bạn chắc chắn rằng superclass sẽ không thay đổi. Trong các trường hợp còn lại, hãy ưu tiên Composition để có được thiết kế linh hoạt, an toàn và dễ bảo trì hơn.