Hướng dẫn polymorphism in php

Vietnamese [Tiếng Việt] translation by Brian Vu [you can also view the original English article]

Trong lập trình hướng đối tượng, đa hình là một công cụ mạnh mẽ và quan trọng. Nó có thể được dùng để tạo ra luồng có hệ thống hơn cho ứng dụng của bạn. Bài viết này sẽ mô tả khái quát khái niệm của tính đa hình, và bạn sẽ thấy nó được triển khai trong PHP dễ dàng như thế nào.

Đa hình là gì?

Đa hình [polymorphism] là 1 từ dài dành cho một khái niệm rất đơn giản.

Đa hình mô tả một mô hình của lập trình hướng đối tượng, mà ở đây các class có chức năng khác nhau nhưng chia sẻ cùng 1 interface.

Cái hay của tính đa hình là mã lệnh làm việc với nhiều class khác nhau không cần biết class nào đang được sử dụng, bởi chúng đều được sử dụng theo cùng 1 cách giống nhau.

Một ví dụ tương đồng cho tính đa hình trong thực tế, bạn có thể hình dung đó là "một cái nút". Mọi người đều biết cách sử dụng nó: đơn giản bạn chỉ cần nhấn nó. Vấn đề cái nút sẽ "thực thi" điều gì, tuy nhiên, còn phụ thuộc vào cái nó được kết nối đến và ngữ cảnh nó được sử dụng -- nhưng kết quả không ảnh hưởng tới cách sử dụng của nó. Nếu ông chủ kêu bạn nhấn một cái nút, bạn đã có đầy đủ thông tin cần thiết để thực hiện nhiệm vụ.

Trong thế giới lập trình, đa hình được sử dụng để làm cho ứng dụng trở nên mô-đun hoá và dễ mở rộng hơn. Thay vì trộn lẫn các câu lệnh điều kiện để xử lý các tiến trình khác nhau của hành động, bạn tạo ra các đối tượng có thể thay thế lẫn nhau, để phục vụ cho mục đích lựa chọn dựa trên nhu cầu. Đó là mục đích cơ bản của đa hình.

Các Interface

Một phần không thể thiếu của đa hình đó là interface. Có hai cách để định nghĩa một interface tronh PHP: inteface và class trừu tượng [abstract class]. Cả hai đều có cách sử dụng riêng của nó, và bạn có thể chọn lọc, kết hợp chúng sao cho phù hợp với hệ thống phân cấp class của bạn.

Interface

Một interface cũng tương tự như một class ngoại trừ nó không bao gồm mã lệnh xử lý. Một interface có thể định nghĩa tên phương thức và tham số, nhưng không bao gồm nội dung của phương thức đó.  Bất kỳ class nào thực thi một interface đều phải thực thi toàn bộ các phương thức được định nghĩa trong interface. Bên cạnh đó, thì một class có thể thực thi nhiều interface.

Một interface được khai báo bằng từ khoá 'interface': 

interface MyInterface {
    // methods
}

và được đính vào một class bằng cách sử dụng các từ khóa 'implements' [trường hợp dùng nhiều interface, mỗi cái sẽ được phân cách bằng dấu phẩy]:

class MyClass implements MyInterface {
    // methods
}

Các phương thức được định nghĩa trong interface cũng tương tự như trong class, tuy nhiên, chúng sẽ không có nội dung [phần nằm giữa ngoặc nhọn]:

interface MyInterface {
    public function doThis[];
    public function doThat[];
    public function setName[$name];
}

Tất cả các phương thức được định nghĩa ở đây sẽ cần được chứa [chính xác như đã mô tả] trong bất kỳ class nào thực thi interface này. [đọc comment trong mã lệnh bên dưới]

// VALID
class MyClass implements MyInterface {
    protected $name;
    public function doThis[] {
        // code that does this
    }
    public function doThat[] {
        // code that does that
    }
    public function setName[$name] {
        $this->name = $name;
    }
}

// INVALID
class MyClass implements MyInterface {
    // missing doThis[]!

    private function doThat[] {
        // this should be public!
    }
    public function setName[] {
        // missing the name argument!
    }
}

Class Trừu Tượng [Abstract Class]

Class trừu tượng là sự pha trộn giữa interface và class thông thường. Nó có thể được dùng như interface [ở hình thức các phương thức trừu tượng [abstract method]]. Class nào kế thừa một abstract class phải thực thi toàn bộ các phương thức abstract được định nghĩa trong abstract class.

Một class trừu tượng được khởi tạo tương tự như một class thông thường, nhưng chúng ta sẽ cần đặt thêm từ khoá abstract ở đầu:

abstract class MyAbstract {
    // methods
}

và được đính vào một class thông thường bằng cách sử dụng từ khoá extends:

class MyClass extends MyAbstract {
    // class methods
}

Chúng ta có thể định nghĩa các phương thức trong class trừu tượng giống như ở class thông thường, bên cạnh đó chúng ta còn có thể định nghĩa ra các phương thức abstract [dùng từ khoá 'abstract']. Về mặt hành vi, phương thức abstract cũng tương tự như phương thức của interface, bắt buộc phải thực thi ở class kế thừa.

