+1

Lập trình hướng đối tượng với PHP (Phần 2)

Mở đầu

Trong phần 1 của series, chúng ta đã đi qua khái niệm về lập trình hướng đối tượng, các tính chất, cách thể hiện trong PHP, các khái niệm được sử dụng phổ biến. Hôm nay chúng ta sẽ tiếp tục làm rõ vài vấn đề khác như:

  • Thế nào là magic functions.
  • PSR và PSR4 là gì?
  • Tìm hiểu về các quy tắc trong PSR2
  • Splat Operator
  • Các phương pháp thiết kế hướng đối tượng (SOLID).

1. Thế nào là magic functions.

Các hàm __construct(), __destruct(), __call(), __callStatic(), __get(), __set(), __isset(), __unset(), __sleep(), __wakeup(), __toString(), __invoke(), __set_state(), __clone()__debugInfo() là các magic functions trong các PHP class. Chúng là các hàm đặc biệt được thiết kế để thực hiện một số hành động nhất định lên đôi tượng trong class đó.

Lưu ý: PHP dự trữ tất cả các hàm có tên bắt đầu bằng __ như magic functions. Nó nhắn nhủ bạn đừng sử dụng tên hàm với __ trừ khi bạn muốn tạo ra magic functions.

2. PSR và PSR4 là gì?

PSR viết tắt của PHP Standards Recommendations, được hiểu là các nguyên tắc trình bài code, tiêu chuẩn khi lập trình với ngôn ngữ PHP.

