Nhập môn lập trình hướng đối tượng với PHP (Phần 3)

Mở đầu:

Như vậy chúng ta đã biết rõ 4 tính chất của OOP nếu chưa bạn có thể xem lại phần 1 của loạt bài viết này. Với các tính chất này của OOP giúp ta xây dựng nhiều vấn đề cụ thể khác nhau trong thế giới thực.

Tuy nhiên trong thực tế, chúng ta sẽ nhận thấy rằng sản phẩm chúng ta làm ra luôn có sự thay đổi và mở rộng chức năng theo thời gian (Nếu bạn đã học môn công nghệ phần mềm sẽ hiểu rõ điều này). Không có phần mềm nào có thể đứng vững theo thời gian mà không đổi. Và chúng ta phải luôn đáp ứng sự thay đổi đó. Chính vì lẽ đó nên trong quy trình phát triển phần mềm, khâu phân tích và thiết kết là cực kì quan trọng. Người thiết kế phải làm thế nào để kiến trúc phần mềm có thể dễ dàng đáp ứng với thay đổi nhất. Và để làm được điều đó thì cần phải có kiến thức rất sâu rộng trong hướng đối tượng, vận dụng linh hoạt các đặc trưng của OOP. Một trong những nguyên tắc (chỉ dẫn) để giúp chúng ta xây dựng được các ứng dụng OOP hiệu quả hơn đó là phương pháp thiết kế SOLID.

Đâ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 dự án áp dụng tốt những nguyên lý này sẽ có code dễ hiểu, dễ bảo trì và mở rộng. Và việc quan trọng nhất là việc bảo trì sẽ được dễ hơn nhiều. SOLID nhĩa là "cứng", áp dụng nguyên lý này nhiều thì bạn sẽ trở thành một code "cứng"😁. Nói vui vậy nhưng cũng không hẳn là sai. Nó là viết tắt của 5 nguyên tắc sau:

  • S : Single responsibility principle
  • O : Open/closed principle
  • L : Liskov substitution principle
  • I : Interface segregation principle
  • D : Dependency inversion principle

Nào chúng ta bắt đầu với từng nguyên tắc.

1. Single Responsibility principle (nguyên lý đơn nhiệm).

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

  • Giải thích: Một class có quá nhiều chức năng sẽ trở nên cồng kềnh và phức tạp. Khi yêu cầu thay đổi một class quá cồng kềnh thì việc thay đổi code rất khó khăn và mất nhiều thời gian. Áp dụng nguyên lý đơn nhiệm chia các chức năng thành nhiều class khác nhau giúp việc quản lý, mở rộng, bảo trì code thuận tiện hơn.
  • Lưu ý:
    • Về bản chất nguyên lý này chỉ là hướng dẫn không phải là nguyên tắc tuyệt đối
    • Việc hiểu và áp dụng nguyên lý này giúp cho việc viết code dễ đọc, dễ hiểu, dễ quản lý hơn. Tuy nhiên nguyên lý đơn nhiệm là nguyên lý đơn giản nhưng khó áp dụng, việc xác định khi nào cần áp dụng khi nào không phụ thuộc vào việc người code xác định được đúng chức năng của module mình đang làm.
  • Ví dụ:
      Class Book {
       private $title;
    
       public function getTitle(){
           return $this->title;
       }
    
       public function displayPreview(){
           //display preview
       }
    }
    
    Class trên thực hiện cả chức năng business logic của class Book và chức năng presentation. Khi thay đổi 1 trong 2 chức năng phải thay đổi cả Class Book, điều này vi phạm nguyên tắc Single responsibility. Cách làm tốt hơn như sau:
    Class Book {
       private $title;
    
       public function getTitle(){
           return $this->title;
       }
       
    }
    
    Class Book_Display {
       public function displayPreview(){
           //display preview
       }
    }
    

