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

4. I – Interface segregation principle (ISP – nguyên lý tách biệt giao tiếp hay phân tách interface)

  • Nội dung: Client không nên phụ thuộc vào giao tiếp (interface) mà chúng không sử dụng.
  • Giải thích:
    • Khi viết các giao tiếp (interface) chỉ nên thêm những phương thức mà nó cần phải có.
    • Nếu thêm các phương thức không cần thiết vào 1 interface các lớp thực hiện giao tiếp (class implement interface) sẽ bị bắt buộc thực thi những phương thức không cần thiết đó.
      Điều này dẫn đến sự dư thừa trong thực thể.

Ví dụ:

  • Đặt vấn đề: Xây dựng 1 interface Phone, các lớp SmartPhone, PhoneBox implement Phone.
interface Phone
{
    public function call();
    public function sms();
    public function picture();
}

class SmartPhone implements Phone
{
    public function call()
    {
        // do call
    }

    public function sms()
    {
        // send sms
    }

    public function picture()
    {
        // take picture
    }
}

class PhoneBox implements Phone
{
    public function call()
    {
        // do call
    }

    public function sms()
    {
        throw new Exception("PhoneBox can't send SMS");
    }

    public function picture()
    {
        throw new Exception("PhoneBox can't take picture");
    }
}

Nếu lớp Phone có thêm các phương thức mới thì các lớp SmartPhone, PhoneBox sẽ phải thay đổi theo trong khi nó không cần hoặc không thế thực hiện.

  • Giải pháp:
    • Chia nhỏ interface Phone thành những interface mang nhiệm vụ đặc thù
    • Các lớp SmartPhone, PhoneBox implement interface đặc thù mà nó cần
interface Callable()
{
    public function call();
}

interface Smsable()
{
    public function sms();
}

interface Pictureable()
{
    public function sms();
}

class SmartPhone implements Callable, Smsable, Pictureable
{
    public function call()
    {
        // do call
    }

    public function sms()
    {
        // send sms
    }

    public function picture()
    {
        // take picture
    }
}

class PhoneBox implements Callable
{
    public function call()
    {
        // do call
    }
}

Như vậy khi mở rộng ta thêm các interface đặc thù với chức năng mở rộng thì sẽ không ảnh hưởng đến các lớp dẫn xuất.

Lưu ý & kết luận:

  • Nguyên lý ISP (tách biệt giao tiếp) có mối liên hệ với nguyên lý OCP (nguyên lý mở rộng – hạn chế).
    Vi phạm ISP có khả năng dẫn tới sự vi phạm OCP.
  • Những lớp trừu tượng chứa quá nhiều thuộc tính và chức năng gọi là những lớp bị ô nhiễm (polluted).
  • Các lớp dẫn xuất phụ thuộc vào polluted interface làm tăng sự kết dính giữa các thực thể.
    Khi nâng cấp, sửa đổi đòi hỏi các interface này thay đổi, các lớp dẫn xuất này buộc phải thay đổi theo, điều này vi phạm nguyên lý OCP ().

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

  • Nội dung:
    • Module ở level cao hơn không nên phụ thuộc vào module ở level thấp hơn mà cả 2 nên phụ thuộc vào sự trừu tượng (abstractions).
    • Sự trừu tượng (abstraction) không nên phụ thuộc vào chi tiết (details) mà ngược lại chi tiết nên phụ thuộc vào sự trừu tượng.
  • 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ụ:

  • Đặt vấn đề: Ta cần 1 lớp đảm nhiệm chức năng thông báo khi nhận được 1 event từ hệ thống. Thiết kế ban đầu có dạng như sau:
class Notice
{
    public function pushNotification()
    {
        //push notic
    }
}

class EventListener
{
    public function listen($event)
    {
        $notice = new Notice;
        $notice->pushNotification();
    }
}

Như thiết kế này lớp EventListener muốn hoạt động thì phụ thuộc vào lớp Notice.

Khi yêu cầu thay đổi ta cần 1 số event thì thông báo 1 số event thì ghi lại log. Lúc này ta thêm 1 lớp LogWriter và sửa code của hàm listen():

class LogWriter
{
    public function writeLog()
    {
        // write something
    }
}

class EventListener
{
    public function listen($event)
    {
        if($event == 'notice') {
            $notice = new Notice;
            $notice->pushNotification();
        } elseif($event == 'write') {
            $writer = new LogWriter;
            $writer->writeLog();
        }
    }
}

Như vậy với mỗi yêu cầu tăng thêm lớp EventListener sẽ càng phụ thuộc nhiều lớp, đồng thời phải sửa hàm listen() nhiều lần điều này vi phạm nguyên lý OCP (nguyên lý mở rộng – hạn chế).

  • Giải pháp:
    • Xây dựng 1 interface Handler, các lớp cụ thể Notice, LogWriter sẽ implements Handler.
    • Lớp EventListener phụ thuộc vào Handler thay vì các lớp cụ thể Notice, LogWriter.
interface Handler
{
    public function fire();
}

class Notice implements Handler
{
    public function fire()
    {
        //push notic
    }
}

class LogWriter implements Handler
{
    public function fire()
    {
        // write something
    }
}

class EventListener
{
    public function listen(Handler $event)
    {
        $event->fire();
    }
}

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

Lưu ý & kết luận:

  • Nguyên lý DIP (nghịch đảo phụ thuộc) có mối liên hệ với nguyên lý OCP (nguyên lý mở rộng – hạn chế). Vi phạm DIP có khả năng dẫn tới sự vi phạm OCP.
  • Dependency Inversion Principles != Dependency Injection.
    Dependency Injection chỉ là 1 trong những pattern để thực hiện Dependency Inversion Principles.

Nguồn: VIBLO