Đa kế thừa trong PHP có khả thi?

Cập nhật: Lượt xem: 103 [ PHP ]

Kế thừa (Inheritance) một trong 4 tính chất quan trọng trong lập trình hướng đối tượng (OOP) giúp các lớp con có thể sử dụng các thuộc tính, phương thức của lớp cha mà không cần định nghĩa lại.

Đa kế thừa trong PHP có khả thi?

I. Giới thiệu

1. Tính chất kế thừa

Kế thừa (Inheritance) một trong 4 tính chất quan trọng trong lập trình hướng đối tượng (OOP) giúp các lớp con có thể sử dụng các thuộc tính, phương thức của lớp cha mà không cần định nghĩa lại.

Tuy nhiên, với trường hợp một lớp con muốn kế thừa các tính năng từ nhiều lớp cha khác nhau (đa kế thừa) thì có khả thi không? Đối với ngôn ngữ PHP thì không hỗ trợ đa kế thừa mỗi một lớp con chỉ có thể kế thừa từ một lớp cha để tránh vấn đề "Diamond problem".

2. "Diamond problem" là gì?

Các bạn hãy xem hình sau để hình dung về vấn đề này.

inheritance

GIả sử class SHAPE có phương thức getArea()

Hai class TRIANGLE, SQUARE kế thừa class SHAPE và định nghĩa lại (override).

Lúc này class CIRCLE kế thừa 2 class TRIANGLE, SQUARE nhưng không override phương thức getArea().

Vậy lúc này class CIRCLE sẽ sử dụng phương thức getArea() của class TRIANGLE hay SQUARE?

Đây là vấn đề chúng ta gặp phải khi sử dụng đa kế thừa cho lớp. Nhưng chúng ta vẫn muốn một lớp con có thể sử dụng các phương thức của các lớp khác mà không cần định nghĩa lại thì phải làm thế nào? Hãy tham khảo 2 cách tiếp cận sau.

II. Cách sử dụng Đa kế thừa trong PHP

PHP không hỗ trợ đa kế thừa nhưng vẫn hỗ trợ một phần tính chất của đa kế thừa thông qua Interface và Trait. Lớp con có thể sử dụng các phương thức được kế thừa từ Interface và Trait nhưng phải tự định nghĩa các thuộc tính riêng của nó.

1. Sử dụng Interface

Interface được mô tả như là 1 bản thiết kế cho các class có chung cách thức hoạt động. Chỉ khai báo các phương thức chứ không định nghĩa, không có các thuộc tính cho một đối tượng cụ thể nên các class con có thể kế thừa mà không gặp phải conflict.

Ưu điểm:
  • Các class con có thể lựa chọn các phương thức phù hợp muốn kế thừa từ các Interface khác nhau.
Nhược điểm:
  • Phải định nghĩa lại các phương thức.
  • Khi Interface có các phương thức mà class con không cần nhưng vẫn phải định nghĩa lại. Lúc này, class con có thể kế thừa các phương thức khác nhau từ các Interface khác nhau. Khi kế thừa sẽ phải triển khai (Implement) và định nghĩa cho phương thức mà nó kế thừa.

Ví dụ:

interface Health
{
	public function showWeight($weight);
}

interface Action
{
	public function speak();
}

class Cat implements Health, Action
{
	public $weight;
	private $say;
	
	public function __construct()
	{
		$this->weight = 20;
		$this->say = 'meow';
	}
	
	public function showWeight($weight)
	{
		echo $weight;
	}
	
	public function speak()
	{
		echo $this->say;
	}
}

$cat = new Cat();

$cat->showWeight($cat->weight); // 20
$cat->speak(); // meow
Kết quả:
20
meow
Trường hợp nếu implements 2 Interface có cùng tên phương thức thì sao?

Ví dụ:

interface Communicate
{
	public function speak();
}

interface Action
{
	public function speak();
}

class Cat implements Communicate, Action
{
	public function speak()
	{
		echo 'meow';
	}
}

$cat = new Cat();
$cat->speak();
  • Trường hợp không chỉ rõ kiểu trả về của function

Xét tính "mềm dẻo" của PHP thì vẫn có thể được, lúc này code sẽ cho ra kết quả. Phương thức speak() của lớp Cat sẽ được xem như là đã định nghĩa 1 lần cho 2 phương thức của 2 interface mà nó kế thừa.

(PHP version 7)
Kết quả: meow
  • Trường hợp chỉ rõ kiểu trả về của function ở Interface