2. Open-Closed principle (nguyên lý mở rộng - hạn chế).

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

  • Giải thích: 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ũ) chứ không nên sửa đổi class cũ. Việc này dẫn đến tình trạng phát sinh nhiều class, nhưng chúng ta sẽ không cần phải test lại các class cũ nữa, mà chỉ tập trung vào test các class mới, nơi chứa các chức năng mới
  • Ta có ví dụ để hiểu rõ hơn:
     class Renderer {
        public function drawCircle(Circle $circle) {
            // draw circle
        }
    
        public function drawTriangle(Triangle $triangle) {
            // draw triangle
        }
    }
    
    Trong class trên, khi cần thêm các hình có thể vẽ được cần viết thêm function draw. Như vậy đã vi phạm nguyên lý open/closed. Cách tốt hơn:
     class Renderer {
        public function draw(Drawable $drawable) {
            $drawable->draw();
        }
    }
    
    interface Drawable {
        public function draw();
    }
    
    class Circle implements Drawable {
        public function draw() {
            // draw circle
        }
    }
    
    class Triangle implements Drawable {
        public function draw() {
            // draw triangle
        }
    }
    

3. Liskov Substitution Principle (nguyên lý thay thế Liskov)

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.

  • Giải thích:
    • 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.
  • Ví dụ: Bắt đầu có sự khó hiểu ở nguyên lý này. Không sao, hãy tưởng tượng bạn có 1 class cha tên Vịt. Các class VịtBầu, VịtXiêm có thể kế thừa class này, chương trình chạy bình thường. Tuy nhiên nếu ta viết class VịtChạyPin, cần đến pin mới chạy được. Khi class này kế thừa class Vịt, vì không có pin không chạy được, sẽ gây lỗi. Đó là 1 trường hợp vi phạm nguyên lý này. Hoặc ví dụ khác bằng các dòng code như sau:
     class Rectangle
        {
            protected $width;
            protected $height;
    
            public function setHeight($height)
                {
                $this->height = $height;
                }
    
            public function setWidth($width)
                {
                $this->width = $width;
                }
    
            public function area()
                {
                    return $this->width * $this->height;
                }
        }
    
        class Square extends Rectangle
        {
        public function setWidth($width)
            {
                $this->width = $width;
                $this->height = $width;
            }
    
            public function setHeight($height)
            {
                $this->width = $height;
                $this->height = $height;
            }
        }
    
        $square = new Square();
        $square->setWidth(3);
        $square->setHeight(4);
        echo $square->area(); // return 16 while it should be 12 as expected from class Rectangle
    
    Trong ví dụ trên, class Square kế thừa từ class Rectangle nhưng cho kết quả hàm area() không đúng như mong đợi từ class Rectangle. Như vậy đã vi phạm nguyên lý Liskov Substitution

4. Interface segregation principle (nguyên lý giao diện phân biệt - 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ể.

  • Giải thích: Nguyên lý này rất dễ hiểu. Hãy tưởng tượng chúng ta có 1 interface lớn, khoảng 100 methods. Việc implements sẽ khá cực khổ, ngoài ra còn có thể dư thừa vì 1 class không cần dùng hết 100 method. Khi tách interface ra thành nhiều interface nhỏ, gồm các method liên quan tới nhau, việc implement và quản lý sẽ dễ hơn.

5. Dependency inversion principle (nguyên lý nghịch đảo phụ thuộc).

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, không phải thông qua implementation.)

  • Nguyên lý này khá lắt léo. Hãy xem xét ví dụ với 2 loại đèn: đèn sợi đốt đuôi tròn và đèn huỳnh quang đuôi tròn. Chúng cùng có đuôi tròn, do đó ta có thể thay thế đèn sợi đốt đuôi tròn bằng đèn huỳnh quanh đuôi tròn cho nhau 1 cách dễ dàng. Ở đây, interface chính là đuôi tròn, 2 implementation là bóng đèn sợi đốt đuôi tròn và bóng đèn huỳnh quang đuôi tròn. Ta có thể thay đổi dễ dàng giữa 2 loại bóng vì ổ điện chỉ quan tâm tới interface (đuôi tròn), không quan tâm tới implementation.

    Trong code cũng vậy, khi áp dụng Dependency Inverse, ta chỉ cần quan tâm tới interface. Để kết nối tới database, ta chỉ cần gọi hàm Get, Save,…của Interface IDataAccess. Khi thay database, ta chỉ cần thay implementation của interface này.

Kết luận:

Phân 3 cũng như phần cuối của loạt bài nhập môn về PHP đến đây là kết thúc. Bài viết khá dài, cảm ơn mọi người đọc đến dòng cuối cùng này 😍.