Sử dụng traits trong PHP

Giảm thiểu sao chép mã thông qua tổ chức tốt hơn và tái sử dụng mã là mục tiêu quan trọng của lập trình hướng đối tượng.

Nhưng trong PHP đôi khi bạn có thể gặp khó khăn vì những hạn chế của mô hình đơn kế thừa mà nó sử dụng; bạn có một số phương thức mà bạn muốn sử dụng trong nhiều lớp nhưng chúng không thể kế thừa từ nhiều lớp.

Các ngôn ngữ như C++Python cho phép chúng ta kế thừa từ nhiều lớp để giải quyết vấn đề này ở một mức độ nào đó và mixins trong Ruby cho phép chúng ta trộn chức năng của một hoặc nhiều lớp mà không cần sử dụng kế thừa.

Nhưng đa kế thừa có nhiều vấn đề chẳng hạn như Diamond Problem và mixins có thể là một cơ chế quá phức tạp để giải quyết vấn đề.

Trong bài viết này chúng ta sẽ thảo luận về traits, một tính năng mới được giới thiệu trong PHP 5.4 để khắc phục các vấn đề như vậy. Bạn có thể xem giới thiệu về traits trong PHP ở bài viết này:

Traits trong PHP | Comdy
Traits trong PHP là gì? Traits trong PHP được sử dụng để làm gì? Cách khai báo và sử dụng traits trong PHP.

Khái niệm về traits tự nó không có gì mới đối với lập trình và được sử dụng trong các ngôn ngữ khác như Scala và Perl. Chúng cho phép chúng ta sử dụng lại mã theo chiều ngang trên các lớp độc lập trong các phân cấp lớp khác nhau.

Traits là gì?

Traits tương tự như một lớp trừu tượng không thể tự khởi tạo (mặc dù nó thường được so sánh với một interface). Tài liệu PHP định nghĩa traits như sau:

Traits là một cơ chế để tái sử dụng mã trong các ngôn ngữ đơn kế thừa như PHP. Traits nhằm giảm một số hạn chế của đơn kế thừa bằng cách cho phép nhà phát triển sử dụng lại các tập phương thức một cách tự do trong một số lớp độc lập ở trong các hệ thống phân cấp lớp khác nhau.

Hãy xem xét ví dụ này:

<?php
    class DbReader extends Mysqli
    {
    }

    class FileReader extends SplFileObject
    {
    }
?>

Sẽ có vấn đề nếu cả hai lớp trên đều cần một số chức năng chung, ví dụ như làm cho cả hai đều là Singleton.

Vì PHP không hỗ trợ đa kế thừa, mỗi lớp sẽ phải triển khai mã cần thiết để hỗ trợ mẫu Singleton hoặc sẽ có một hệ thống phân cấp thừa kế không có ý nghĩa.

Traits cung cấp một giải pháp chính xác cho loại vấn đề này.

<?php
trait Singleton
{
    private static $instance;

    public static function getInstance() {
        if (!(self::$instance instanceof self)) {
            self::$instance = new self;
        }
        return self::$instance;
    }
}

class DbReader extends ArrayObject
{
    use Singleton;
}

class  FileReader
{
    use Singleton;
}
?>

Traits Singleton này có một triển khai rõ ràng của mẫu Singleton với một phương thức tĩnh getInstance() tạo ra một đối tượng của lớp bằng cách sử dụng trait này (nếu nó chưa được tạo) và trả về nó.

Hãy thử tạo các đối tượng của các lớp này bằng phương thức getInstance() này.

<?php
$a = DbReader::getInstance();
$b = FileReader::getInstance();
var_dump($a);
var_dump($b);
?>

Đây là kết quả:

object(DbReader)#1 (0) {
}
object(FileReader)#2 (0) {
}

Chúng ta có thể thấy $a là một đối tượng của DbReader$b là một đối tượng của FileReader, nhưng cả hai hiện đang hành xử như là những Singleton. Phương thức từ traits Singleton đã được inject theo chiều ngang cho các lớp sử dụng nó.