PSR bao gồm 12 phần (http://www.php-fig.org/psr/) từ PSR-1, PSR-2, PSR-3, PSR-4, PSR-6, PSR-7. Các tiêu chuẩn thành phần hoàn chỉnh của PSR đó gồm:

  • Basic Coding Standard: Tiêu chuẩn cơ bản khi viết code PHP
  • Coding Style Guide: Tiêu chuẩn trình bày code
  • Logger Interface: Giao diện logger
  • Autoloading Standard: Tiêu chuẩn về tự động nạp
  • Caching Interface: Giao diện về Caching
  • HTTP Message Interface: Tiêu chuẩn Giao diện thông điệp HTTP

Việc thống nhất chung về cách thức viết code, tổ chức các class,... sẽ giúp chúng ta dễ dàng đọc hiểu, giúp code sáng sủa và dễ bảo trì hơn.


PSR-4 là nguyên tắc mô tả đặc điểm kỹ thuật cho việc tự động nạp từ các file. Nó hoàn toàn tương thích và có thể được sử dụng ở bất kỳ việc tự động nạp nào khác, bao gồm cả PSR-0. PSR-4 cũng mô tả vị trí xếp các file để chúng sẽ được tự động nạp.

  • Lớp được hiểu là class, interface, trait, và các cấu trúc tương tự.
  • Tên định danh đầy đủ của 1 lớp như sau: \<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>
    • Tên định danh đầy đủ PHẢI có một namespace gốc (hiểu là tên vendor)
    • Tên định danh đầy đủ CÓ THỂ có một hoặc nhiều namespace con.
    • Tên định danh đầy đủ nó PHẢI có một tên lớp kết thúc (ClassName)
    • Kí tự _ không có ý nghĩa đặc biệt trong tên lớp định danh đầy đủ.
    • Kí tự chữ cái trong tên class đầy đủ CÓ THỂ là tổ hợp của kí tự thường và hoa.
    • Tất cả tên class PHẢI được tham chiếu trong một cách phù hợp.
  • Khi nạp một file thì nó phải tương ứng với một tên xác định đầy đủ của lớp.
    • Mỗi tên định danh đầy đủ phải tương ứng với một cấu trúc thư mục.
    • Tên lớp kết thúc tương ứng với tên file: <ClassName>.php.

3. Tìm hiểu về các quy tắc trong PSR2.

PSR-2 là các quy tắc kết thừa và mở rộng của các quy tắc của PSR-1.

Điểm qua các quy tắc được liệt kê trong PSR-2 gồm:

  • Code PHẢI tuân thủ PSR-1
  • Thẻ đóng ?> PHẢI bỏ với các file chỉ chứa mã nguồn PHP.
  • Code PHẢI sử dụng 4 khoảng cách để lùi dòng thay vì TABS.
  • Mỗi dòng code PHẢI dưới 120 ký tự, NÊN dưới 80 ký tự.
  • Các hằng số true, false, null PHẢI viết thường.
  • PHẢI có 1 dòng trắng sau namespace, và PHẢI có một dòng trắng sau mỗi khối code.
  • Ký tự mở lớp { PHẢI ở dòng tiếp theo sau tên class, và đóng lớp } PHẢI ở dòng tiếp theo của thân class.
  • Ký tự { cho hàm PHẢI ở dòng tiếp theo sau tên hàm, và ký tự } kết thúc hàm PHẢI ở dòng tiếp theo của thân hàm.
  • Từ khóa extends, implements PHẢI viết cùng dòng với tên class.
  • Implements nhiều lớp, thì mỗi lớp trên một dòng.
<?php
namespace Vendor\Package;

use FooClass;
use BarClass as Bar;
use OtherVendor\OtherPackage\BazClass;

class ClassName extends ParentClass implements
    \ArrayAccess,
    \Countable,
    \Serializable
{
    // constants, properties, methods
}

  • Từ khóa var KHÔNG ĐƯỢC dùng sử dụng khai báo thuộc tính.
<?php
namespace Vendor\Package;

class ClassName
{
    public $foo = null;
}

  • Tên thuộc tính KHÔNG NÊN có tiền tố _ nhằm thể hiện thuộc protect hay private.
  • Tham số cho hàm, phương thức: KHÔNG được thêm space vào trước dấu , và PHẢI có một space sau , .
<?php
namespace Vendor\Package;

class ClassName
{
    public function foo($arg1, &$arg2, $arg3 = [])
    {
        // method body
    }
}

  • Các visibility (public, private, protected) PHẢI được khai báo cho tất cả các hàm và các thuộc tính của lớp
  • abstract, final PHẢI đứng trước visibility, còn static PHẢI đứng sau.
<?php
namespace Vendor\Package;

abstract class ClassName
{
    protected static $foo;

    abstract protected function zim();

    final public static function bar()
    {
        // method body
    }
}
  • Các từ khóa điều khiển khối(if, elseif, else) PHẢI có một khoảng trống sau chúng, hàm và lớp thì KHÔNG ĐƯỢC làm như vậy. Nên sử dụng elseif thay vì else if.
<?php
if ($expr1) {
    // if body
} elseif ($expr2) {
    // elseif body
} else {
    // else body;
}

4. Splat Operator.

Chúng ta cùng xem qua 1 ví dụ chính là hàm groupBy của Eloquent:

public function groupBy(...$groups)
 {
        foreach ($groups as $group) {
            $this->groups = array_merge(
                (array) $this->groups,
                array_wrap($group)
            );
        }

        return $this;
}

Dấu ...được sử dụng ở đây được gọi là Splat Operator. Theo đó, nó đại diện cho các parameter mà ta truyền vào hàm, và đưa nó thành một cái mảng.

Ví dụ đối với code trên, khi gọi groupBy('col1', 'col2') thì cái biến $groups đó nó sẽ là một cái mảng ['col1', 'col2'].

Đến đây thì nó rõ ràng là khá hữu dụng, vì giải quyết được mấy vấn đề:

  • Dynamic parameters: Bạn có thể thoải mái về số params truyền vào, ngay ví dụ trên nó đã chứng minh điều đó
  • Với công cụ này, bạn hoàn toàn kết hợp được giữa việc truyền tham số thông thường và truyền tham số với splat operation. Ta có thể truyền tham số là một hàm, cùng với các tham số còn lại là biến. Kiểu:
function concatenate($transform, ...$strings) {
        $string = '';
        foreach($strings as $piece) {
            $string .= $piece;
        }
        return($transform($string));
    }

    echo concatenate("strtoupper", "I'd ", "like ",
        4 + 2, " apples");

Với ví dụ này ta nhận ra thêm một chú ý là chỉ có tham số cuối cùng là áp dụng Splat Operator được. Điều đó có nghĩa là function(...$b, ...$c) là không hợp lệ

5. Các phương pháp thiết kế hướng đối tượng (SOLID).

SOLID - 5 nguyên lý của thiết kế hướng đối tượng:

  • S – Single-responsiblity principle (nguyên lý đơn nhiệm)
  • O – Open-closed principle (nguyên lý mở rộng - hạn chế)
  • L – Liskov substitution principle (nguyên lý thay thế Liskov)
  • I – Interface segregation principle (nguyên lý hay phân tách interface)
  • D – Dependency Inversion Principle (nguyên lý nghịch đảo phụ thuộc)

S – Single-responsiblity principle (nguyên lý đơn nhiệm):

Một class hay method chỉ nên giữ một trách nhiệm duy nhất, thực hiện một nhiệm vụ cụ thể.

Ví dụ:

abstract class Employee
{
    abstract public function working();
}

class Developer extends Employee
{
    public function working()
    {
        // TODO
    }
}

class Manager extends Employee
{
    public function working()
    {
        // TODO
    }
}

Với cách thiết kế này thì ta đã tuân thủ nguyên tắc S trong SOLID, mỗi class Develop, Manager chỉ đảm nhiệm một nhiệm vụ duy nhất.


O – Open-closed principle (nguyên lý mở rộng - hạn chế):

Module có thể được mở rộng nhưng hạn chế sửa đổi bên trong nó.

Theo nguyên lý, một module phải đáp ứng hai điều kiện:

  • Dễ mở rộng: dễ dàng nâng cấp, mở rộng, thêm tính năng mới cho modue khi có yêu cầu.
  • Hạn chế sửa đổi: Hạn chế việc sửa mã nguồn của module sẵn.

Ví dụ:

Đặt vấn đề: Ta cần 1 lớp đảm nhận việc kết nối đến CSDL. Thiết kế ban đầu chỉ có SQL Server và MySQL. Thiết kế ban đầu có dạng như sau:

class ConnectionManager
{
    public function doConnection(Object $connection)
    {
        if($connection instanceof SqlServer) {
            //connect with SqlServer
        } elseif($connection instanceof MySql) {
            //connect with MySql
        }
    }
}

Sau đó yêu cầu đặt ra phải kết nối thêm đến Oracle và một vài hệ CSDL khác. Để thêm chức năng ta phải thêm vào code những khối esleif khác, việc này làm code cồng kềnh và khó quản lý hơn.

  • Giải pháp:
    • Áp dụng Abstract thiết kế lại các lớp SqlServer, MySql, Oracle...
    • Các lớp này đều có chung nhiệm vụ tạo kết nối đến csdl tương ứng có thể gọi chung là Connection.
    • Cách thức kết nối đến csdl thay đổi tùy thuộc vào từng loại kết nối nhưng có thể gọi chung là doConect.
    • Vậy ta có lớp cơ sở Connection có phương thức doConnect, các lớp cụ thể là SqlServer, MySql, Oracle... kế thừa từ Connection và overwrite lại phương thức doConnect phù hợp với lớp đó.

Thiết kế sau khi làm lại có dạng như sau:

abstract class Connection()
{
        public abstract function doConnect();
}

class SqlServer extends Connection
{
    public function doConnect()
    {
        //connect with SqlServer
    }
}

class MySql extends Connection
{
    public function doConnect()
    {
        //connect with MySql
    }
}

class ConnectionManager
{
    public function doConnection(Connection $connection)
    {
        //something
        //.................
        //connection
        $connection->doConnect();
    }
}

L – Liskov substitution principle (nguyên lý thay thế Liskov):

Lớp con nên ghi đè lại phương thức của lớp cha theo cách mà không phá vỡ tính đúng đắn theo quan điểm của client.

Giải thích:

  • Nội dung nguyên lý có thể hiểu như sau trong một chương trình, các object của class con có thể thay thế class cha mà không làm thay đổi tính đúng đắn của chương trình.
  • Mối quan hệ IS-A (là một) thường được dùng để xác định kế thừa. Lớp B kế thừa lớp A khi B là một A, do đó B có thể thay thay thế hoàn toàn cho A mà không làm mất đi tính đúng đắn.

Giờ ta có ví dụ sau: Giả sử, ta muốn viết một chương trình để mô tả các loài chim bay. Đại bàng, chim sẻ bay được, nhưng chim cánh cụt không bay được. Do chim cánh cụt cũng là chim, ta cho nó kế thừa class Bird. Tuy nhiên, vì cánh cụt không biết bay, khi gọi hàm bay của chim cánh cụt, ta sẽ quăng NoFlyException.

class Bird 
{
    public function fly()
    {
        return "Fly";
    }
}
class EagleBird extends Bird
{
    public function fly()
    {
        return "Eagle Fly";
    }
}
class SparrowBird extends Bird
{
    public function fly()
    {
        return "Sparrow Fly";
    }
}
class PenguinBird extends Bird
{
    public function fly()
    {
        throw new NoFlyException();
    }
}

$birds = [new Bird(), new EagleBird(), new SparrowBird(), new PenguinBird()];
foreach ($birds as $bird) {
    $bird->fly();
}

Ta tạo 1 mảng chứa các loài chim rồi duyệt các phần tử. Khi gọi hàm fly() của class "PenguinBird", hàm này sẽ quăng lỗi. Class "PenguinBird" gây lỗi khi chạy, không thay thế được class cha của nó là "Bird", do đó nó đã vi phạm LSP.


I – Interface segregation principle (nguyên lý hay phân tách interface)

Thay vì dùng 1 interface lớn, ta nên tách thành nhiều interface nhỏ, với nhiều mục đích cụ thể

Để thiết kế một hệ thống linh hoạt, dễ thay đổi, các module của hệ thống nên giao tiếp với nhau thông qua interface. Mỗi module sẽ gọi chức năng của module khác thông qua interface mà không cần quan tâm tới implementation bên dưới. Như đã nói ở trên, do interface chỉ chứa khai báo rỗng về method, khi một class implement một interface, class đó phải implement toàn bộ các method được khai báo trong interface đó.

Để hiểu rõ về nguyên lý này chúng ta xem qua ví dụ:

interface AnimalInterface
{
    public function eat();
    public function drink();
    public function sleep();
}
interface BirdInterface
{
    public function fly();
}
interface FishInterface
{
    public function swim();
}

// Các class chỉ cần kế thừa những interface có chúc năng chúng cần
class Dog implements AnimalInterface
{
   public function eat() {} 
   public function drink() {} 
   public function sleep() {} 
}
class Bird implements AnimalInterface,  BirdInterface
{
   public function eat() {} 
   public function drink() {} 
   public function sleep() {} 
   public function fly() {}
}
class Fish implements AnimalInterface,  FishInterface
{
   public function eat() {} 
   public function drink() {} 
   public function sleep() {} 
    public function swim() {}
}

Ở trên, chúng ta đã tạo ra các interface khác nhau: AnimalInterface, BirdInterface, FishInterface để đặc tả từng hành động của các loài động vật có những đặc điểm riêng biệt.


D – Dependency Inversion Principle (nguyên lý nghịch đảo phụ thuộc)

Các module cấp cao không được phụ thuộc vào các module cấp thấp, nhưng nên được trừu tượng hóa.

Các đối tượng nên được trừu tượng chứ không nên gắn chặt với nhaóa

Nguyên tắc này nói cho bạn rằng bạn không nên viết code gắn chặt với nhau bởi vì sẽ là cơn ác mộng cho việc bảo trì khi ứng dụng trở lên lớn dần. Nếu một class phụ thuộc một class khác, bạn sẽ cần phải thay đổi class đó nếu một trong những class phụ thuộc phải thay đổi. Chúng ta nên cố gắng viết các class ít phụ thuộc nhất có thể.

OK, chúng ta cùng xem ví dụ sau:

interface MessengerInterface
{
    public function sendMessage();
}
class Email implements MessengerInterface
{
    public function sendMessage()
    {
        // code to send email
    }
}

class SMS implements MessengerInterface
{
    public function sendMessage()
    {
        // code to send SMS
    }
}

class Notification
{
    private $messenger;
    public function notification(MessengerInterface $messenger)
    {
        $this->messenger = $messenger;
    }
    public function doNotify()
    {
        $this->messenger->sendMessage();
    }
}

Như vậy lớp Notification không còn phụ thuộc vào các lớp cụ thể (SMS hay Email) mà chỉ phụ thuộc vào 1 lớp trừu tượng MessengerInterface, ta có thể thêm các lớp cụ thể (SMS hay Email) mà không hề ảnh hưởng đến lớp Notification.


Kết luận

Như vậy qua bài viết, mình đã giới thiệu thêm cho các bạn về các magic functions, các nguyên tắc khi lập trình PHP, splat operator và phương pháp thiết kế hướng đối tượng SOLID.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí