Lập trình hướng đối tượng PHP-Phần 5

0

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

SOLID là gì? Những nguyên lý mình giới thiệu hôm nay là những nguyên lý thiết kế trong OOP. Đây là những nguyên lý được đúc kết bởi máu xương vô số developer, rút ra từ hàng ngàn dự án thành công và thất bại. Một project áp dụng những nguyên lý này sẽ có code dễ đọc, dễ test, rõ ràng hơn. Và việc quan trọng nhất là việc maintainace code sẽ dễ hơn rất nhiều. Nắm rõ được những nguyên lý của SOLID, bạn sẽ thành 1 lập trình viên thực thụ. SOLID dựa trên 5 nguyên tắc sau:

S : Single responsibility principle

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

1 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)

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.

class Employee
{
    public function developSoftware(){};
    public function testSoftware(){};
    public function saleSoftware(){};
}

Giả sử một công ty có đợt tuyển nhân viên với 3 chức năng khác nhau.

  • Developer sẽ vào developSoftware(),
  • Tester sẽ vào testSoftware()
  • Nhân viên sale sẽ vào saleSoftware().

Mỗi nhân viên sẽ được xếp vào đúng phòng ban và thực hiện đúng chức năng của mình. Developer sẽ không testSoftware(), ngược lại Tester sẽ chỉ thực hiện testSoftware(). Khi số lượng nhân viên có lớn thêm thì việc quản lý cũng dễ dàng hơn và điều quan trọng nhất là các nhân viên sẽ làm việc có hiệu quả hơn.

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)

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 1 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ý ifvàelsekia. 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 Tester và Programmer 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 ????????

L: Liskov substitution principle

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.

I : Interface segregation principle

1 class không nên thực hiện 1 interface mà nó không dùng đến hoặc không nên phụ thuộc vào 1 phương thức mà nó không sử dụng. Để làm điều này, thay vì 1  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. 1 class implements từ 1 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. 1 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  ????

D : Dependency Inversion Principle

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.

Các quy tắc trong PSR-2

Viết code chuẩn (coding convention) là chúng ta tuân thủ một quy định trong viết code của một tập thể hay một công ty dựa theo quy chuẩn trong lập trình. Tùy thuộc vào ngôn ngữ sẽ có chuẩn viết code khác nhau. Trong bài viết này mình sẽ nói về chuẩn code PSR-2 trong PHP.

PSR có nghĩa là PHP Standards Recommendations. Có rất nhiều PSR từ PSR-0 đến PSR-7. Trong đó PSR-1 và PSR-2 chúng ta sẽ tiếp xúc rất nhiều. PSR-1 sẽ giúp chúng ta biết thề nào và làm thế nào để đặt tên biến, tên hàm sau cho dể hiểu, dể đọc mang tính thống nhất toàn bộ.

Khác với PSR-1, PSR-2 sẽ mang tính trình bày là chính. Nó có nhiệm vụ rất quan trong trong việc trình bài các dòng code của bạn. từ các dòng tab hay xuống hàng giữa các dòng, các hàm một cách tỉ mỉ.

Vậy tại sao lại phải viết code chuẩn

  • Như mình đã đề cập ở trên việc viết code chuẩn rất quan trọng. đối với những ai thường không tuân theo quy định viết code chuẩn (viết tùy ý) đó chỉ có thể làm việc cá nhân.
  • Các bạn nên nhớ rằng khi đi làm chúng ta điều làm teamwork không thể mõi người một cách viết. Mõi người một cách viết là đúng khi nói về tư duy logic trong lập trình có thể cùng một kết quả nhưng lại có nhiều cách viết khác nhau.
  • Sự thống nhất về cách trình bày. Bạn viết như thế nào khi người khác xem lại hoặc sữa code của bạn vẫn hiểu được cái function của bạn đang viết cái gì phải không nào.

Các quy tắc:

  • Code PHẢI tuân thủ PSR-1
  • Code PHẢI sử dụng 4 ký tự space để lùi khối (không dùng tab)
  • Mỗi dòng code PHẢI dưới 120 ký tự, NÊN dưới 80 ký tự.
  • 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, 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, và ký tự } kết thúc hàm PHẢI ở dòng tiếp theo của thân hàm.
  • 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;
  • 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.
  • Mở khối { cho cấu trúc điều khiển PHẢI trên cùng một dòng; và đóng khối này với } ở dòng tiếp theo của thân khối.
  • Hằng số true, false, null PHẢI viết với chữ thường.
  • Từ khóa extends và implements PHẢI cùng dòng với class
  • Implements nhiều lớp, thì mỗi lớp trên một dòng
  • Keyword var KHÔNG ĐƯỢC dùng sử dụng khai báo property.
  • Tên property KHÔNG NÊN có tiền tố _ nhằm thể hiện thuộc tính 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 ,. Các tham số CÓ THỂ trên nhiều dòng, nếu làm như vậy thì PHẢI mỗi dòng 1 tham số.
  • Abstract, final PHẢI đứng trước visibility, còn static phải đi sau.

Một số chi tiết về chuẩn PSR-1:

  • Các file phải sử dụng thẻ <?php hoặc <?.
  • File PHP chỉ sử dụng encode: UTF-8 without BOM với code PHP.
  • Các file nên dùng để khai báo các thành phần PHP (các lớp, hàm, hằng …) hoặc dùng với mục đích làm hiệu ứng phụ (như include, thiết lập ini cho PHP …), nhưng không nên dùng trong cả 2 trường hợp.
  • Các namespace và các class phải theo chuẩn “autoloading” PSR: [PSR-0 và PSR-4].
  • Tên Class phải có dạng NameClass.
  • Hằng số trong class phải được khai báo bằng cách viết hoa và chia ra bởi dấu _.
Leave A Reply

Your email address will not be published.