Traits không áp đặt bất kỳ ngữ nghĩa bổ sung nào trên lớp. Theo một cách nào đó, bạn có thể nghĩ về nó như là một cơ chế sao chép và dán hỗ trợ trình biên dịch trong đó các phương thức của traits được sao chép vào lớp chứa nó.

Nếu chúng ta chỉ đơn giản là tạo lớp con DbReader từ lớp cha có thuộc tính private $instance, thuộc tính đó sẽ không được hiển thị trong kết xuất của ReflectionClass::export(). Nhưng với traits thì có!

Class [  class FileReader ] {
  @@ /home/comdy/workplace/php54/index.php 19-22

  - Constants [0] {
  }
  - Static properties [1] {
    Property [ private static $_instance ]
  }
  - Static methods [1] {
    Method [  static public method instance ] {
      @@ /home/comdy/workplace/php54/index.php 6 - 11
    }
  }
  - Properties [0] {
  }
  - Methods [0] {
  }
}

Nhiều traits

Cho đến nay chúng ta chỉ sử dụng một traits trong một lớp, nhưng trong một vài trường hợp, chúng ta có thể cần kết hợp chức năng của nhiều traits.

<?php
trait Hello
{
    function sayHello() {
        echo "Hello";
    }
}

trait World
{
    function sayWorld() {
        echo "World";
    }
}

class MyWorld
{
    use Hello, World;
}

$world = new MyWorld();
echo $world->sayHello() . " " . $world->sayWorld();
?>

Đây là kết quả:

Hello World

Ở đây chúng tôi có hai traits là HelloWorld. Traits Hello chỉ có thể nói "Hello" và traits World chỉ có thể nói "World".

Trong lớp MyWorld chúng tôi đã sử dụng cả hai traits HelloWorld, do đó đối tượng của MyWorld sẽ có các phương thức từ cả hai traits này và có thể nói "Hello World".

Traits lồng nhau

Khi ứng dụng lớn lên, rất có thể chúng ta sẽ có một tập hợp các traits được sử dụng trên các lớp khác nhau.

PHP 5.4 cho phép chúng ta có các traits lồng nhau để chúng ta có thể thêm chỉ một traits thay vì nhiều traits vào trong tất cả các lớp này.

Điều này cho phép chúng ta viết lại ví dụ trước như sau:

<?php
trait HelloWorld
{
    use Hello, World;
}

class MyWorld
{
    use HelloWorld;
}

$world = new MyWorld();
echo $world->sayHello() . " " . $world->sayWorld();
?>

Đây là kết quả:

Hello World

Ở đây chúng tôi đã tạo ra traits HelloWorld sử dụng các traits Hello và World, và đưa nó vào lớp MyWorld.

Vì traits HelloWorld có các phương thức từ hai traits khác, nên nó giống hệt như chúng ta sử dụng hai traits trong lớp.

Thứ tự ưu tiên

Như tôi đã đề cập, traits hoạt động như thể các phương thức của chúng đã được sao chép và dán vào các lớp sử dụng chúng và chúng hoàn toàn được làm phẳng theo định nghĩa của các lớp.

Có thể có các phương thức có cùng tên trong các traits khác nhau hoặc trong chính lớp đó. Bạn có thể tự hỏi cái nào sẽ có hiệu lực trong đối tượng của lớp con.

Thứ tự ưu tiên là:

  1. Các phương thức của traits ghi đè các phương thức được kế thừa từ lớp cha.
  2. Các phương thức được định nghĩa trong lớp hiện tại ghi đè các phương thức từ traits.

Điều này được làm rõ trong ví dụ sau:

<?php
trait Hello
{
    function sayHello() {
        return "Hello";
    }

    function sayWorld() {
        return "Trait World";
    }

    function sayHelloWorld() {
        echo $this->sayHello() . " " . $this->sayWorld();
    }

