Tìm Hiểu Về Nguyên Lý "Composition over Inheritance"

Composition over Inheritance là một nguyên lý lập trình được sử dụng rất phổ biến tuy nhiên không phải bạn dev nào cũng hiểu rõ nguyên lý này. Vậy chính xác Composition over Inheritance là gì và cách sử dụng nó như thế nào. Chúng ta sẽ cùng nhau tìm hiểu nguyên lý Composition over Inheritance trong bài viết này sử dụng ngôn ngữ lập trình PHP.

Nguyên Lý Composition over Inheritance

Composition over Inheritance là nguyên lý cơ bản trong lập trình trong đó ưu tiên hợp đối tượng (object compostion) thay vì thừa kế lớp (class inheritance).

Để hiểu rõ hơn nguyên lý này chúng ta hãy cùng nhau tìm hiểu một ví dụ sau:

Bạn cần viết một ứng dụng game săn vịt. Để game này trở nên sinh động, bạn quyết đinh sẽ cho phép người chơi săn nhiều loại vịt khác nhau như vịt trời, vịt bầu, vịt xiên...

Kiểu này người chơi game cứ gọi là tha hồ mà săn vịt nhé. Càng nhiều giống vịt thì người chơi sẽ săn càng hăng. Phen này thì mình sắp giàu to rồi, bạn cảm thấy sung sướng khi nghĩ về điều này!!!

Với kinh nghiệm lập trình của mình, bạn nhanh chóng nhận ra rằng dù các giống vịt có khác nhau thật nhưng về căn bản chúng cũng chỉ là ... vịt. Do đó khi viết ứng dụng bạn quyết định tạo một class tên là Duck dùng như là một lớp cha để đặc trưng cho các giống vịt nói chung. Class này sẽ là abstract class và có 3 method là swim() (thực hiện hành động bơi), quack()(thực hiện hành động kêu) và fly() (thực hiện hành động bay, đối với giống vịt trời) và một method có tên là display() dùng để hiển thị vịt. Do mỗi giống vịt khác nhau sẽ được hiển thị các chi tiết như màu lông, kích cỡ... khác nhau nên method display sẽ được implement khác nhau cho từng giống vịt và bởi vậy nên trong class Duck thì method này được định nghĩa là abstract method. Code của class Duck sẽ như sau:

class Duck {
    public function swim(){ // bơi
        echo "Vịt bắt đầu bơi";
    }
    public function quack(){ // kêu
        echo "Vịt bắt đầu kêu";
    }
    public function fly() { // bay (dành cho vịt trời)
        echo "Vịt bắt đầu bay";
    }
    public abstract function display(); // hiển thị màu lông, kích cỡ... của vịt
}

Tiếp đến chúng ta sẽ có các class cho từng giống vịt cụ thế, đầu tiên là giống vịt trời (Diving Duck)

class DivingDuck extends Duck {
    public function display() {
        echo "Em là vịt trời";
    }
}

Ở trên class DivingDuck kế thừa class Duck và bởi vì giống vịt trời vừa có thể bơi, kêu và bay nên chúng ta có thể giữ nguyên cả 3 method swim(), quack()fly().

Tiếp theo là giống vịt bầu (Mallard Duck):

class MallardDuck {
    function fly() {
        return false;
    }
    public function display() {
        echo "Em là vịt bầu";
    }
}

Tương tự MallardDuck cũng kết thừa class Duck tuy nhiên bởi vì giống vịt bầu không biết bay nên chúng ta cần override lại method fly() để trả về false.

Tới đây bạn thấy thừa kế có vẻ ổn vì chúng ta chỉ cần phải override lại một method là fly trong lớp MallardDuck sau khi thừa kế lớp cha Duck. Sau một ngày viết code vất vả bạn quyết định rủ vài thằng bạn bợm nhậu đi làm vài ve vừa để giải toả căng thẳng ừa để chém gió về ứng dụng game hay ho bạn đang phát triển. Và cũng không có gì ngạc nhiên khi trong buổi nhậu mấy bợm nhậu hết lời khen ngợi bạn (một phần cũng do bạn phải bao nửa chầu nhậu để gầy được kèo nhậu đông đến khốn khổ khốn nạn này :v).

Làm một giấc ngủ so deep thật sảng khoái sau khi nhậu về, sáng hôm tỉnh dậy bạn lại tiếp tục code. Bây giờ bạn cần viết các class cho các giống vịt tiếp theo như vịt xiên, vịt siêu thịt, vịt siêu trứng... Với kinh nghiệm lập trình hướng đối tượng OOP đầy mình chỉ 1h sau bạn đã viết xong hết các class này. Tuy nhiên lúc này bạn nhận ra có quá nhiều class con (dành cho từng giống vịt) đã phải override lại method fly() và bạn nhận ra việc sử dụng thừa kế trong trường hợp này có gì đó sai sai!!!

Vấn Đề Của inheritance

Nếu học về lập trình hướng đối tượng (OOP) bạn sẽ biết rằng việc sử dụng inheritance cho phép các class con có thể thừa kế các thuộc tính và phương thức của class cha. Ngoài ra, tính đa hình (polymorphism) trong OOP cũng cho phép các class con có thể override lại các method trong class cha (như bạn áp dụng trong method fly() ở các class dành cho các giống vịt không phải là vịt trời).

Tuy nhiên hai tính chất này nếu không được áp dụng hợp lý sẽ diễn ra tình trạng như bạn gặp phải ở trên. Tất nhiên bạn có thể bỏ method fly() ở class Duck và chỉ thêm method này vào trong class MallardDuck thì khi đó các class đặc trưng cho từng giống vịt khác nhau sẽ không phải override lại method này. Okie cách làm này hoàn toàn đúng. Tuy nhiên bạn thử tưởng tượng trong một trường hợp nếu có 200 giống vịt biết bay và 300 giống vịt khác không biết bay. Lúc này bạn sẽ phải đối mặt với một trong hai vấn đề sau:

  • Nếu bạn bỏ method fly()Duck thì bạn sẽ khỏi cần implement method này trong class dành cho 300 giống vịt không biết bay nhưng ngược lại bạn lại cần phải implement method này trong class dành cho 200 giống vịt biết bay .
  • Nếu vẫn giữ method fly()Duck thì bạn sẽ không cần implement method này trong class dành cho 200 giống vịt biết bay nhưng ngược lại bạn lại phải override method này trong class dành cho 300 giống vịt không biết bay.

Một trong những cách để giải quyết cho bài toán trên đó là tạo ra hai class cha:

  • Một lớp DuckNoFly với 2 method là swim()quack()
  • Một lớp DuckCanFly với 3 method là swim(), quack()fly().

Và sau đó tuỳ vào giống vịt mà bạn sẽ extends các class cha cho phù hợp. Có vẻ ổn phải không nào?

Đợi xíu! Thử nhìn lại xem bạn đã làm gì với 2 class trên. Duplicate code (code trùng) khi mà bạn đã định nghĩa method swim()quack() ở cả 2 class DuckNoFlyDuckCanFlyphải không? Nếu như có code review thì việc bạn được technical leader gọi vào phòng riêng và nghe chửi là điều không cần bàn (no table)!

Sử Dụng Nguyên Lý "Composition over Inheritance"

Dù vấn đề trên có vẻ nan giải nhưng bởi vì bạn là một developer với kinh nghiệm đầy mình (hoàn toàn không phải mấy coder tay mơ hay hạng xoàng), vì vậy bạn quyết định áp dụng nguyên lý thần thánh học được từ môn bí kíp lập trình hồi còn là sinh viên, nguyên lý này có tên là Composition over Inheritance. Cụ thể khi áp dụng nguyên lý này nó sẽ là như zầy:

Thay vì thừa kế lớp cha Duck, qua đó sẽ tạo ra loại quan hệ is a (hay là một) giữa các class con và class cha, ví dụ như MallardDuck là một loại Duck, lúc này bạn chuyển qua sử dụng hợp đối tượng (hay object composition), qua đó sẽ tạo ra kiểu quan hệ has a hay có một trong một object, ví dụ object $mallardDucktạo bởi class MallardDucksẽ có các behaviour như swim(), quack() hay object $divingDuck sẽ có các behaviour như swim(), quack()fly(). Việc thiết lập các behaviour sẽ được tạo ra một cách linh hoạt thay vì cứng nhắc như sử dụng tính thừa kế.

Okie có vẻ bạn đang cảm thấy hơi khó hiểu phải không, bạn cũng không cần phải lo vì chúng ta sẽ hiểu rõ hơn khi đi vào code.

Chúng ta sẽ bắt với class cha Duck, lúc này Duck sẽ có chút ít thay đổi bằng việc bỏ method fly() và đồng thời thêm vào method setFlyBehavior() như sau:

abstract class Duck {
    public $flyBehaviour;
    public function setFlyBehaviour($flyBehaviourObject) {
        $this->flyBehaviour = $flyBehaviourObject;
    }
    public function quack() {
        echo "Vịt kêu quác quác";
    }
    public function swim() {
        echo "Vịt bắt đầu bơi";
    }
    public abstract function display();
}

Tiếp theo chúng ta tạo 2 class là CanFlyBehaviourNoFlyBehaviour như sau:

class CanFlyBehaviour {
    public function fly() {
        echo "Vịt bắt đầu bay";
    }
}

class NoFlyBehaviour {
    public function fly() {
        echo "Em bó tay";
    }
}

Và lúc này class MallardDuck dành cho vịt bầu sẽ đơn giản như sau:

class MallardDuck extends Duck {
    public function __construct() {
        $this->flyBehaviour = new NoFlyBehaviour();
    }
    public function display() {
        echo "Em là vịt bầu";
    }
}

Tương tự cho DivingDuck dành cho vịt trời:

class DivingDuck extends Duck {
    public function __construct() {
        $this->flyBehaviour = new CanFlyBehaviour();
    }
    public function display() {
        echo "Em là vịt bầu";
    }
}

Lúc này khi tạo một object cho giống vịt bầu từ class MallardDuck bạn có thể gọi tới method fly() từ thuộc tính $flyBehaviour như sau:

$mallardDuck = new MallardDuck();

$malllardDuck->flyBehaviour->fly(); // Em bó tay

Như vậy bằng việc không sử dụng tính thừa kế cho fly behaviour từ lớp cha Duck và thay vào đó chúng ta sẽ thiết lập behaviour này tuỳ vào từng class con. Lúc này chúng ta nói class con sở hữu thuộc tính fly behaviour thay vì thừa kế lại từ lớp cha. Đây cũng chính là nội dung của nguyên lý Composition over Inheritance trong lập trình.

Bài viết có tham khảo nội dung từ cuốn sách Head this first: Design Patterns (trong sách này sử dụng ngôn ngữ Java cho ví dụ minh hoạ).

1 Phản Hồi

Viet Nguyen

$malllardDuck->flyBehaviour->fly(); // Em bó tay

Bạn viết bài có đọc lại hay hiểu ý nghĩa ko vậy (hoặc nếu bạn chưa hoàn thành bài viết thì mình xin lỗi)? Sao lại gọi method fly từ flyBehaviour. Cái này phải triển khai trong abstract class Duck nữa chứ, và 2 cái này CanFlyBehaviour, NoFlyBehaviour lại ko implement từ interface nào thì FlyBehaviour để làm chi :v

*mà behavior nha bạn :D

Thêm bình luận
Huỷ

Thêm Phản Hồi

Bài Viết Liên Quan