abstract class MyAbstract {
    public $name;
    public function doThis[] {
        // do this
    }
    abstract public function doThat[];
    abstract public function setName[$name];
}

Bước 1: Xác Định Vấn Đề

Giả định rằng bạn có một class Article chịu trách nhiệm cho việc quản lý các article trong trang web của bạn. Nó chứa thông tin của một article, bao gồm tiêu đề [title], tác giả [author], ngày tháng [date], và chuyên mục [category]. Như sau:

class poly_base_Article {
    public $title;
    public $author;
    public $date;
    public $category;

    public function  __construct[$title, $author, $date, $category = 0] {
        $this->title = $title;
        $this->author = $author;
        $this->date = $date;
        $this->category = $category;
    }
}

Ghi chú: Các class trong ví dụ của bài viết này sử dụng quy ước đặt tên theo "package_component_Class" Đây là một cách phổ biến để tách rời các class thành namespace ảo nhằm tránh trùng lặp tên.

Bây giờ bạn muốn thêm một phương thức để xuất ra thông tin với nhiều đinh dạng khác nhau như XML hay JSON. Bạn có thể bị cám dỗ và thực hiện như sau:

class poly_base_Article {
    //...
    public function write[$type] {
        $ret = '';
        switch[$type] {
            case 'XML':
                $ret = '
'; $ret .= '' . $obj->title . ''; $ret .= '' . $obj->author . ''; $ret .= '' . $obj->date . ''; $ret .= '' . $obj->category . ''; $ret .= '
'; break; case 'JSON': $array = array['article' => $obj]; $ret = json_encode[$array]; break; } return $ret; } }

Đây là một kiểu mã xấu, nhưng nó vẫn chạy -- ở thời điểm hiện tại. Hãy tự hỏi bản thân xem điều gì sẽ xảy đến trong tương lai, khi chúng ta muốn thêm nhiều định dạng hơn? Bạn có thể tiếp tục chỉnh sửa class, thêm ngày càng nhiều các trường hợp, nhưng bạn chỉ đang làm mất đi bản chất của class.

Một nguyên tắc quan trọng trong OOP là một class chỉ nên thực hiện một việc, và nó nên làm điều đó thật tốt.

Với khái niệm này, các câu lệnh điều kiện nên được coi như là một cờ báo hiệu rằng class của bạn đang làm quá nhiều thứ khác nhau. Đây chính là nơi đa hình được cần đến.

Trong ví dụ của chúng ta, rõ ràng có hai tác vụ được thực hiện: quản lý các article và định dạng dữ liệu của chúng. Trong bài viết này, chúng ta sẽ di dời phần mã lệnh định dạng dữ liệu vào một nhóm class mới và khám phá xem đa hình được sử dụng dễ dàng như thế nào.

Bước 2: Định Nghĩa Interface Của Bạn

Điều đầu tiên chúng ta nên làm là định nghĩa interface. Suy nghĩ cẩn thận về interface là điều rất quan trọng bạn nên làm, bởi bất kỳ sự thay đổi nào của nó có thể sẽ yêu cầu việc thay đổi các mã lệnh thực thi nó. Trong ví dụ của này, chúng ta sẽ sử dụng một interface đơn giản để định nghĩa một phương thức:

interface poly_writer_Writer {
    public function write[poly_base_Article $obj];
}

Chúng ta định nghĩa một phương thức public write[], nhận tham số truyền vào là một đối tượng Article. Bất kỳ class nào thực thi interface Writer cũng sẽ phải có phương thức này trong nó.

Mẹo: Nếu bạn muốn giới hạn kiểu tham số truyền vào hàm hay phương thức, bạn có thể sử dụng type hints, tương tự như cách chúng ta đã làm với write[]; nó chỉ cho phép đối tượng poly_base_Article truyền vào hàm. Không may, type hints chưa thể ứng dụng cho kiểu dữ liệu trả về, bởi PHP chưa hỗ trợ điều này, do đó bạn hãy tự kiểm soát vấn đề kiểu dữ liệu đầu ra nhé.

Bước 3: Xây Dựng Class Thực Thi

Với interface đã định nghĩa ở trên, giờ là lúc để xây dựng class thực thi nó. Trong ví dụ, chúng ta có hai kiểu định dạng muốn xuất ra. Do đó chúng ta sẽ có 2 class Writer: XMLWriter và JSONWriter. Dựa vào 2 class này, chúng ta sẽ lấy ra dữ liệu từ đối tượng Article và định dạng nó.

Class XMLWriter trông sẽ như sau:

class poly_writer_XMLWriter implements poly_writer_Writer {
    public function write[poly_base_Article $obj] {
        $ret = '
'; $ret .= '' . $obj->title . ''; $ret .= '' . $obj->author . ''; $ret .= '' . $obj->date . ''; $ret .= '' . $obj->category . ''; $ret .= '
'; return $ret; } }

Bạn có thể thấy, chúng sử dụng từ khoá implements để thực thi interface của chúng ta. Phương thức write[] chứa mã xử lý cụ thể việc định dạng dữ liệu thành kiiểu XML.

Nào, bây giờ sẽ tới class JSONWriter:

class poly_writer_JSONWriter implements poly_writer_Writer {
    public function write[poly_base_Article $obj] {
        $array = array['article' => $obj];
        return json_encode[$array];
    }
}

Các mã lệnh xử lý cho từng định dạng, giờ đây đã được chứa trong từng class riêng biệt. Mỗi class sẽ chịu trách nhiệm xử lý duy nhất một định dạng cụ thể, và chỉ có vậy. Không thành phần nào khác trong ứng dụng của bạn cần quan tâm về cách hoạt động của chúng mà vẫn có thể sử dụng chúng, cám ơn interface của chúng ta.

Bước 4: Sử Dụng Đối Tượng Thực Thi

Với những class mới đã được định nghĩa, giờ là thời điểm để chúng ta trở lại class Article. Toàn bộ mã lệnh thực thi ban đầu của phương thức write[] đã được viết lại trong các class mới. Mọi việc cần làm cho phương thức này của Article bây giờ, là sử dụng những class mới đã được tạo ra, như sau:

class poly_base_Article {
    //...
    public function write[poly_writer_Writer $writer] {
        return $writer->write[$this];
    }
}

Toàn bộ quy trình thực thi của phương thức này là ép một tham số truyền vào phải là đối tượng của class Writer [những đối tượng được khởi tạo từ bất kỳ class nào thực thi interface Writer], gọi phương thức write[], truyền vào nó đối tượng nội tại class [$this], sau đó chuyển tiếp giá trị được trả về thẳng tới người dùng. Nó không còn cần phải lo lắng về chi tiết của việc thực thi định dạng dữ liệu, giờ đây, nó có thể tập trung vào nhiệm vụ chính của mình.

Lấy Một Writer

Bạn có thể tự hỏi đâu là nơi mà bạn lấy một đối tượng Writer để bắt đầu, vì bạn cần phải truyền một trong số chúng tới phương thức này. Điều này tùy thuộc vào bạn, có rất nhiều chiến lược để thực hiện nó. Ví dụ, bạn có thể sử dụng một factory class để nhận dữ liệu request và khởi tạo một đối tượng:

class poly_base_Factory {
    public static function getWriter[] {
        // grab request variable
        $format = $_REQUEST['format'];
        // construct our class name and check its existence
        $class = 'poly_writer_' . $format . 'Writer';
        if[class_exists[$class]] {
            // return a new Writer object
            return new $class[];
        }
        // otherwise we fail
        throw new Exception['Unsupported format'];
    }
}

Như tôi đã nói, có nhiều chiến lược khác nhau để sử dụng, tuỳ vào nhu cầu của bạn. Trong ví dụ này, một biến request chọn kiểu định dạng dữ liệu để sử dụng. Nó xây dựng một tên class từ biến request, kiểm tra sự tồn tại của class dựa trên tên của nó, trường hợp nó tồn tại thì trả về một đối tượng Writer mới. Nếu không có class nào tồn tại dưới cái tên được tạo ra, một exception sẽ được ném về cho người dùng để thông báo với họ điều gì đang xảy ra.

Bước 5: Tập Hợp Mọi Thứ Lại Cùng Nhau

Bước cuối cùng, sau khi đã có đủ các thành phần, chúng ta sẽ tập hợp chúng lại cùng nhau:

$article = new poly_base_Article['Polymorphism', 'Steve', time[], 0];

try {
    $writer = poly_base_Factory::getWriter[];
}
catch [Exception $e] {
    $writer = new poly_writer_XMLWriter[];
}

echo $article->write[$writer];

Đầu tiên, chúng ta tạo ra một đối tượng Article để làm việc. Sau đó, chúng ta cố gắng lấy một đối tượng Writer từ Factory Class, trong trường hợp thất bại và một exception được ném ra, chúng ta sẽ khởi tạo lại một đối tương tượng mặc định [XMLWriter]. Cuối cùng, chúng ta truyền đối tượng Writer tới phương thức write[] của đối tượng Article, và xuất ra kết quả.

Kết Luận

Trong bài hướng dẫn này, tôi đã giới thiệu tới bạn về tính đa hình và giải thích về các interface trong PHP. Hy vọng bạn sẽ nhận ra rằng tôi mới chỉ thể hiện cho bạn thấy sức mạnh của đa hình ở một trường hợp. Còn rất, rất nhiều cách để ứng dụng nó. Đa hình là một phương pháp chuẩn mực để cách ly mã xấu ra khỏi mã lập trình hướng đối tượng của bạn. Nó tuân thủ nguyên tắc giữ các thành phần tách bạch, và là một phần không thể thiếu của nhiều mô hình thiết kế [design pattern]. Nếu bạn có bất kỳ câu hỏi nào, đừng ngại để lại comment nhé!

Chủ Đề