interface Communicate
{
	public function speak(): int;
}

interface Action
{
	public function speak(): string;
}

class Cat implements Communicate, Action
{
	public function speak()
	{
		echo 'meow';
	}
}

$cat = new Cat();
$cat->speak();

Phương thức speak() chưa chỉ rõ kiểu dữ liệu trả về nên sẽ lỗi.

FATAL ERROR Declaration of Cat::speak() must be compatible with Communicate::speak(): int on line number 21
  • Trường hợp chỉ rõ kiểu trả về của function ở Interface và ở Class
interface Communicate
{
	public function speak(): int;
}

interface Action
{
	public function speak(): string;
}

class Cat implements Communicate, Action
{
	public function speak(): int
	{
		echo 'meow';
	}
}

$cat = new Cat();
$cat->speak();

Vì buộc phải định nghĩa kiểu dữ liệu trả về giống với Interface nên nếu phương thức speak() ở class Cat chỉ định nghĩa theo kiểu :int sẽ dẫn đến lỗi chưa định nghĩa cho kiểu :string và ngược lại.

FATAL ERROR Declaration of Cat::speak(): int must be compatible with Action::speak(): string on line number 14

Như vậy, kế thừa theo kiểu Interface vẫn có hạn chế của nó, khi triển khai các bạn cần lưu ý để tránh gặp lỗi nhé.

2. Sử dụng Trait

Trait cũng tương tự như Interface nhưng Trait có các phương thức đã được xây dựng sẵn, các class con có thể kế thừa và sử dụng luôn không cần định nghĩa hoặc có thể override lại theo nhu cầu.

Ưu điểm:
  • Các class con có thể kế thừa nhiều Trait.
  • Class con có thể kế thừa phương thức mà không cần định nghĩa hoặc có thể override lại phương thức.
Nhược điểm:
  • Class con sẽ kế thừa các phương thức không cần thiết trong Trait.

Ví dụ:

trait Communicable
{
    public function sayHello()
    {
         echo "Hello World";
    }
}
trait Developable
{
    public function dev()
    {
         echo "I can code PHP";
    }
}

class Student
{
    use Communicable, Developable;
}

$student = new Student();
$student->sayHello(); // Hello World
$student->dev(); // I can code PHP

Một số lưu ý khi sử dụng Trait

1. Trait được ưu tiên hơn extends class cha.

Trường hợp class cha và Trait mà class con kế thừa có chung tên phương thức thì phương thức của Trait sẽ được ưu tiên hơn.

trait Communicable
{
    public function sayHello()
    {
         echo "Hello World";
    }
}

class Human
{
	public function sayHello()
    {
         echo "I am human";
    }
}

class Student extends Human
{
    use Communicable;
}

$student = new Student();
$student->sayHello(); // Hello World
2. Sử dụng nhiều Trait có trùng tên phương thức.

Trường hợp phát sinh này cũng giống như Interface nhưng ở Trait do đã định nghĩa sẵn phương thức nên sẽ có cách giải quyết cho vấn đề này là class con sẽ có quyền lựa chọn kế thừa phương thức nào từ Trait nào bằng từ khóa insteadof

trait A {
    public function smallTalk() {
        echo 'a';
    }
    public function bigTalk() {
        echo 'A';
    }
}

trait B {
    public function smallTalk() {
        echo 'b';
    }
    public function bigTalk() {
        echo 'B';
    }
}

trait C {
    public function smallTalk() {
        echo 'c';
    }
    public function bigTalk() {
        echo 'C';
    }
}

class Talker {
    use A, B, C {
        B::smallTalk insteadof A, C;
        A::bigTalk insteadof B, C;
        C::bigTalk as talk;
    }
}

$talker = new Talker();
$talker->smallTalk(); // b
$talker->bigTalk(); // A

Lưu ý:

Class con sẽ kế thừa tất cả các phương thức có trong Interface, Trait nên cần kế thừa có chọn lọc các Interface, Trait cần thiết. Vi vậy, nên tách nhỏ các Interface, Trait theo các chức năng riêng phù hợp.

III. Tổng quát

Qua bài viết này hy vọng các bạn sẽ hiểu hơn về Đa kế thừa trong PHP, vì sao PHP không có đa kế thừa. Bản chất khi sử dụng kế thừa từ Interface, Trait để áp dụng cho phù hợp.

Tài liệu tham khảo