    function sayBaseWorld() {
        echo $this->sayHello() . " " . parent::sayWorld();
    }
}

class Base
{
    function sayWorld(){
        return "Base World";
    }
}

class HelloWorld extends Base
{
    use Hello;
    function sayWorld() {
        return "World";
    }
}

$h =  new HelloWorld();
$h->sayHelloWorld();
echo "<br />";
$h->sayBaseWorld();
?>

Đây là kết quả:

Hello World
Hello Base World

Chúng ta có lớp HelloWorld kế thừa từ lớp Base và cả hai lớp có một phương thức tên là sayWorld() nhưng với các cách triển khai khác nhau. Ngoài ra, chúng tôi cũng đã thêm trait Hello vào trong lớp HelloWorld.

Chúng tôi có hai phương thức là sayHelloWorld()sayBaseWorld() cùng gọi phương thức sayWorld() tồn tại trong cả hai lớp cũng như trong traits.

Nhưng trong đầu ra, chúng ta có thể thấy phương thức sayWorld() của lớp con đã được gọi. Nếu muốn tham chiếu tới phương thức của lớp cha, chúng ta có thể sử dụng từ khóa parent như được trình bày trong phương thức sayBaseWorld().

Giải quyết xung đột và bí danh

Khi sử dụng nhiều traits có thể xảy ra tình huống trong đó các traits khác nhau sử dụng cùng tên phương thức.

Ví dụ: PHP sẽ đưa ra một lỗi nghiêm trọng nếu bạn cố chạy mã sau đây vì xung đột tên các phương thức:

<?php
trait Game
{
    function play() {
        echo "Playing a game";
    }
}

trait Music
{
    function play() {
        echo "Playing music";
    }
}

class Player
{
    use Game, Music;
}

$player = new Player();
$player->play();
?>

Những xung đột như vậy không được giải quyết tự động cho bạn. Thay vào đó, bạn phải chọn phương thức nào sẽ được sử dụng bằng từ khóa insteadof.

<?php
class Player
{
    use Game, Music {
        Music::play insteadof Game;
    }
}

$player = new Player();
$player->play();
?>

Đây là kết quả:

Playing music

Ở đây chúng tôi đã chọn sử dụng phương thức play() của traits Music để lớp Player sẽ phát nhạc chứ không phải trò chơi.

Trong ví dụ trên, một trong hai phương thức của hai traits đã được chọn.

Nhưng có một số trường hợp bạn muốn giữ cả hai phương thức mà vẫn tránh được xung đột. Trong trường hợp này bạn có thể sử dụng một tên mới (bí danh) cho một phương thức của traits.

Một bí danh không làm thay đổi tên của phương thức, nhưng nó cung cấp một tên thay thế để gọi phương thức đó. Bí danh được tạo bằng cách sử dụng từ khóa as.

<?php
class Player
{
    use Game, Music {
        Game::play as gamePlay;
        Music::play insteadof Game;
    }
}

$player = new Player();
$player->play();
echo "<br />";
$player->gamePlay();
?>

Đây là kết quả:

Playing music
Playing a game

Bây giờ bất kỳ đối tượng nào của lớp Player cũng sẽ có một phương thức gamePlay(), nó trương tự như Game::play().

Reflection

Reflection API là một trong những tính năng mạnh mẽ của PHP để phân tích cấu trúc bên trong của các interface, lớp và phương thức và dịch ngược chúng.

Và vì chúng ta đang nói về traits, bạn có thể muốn biết về các hỗ trợ của Reflection API  cho traits. Trong PHP 5.4, bốn phương thức đã được thêm vào ReflectionClass để lấy thông tin traits trong một lớp.

Chúng ta có thể sử dụng ReflectionClass::getTraits() để có được một mảng của tất cả các traits được sử dụng trong một lớp.

Phương thức ReflectionClass::getTraitNames() trả về một mảng các tên traits trong lớp đó.

Phương thức ReflectionClass::isTrait() được sử dụng để kiểm tra một cái gì đó có phải là traits hay không.

Trong phần trước chúng ta đã thảo luận về việc sử dụng bí danh cho các traits để tránh xung đột do các traits có cùng tên.

Phương thức ReflectionClass::getTraitAliases() sẽ trả về một mảng các bí danh của traits được ánh xạ tới tên ban đầu của nó.

Các tính năng khác

Ngoài những điều đã đề cập ở trên, còn có những đặc điểm khác khiến traits trở nên thú vị hơn.

Chúng ta biết rằng trong kế thừa cổ điển, các thuộc tính riêng của một lớp không thể được truy cập bởi các lớp con.

Tuy nhiên traits có thể truy cập các thuộc tính hoặc phương thức riêng của các lớp sử dụng nó và ngược lại! Đây là một ví dụ:

<?php
trait Message
{
    function alert() {
        echo $this->message;
    }
}

class Messenger
{
    use Message;
    private $message = "This is a message";
}

$messenger = new Messenger;
$messenger->alert();
?>

Đây là kết quả:

This is a message

Vì traits được làm phẳng hoàn toàn vào lớp sử dụng chúng, bất kỳ thuộc tính hoặc phương thức nào của traits sẽ trở thành một phần của lớp đó và chúng tôi truy cập chúng giống như bất kỳ thuộc tính hoặc phương thức khác của lớp.

Chúng ta thậm chí có thể có các phương thức trừu tượng trong traits để bắt các lớp sử dụng nó phải ghi đè các phương thức này. Ví dụ:

<?php
trait Message
{
    private $message;

    function alert() {
        $this->define();
        echo $this->message;
    }
    abstract function define();
}

class Messenger
{
    use Message;
    function define() {
        $this->message = "Custom Message";
    }
}

$messenger = new Messenger;
$messenger->alert();
?>

Đây là kết quả:

Custom Message

Ở đây chúng tôi có một traits tên là Message với một phương thức trừu tượng tên là define(). Nó yêu cầu tất cả các lớp sử dụng traits này phải ghi đè phương thức này.

Mặt khác, PHP sẽ đưa ra một lỗi cho biết có một phương thức trừu tượng chưa được ghi đè.

Không giống như traits trong Scala, traits trong PHP có thể có một hàm khởi tạo nhưng nó phải được khai báo public (một lỗi sẽ được ném nếu là private hoặc protected).

Dù sao đi nữa, bạn hãy thận trọng khi sử dụng các hàm khởi tạo trong traits, bởi vì nó có thể dẫn đến các xung đột ngoài ý muốn trong các lớp sử dụng nó.

Tóm lược

Traits là một trong những tính năng mạnh nhất được giới thiệu trong PHP 5.4 và tôi đã thảo luận gần như tất cả các tính năng của chúng trong bài viết này.

Chúng cho phép các lập trình viên sử dụng lại các đoạn mã theo chiều ngang trên nhiều lớp mà không phải nằm trong cùng một hệ thống phân cấp thừa kế.

Thay vì có ngữ nghĩa phức tạp, chúng cung cấp cho chúng ta cơ chế đơn giản để tái sử dụng mã.

Mặc dù traits có một số nhược điểm nhưng chắc chắn chúng có thể giúp cải thiện thiết kế ứng dụng của bạn, loại bỏ sao chép mã và làm cho nó trở nên DRY (Don't Repeat Yourself) hơn.



Bài viết liên quan:

Hướng dẫn lập trình PHP toàn tập sẽ giúp bạn từng bước tìm hiểu và nắm vững ngôn ngữ lập trình PHP.

Hướng dẫn cách truy xuất, lọc, sắp xếp dữ liệu MySQL trong PHP sử dụng MySQLi và PDO.

MySQL prepared statements trong PHP rất hữu ích để chống lại các cuộc tấn công SQL Injection.