SSV
+13

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

Xin chào tất cả mọi người. Hôm nay mình quay lại loạt bài về Lập trình hướng đối tượng trong PHP. Trong hai phần trước, mình đã đề cập đến rất nhiều vấn đề quan trọng của Lập trình hướng đối tượng trong PHP. Trong phần cuối này, mình sẽ chia sẻ về 3 vấn đề cuối cùng trong loạt bài viết, đó là: Các phương pháp thiết kế hướng đối tượng (SOLID)

Mục lục

Phần 1

  • Các đặc điểm cơ bản của lập trình hướng đối tượng. Chúng được thể hiện như thế nào trong PHP
  • Sự khác biệt giữa Abstract Class và Interface.
  • Thế nào là một hàm static. Phân biệt cách dùng từ khoá static::method() với self::method()

Phần 2

  • Thế nào là Trait
  • Thế nào là Namespaces
  • Thế nào là magic functions
  • Tìm hiểu về các quy tắc trong PSR-2

Phần 3

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

Nội dung

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

SOLID là gì? SOLID dịch ra là cứng. Nghe vẫn chả liên quan gì lắm đúng không =))). Nhưng sau đây khi mình tách nó ra và phân tích thì sẽ thấy nó rất quan trọng và không thể thiếu được trên con đường trở thành một lập trình viên "cứng". Nguyên lý đầu tiên:

S : Single responsibility principle

Nội dung của nguyên lý này là :

Một class chỉ nên giữ 1 trách nhiệm duy nhất (Chỉ có thể sửa đổi class với 1 lý do duy nhất)

Bạn nhìn thấy bộ dao kia chứ. Tiện lợi nhỉ. Ta có thể cắt với nhiều loại hình vật khác nhau. Nhưng... Nếu nó hỏng đúng một con dao mà ta cần thì sao. Ta sẽ phải tháo cả bộ ra để sửa. Điều này rất phức tạp, có thể ảnh hưởng tới nhiều bộ phận khác nhau. Trong IT cũng vậy, việc các requirement rất hay thay đổi, dẫn tới sự thay đổi code. Nếu một class có quá nhiều chức năng, quá cồng kềnh, việc thay đổi code sẽ rất khó khăn, mất nhiều thời gian, còn dễ gây ảnh hưởng tới các module đang hoạt động khác Ta sẽ làm một ví dụ về sự vi phạm nguyên lý này.

  public class ReportManager()
  {
     public void ReadDataFromDB();
     public void ProcessData();
     public void PrintReport();
  }

Class này đang chịu trách nhiệm cho 3 chức năng đó là ReadDataFromDB, ProcessData, PrintReport. Điều này vi phạm nguyên lý đầu tiên của chúng ta. Ta cần phải tách class này ra thành 3 class khác. Tuy là số class tăng lên nhưng việc bảo trì sẽ đơn giản và dễ dàng hơn. Giống như hình ảnh về bộ dao ở trên. Khi ta tách nhỏ các con dao theo mỗi chức năng riêng của nó, thay vì việc sửa một con dao liên quan đến cả bộ, ta sẽ chỉ sửa một mình nó và không làm ảnh hưởng đến các con dao khác. 😄 Nguyên lý thứ 2:

O : Open/Closed principle

Nội dung của nguyên lý này:

Có thể thoải mái mở rộng 1 class, nhưng không được sửa đổi bên trong class đó (open for extension but closed for modification)

Theo nguyên lý này, mỗi khi ta muốn thêm chức năng.. cho chương trình, chúng ta nên viết class mới mở rộng class cũ ( bằng cách kế thừa hoặc sở hữu class cũ) không nên sửa đổi class cũ.

Thử hình dung rằng “tiện nghi sống” của bạn đang là 1 căn nhà, bây giờ bạn muốn có thêm 1 tính năng là “hồ bơi” để thư giãn. Bạn có 2 cách để làm điều này: Cách 1: thay đổi hiện trạng của căn nhà, xây thêm 1 tầng nữa để làm hồ bơi. Cách 2: không làm thay đổi căn nhà đang có, mua thêm 1 mảnh đất cạnh nhà bạn và xây hồ bơi ở đó. Mặc dù cả 2 cách đều giải quyết được vấn đề nhưng cách đầu tiên có vẻ rất thiếu tự nhiên và kì cục. Cách này làm thay đổi hiện trạng của căn nhà, và nếu không cẩn thận có thể làm hư luôn những thứ đang có. Cách thứ 2 an toàn hơn rất nhiều và đáp ứng tốt được nhu cầu muốn có hồ bơi của bạn. Nguyên tắc này có ý rằng: không được thay đổi hiện trạng của các lớp có sẵn, nếu muốn thêm tính năng mới, thì hãy mở rộng bằng cách kế thừa để xây dựng class mới. Làm như vậy sẽ tránh được các tình huống làm hỏng tính ổn định của chương trình đang có. Ví dụ:

<?php
// Open Closed Principle Violation
class Programmer
{
    public function code()
    {
        return 'coding';
    }
}
class Tester
{
    public function test()
    {
        return 'testing';
    }
}
class ProjectManagement
{
    public function process($member)
    {
        if ($member instanceof Programmer) {
            $member->code();
        } elseif ($member instanceof Tester) {
            $member->test();
        };
        throw new Exception('Invalid input member');
    }
}

Nhìn vào đây các bạn sẽ thấy code này chạy đúng. Nhưng mấu chốt là ở đoạn hàm xử lý ifelsekia. Nếu$member` thuộc một class khác thì sao. Ta sẽ phải xử lý thêm ở trong function process và như thế, mọi thứ càng ngày sẽ càng cồng kềnh. Vì thế, cách giải quyết là tạo một interface, mình sẽ gọi là Workable, sau đó chúng ta sẽ dùng 2 class TesterProgrammer implements interface này.

interface Workable
{
    public function work();
}
class Programmer implements Workable
{
    public function work()
    {
        return 'coding';
    }
}
class Tester implements Workable
{
    public function work()
    {
        return 'testing';
    }
}
class ProjectManagement
{
    public function process(Workable $member)
    {
        return $member->work();
    }
}

Nhìn xem, bây giờ nếu bạn muốn mở rộng class ProjectManagement, bạn chỉ việc tạo thêm các class khác implements từ class Workable mà không cần phải xử lý if else như trên nữa. Cảm giác mọi thứ đã mượt mà hơn rất nhiều 😄. Nguyên lý thứ 3:

L: Liskov substitution principle

Nội dung :

Các đối tượng kiểu class con có thể thay thế các đối tượng kiểu class cha mà không gây ra lỗi.

Ví dụ :

Giả sử có công ty sẽ điểm danh vào mỗi buổi sáng, và chỉ có các nhân viên thuộc biên chế chính thức mới được phép điểm danh. Ta bổ sung phương thức “checkAttendance()” vào lớp Employee. Hình dung có một trường hợp sau: công ty thuê một nhân viên lao công để làm vệ sinh văn phòng, mặc dù là một người làm việc cho công ty nhưng do không được cấp số ID nên không được xem là một nhân viên bình thường, mà chỉ là một nhân viên thời vụ, do đó sẽ không được điểm danh. Hình ảnh này mô tả một sự vi phạm đến nguyên lý thứ 3. Nếu chúng ta tạo ra một lớp cleanerStaff kế thừa từ lớp Employee, và implement hàm “working()” cho lớp này, thì mọi thứ đều ổn, tuy nhiên lớp này cũng vẫn sẽ có hàm “checkAttendance()” để điểm danh, mà như thế là sai quy định dẫn đến chương trình bị lỗi. Như vậy, thiết kế lớp cleanerStaff kế thừa từ lớp Employee là không được phép. Có nhiều cách để giải quyết tình huống này ví dụ như tách hàm checkAttendance() ra một interface riêng và chỉ cho các lớp Developer, Tester và Salesman implements.

Ví dụ: Chúng ta tạo một class Rectangle dưới đây:

class Rectangle {
    public $width;
    public $height;
    
    public function setWidth($width) {
        $this->width = $width;
    }
    public function setHeight($height) {
        $this->height= $height;
    }
    public function area() {
        return $this->width * $this->height;
    }
}

Như chúng ta biết, hình vuông là một hình chữ nhật có chiều dài bằng chiều rộng, nên ta sẽ kế thừa từ hình chữ nhật:

class Square extends Rectangle { 
    public function setWidth($width) {
        $this->width = $width;
        $this->height = $width;
    }
    public function setHeight($height) {
        $this->width = $height;
        $this->height = $height;
    }    
}

Tính diện tích:

$rect = new Rectangle();
$rect->setWidth(10);
$rect->setHeight(20);
echo $rect->area(); // Kết quả là 10 * 20

$square = new Square(); 
$square->setWidth(10);
$square->setHeight(20);
echo $square->area(); // Kết quả 20 * 20, như vậy class Square đã sửa định nghĩa class cha Rectangle

Ở đây, sau khi $square->setHeight có giá trị. Lập tức nó sẽ set giá trị cho width cũng bằng giá trị của height. Tại sao lại như vậy?

Trong hình học, hình vuông là hình chữ nhật, nó là trường hợp đặc biệt của hình chữ nhật. Các phương thức setWidth và setHeight trong Rectangle, nó đúng trong Rectangle nhưng nếu tham chiếu sang Square, hai phương thức này không có ý nghĩa bởi nó được sử dụng để thiết lập cho một đối tượng khác không phải là hình vuông. Trong trường hợp này, Square không tuân theo nguyên lý thay thế Liskov và sự trìu tượng trong kế thừa từ Rectangle là không ổn.

Nguyên lý thứ 4 :

I : Interface segregation principle

Nội dung :

Một class không nên thực hiện một interface mà nó không dùng đến hoặc không nên phụ thuộc vào một phương thức mà nó không sử dụng. Để làm điều này, thay vì một interface lớn bao trùm chúng ta tách thành nhiều interface khác nhau.

Như các bạn đã biết. Một class implements từ một interface sẽ phải thực hiện việc override lại tất cả các phương thức của interface này. Một interface thì có thể có nhiều class implements , và có thể có những phương thức trong interface mà class này không dùng đến. Điều này dẫn dến sự dư thừa và không tối ưu. Cùng xem ví dụ sau:

<?php
interface Workable
{
    public function canCode();
    public function code();
    public function test();
}
class Programmer implements Workable
{
    public function canCode()
    {
        return true;
    }
    public function code()
    {
        return 'coding';
    }
    public function test()
    {
        return 'testing in localhost';
    }
}
class Tester implements Workable
{
    public function canCode()
    {
        return false;
    }
    public function code()
    {
         throw new Exception('Opps! I can not code');
    }
    public function test()
    {
        return 'testing in test server';
    }
}
class ProjectManagement
{
    public function processCode(Workable $member)
    {
        if ($member->canCode()) {
            $member->code();
        }
    }
}

Sự dư thừa đã hiện lên, và chúng ta sẽ tối ưu lại bằng cách tách interface tổng thành các interface nhỏ hơn:

interface Codeable
{
    public function code();
}
interface Testable
{
    public function test();
}
class Programmer implements Codeable, Testable
{
    public function code()
    {
        return 'coding';
    }
    public function test()
    {
        return 'testing in localhost';
    }
}
class Tester implements Testable
{
    public function test()
    {
        return 'testing in test server';
    }
}
class ProjectManagement
{
    public function processCode(Codeable $member)
    {
        $member->code();
    }
}

Nhìn quả thật mọi thứ đã trở nên tối ưu hơn rất nhiều phải không 😄 Nguyên lý thứ 5:

D : Dependency Inversion Principle

Nội dung:

Các module cấp cao không nên phụ thuộc vào các modules cấp thấp. Cả 2 nên phụ thuộc vào abstraction. Interface (abstraction) không nên phụ thuộc vào chi tiết, mà ngược lại (Các class giao tiếp với nhau thông qua interface (abstraction), không phải thông qua implementation).

Giải thích:

  • Có thể hiểu nguyên lí này như sau: những thành phần trong 1 chương trình chỉ nên phụ thuộc vào những cái trừu tượng (abstraction). Những thành phần trừu tượng không nên phụ thuộc vào các thành phần mang tính cụ thể mà nên ngược lại.
  • Những cái trừu tượng (abstraction) là những cái ít thay đổi và biến động, nó tập hợp những đặc tính chung nhất của những cái cụ thể. Những cái cụ thể dù khác nhau thế nào đi nữa đều tuân theo các quy tắc chung mà cái trừu tượng đã định ra. Việc phụ thuộc vào cái trừu tượng sẽ giúp chương trình linh động và thích ứng tốt với các sự thay đổi diễn ra liên tục.

Ví dụ:

<?php
// Dependency Inversion Principle Violation
class Mailer
{
//
}
class SendWelcomeMessage
{
    private $mailer;
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
}

Trong ví dụ này, chương trình phụ thuộc vào cái cụ thể, chính là $mailer. Đặt vấn đề, nếu chúng ta muốn gửi mail theo nhiều dạng, như Smtp, hay SendGrid thì sao. Đây chính là cái trừu tượng mà mình đã giải thích ở trên. Nó luôn luôn thay đổi nhưng bản chất vẫn là send. Ta sẽ sửa lại một chút để theo nguyên lý này:

interface Mailer
{
    public function send();
}
class SmtpMailer implements Mailer
{
    public function send()
    {
     //
    }
}
class SendGridMailer implements Mailer
{
    public function send()
    {
     //
    }
}
class SendWelcomeMessage
{
    private $mailer;
    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
}

Tuyệt vời, giờ bạn muốn gửi mail theo loại gì thì sẽ phân chia trong class đó. bạn chỉ việc thêm 1 class mới implements từ interface Mailer và xử lý trong nó. Ngoài ra code cũng rõ ràng và đẹp hơn rất nhiều.

Kết Luận

Trên đây mình chỉ giới thiệu tổng quát để các bạn có cái nhìn tổng quan về các nguyên lý này. Tuy nhiên có 1 số nguyên lý không phải lúc nào cũng có thể áp dụng được, tất nhiên cũng có ý kiến trái chiều khác nhau, nhưng quan trọng hơn cả là cần có cái nhìn tổng thể và phân tích tốt hệ thống để biết được khi nào áp dụng và không áp dụng sẽ hiệu quả hơn nhiều. Loạt bài về Lập trình hướng đối tượng trong PHP của mình xin dừng lại tại đây. Mình mong nhận được sự quan tâm của các bạn. Hi vọng đã có thể có những thông tin bổ ích cho các bạn. Tạm biệt!

<< Phần 2


All Rights Reserved