SOLID là gì ? Áp dụng các nguyên lý SOLID trong thiết kế (Phần 2)

solid là gì ? Áp dụng các nguyên lý solid trong thiết kế (phần 2)

3. Liskov substitution principle (LSP)

Nguyên lý thứ ba, tương ứng với chữ L trong SOLID. Nội dung nguyên lý:

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.

Hơi khó hiểu? Không sao, lúc mới đọc mình cũng vậy. Hãy tưởng tượng một con vịt sống và một con vịt đồ chơi (các classes PHP). Cả hai lớp này đều triển khai giao diện TheDuck .

interface TheDuck 

{ 

public function swim(): void; 

}

Chúng ta cũng có một bộ điều khiển với hành động ‘swim()’

class SomeController 

{ 

public function swim(): void { 

$this->releaseDucks([ 

new LiveDuck(), 

new ToyDuck() 

]); 

} 


private function releaseDucks(array $ducks): void { 

/*

* @var TheDuck $duck 

*/ 

foreach ($ducks as $duck) { 

$duck->swim(); 

} 

} 

} 

Nhưng sau khi gọi hành động này, ToyDuckkhông bơi. Tại sao? Bởi vì để làm cho nó bơi, trước tiên bạn phải gọi phương thức " turnOn() ". Chúng ta có thể sửa đổi hành động của bộ điều khiển và thêm một điều kiện mà chúng ta gọi là turnOn() trên phiên bản ToyDuck trước swim() .

private function releaseDucks(array $ducks): void

{

    /** @var TheDuck $duck */

    foreach ($ducks as $duck) {

        if ($duck instanceof ToyDuck) {

            $duck->turnOn();

        }

            

        $duck->swim();

    }

}

 Một interface chung cho cả hai con vịt này không phải là một ý tưởng hay, hoạt động của chúng hoàn toàn khác nhau, mặc dù chúng ta nghĩ rằng cả hai đều hoạt động tương tự nhau vì chúng đang bơi, nhưng thực tế không phải vậy. Và đây là 1 trường hợp vi phạm nguyên lý này.

4. Interface Segregation Principle

Nguyên lý thứ tư, tương ứng với chữ I trong SOLID. Nội dung nguyên lý:

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ể

Nguyên lý này khá 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 (DIP)

Nguyên lý cuối cùng, tương ứng với chữ D trong SOLID. Nội dung nguyên lý:

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

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

Để minh họa vấn đề, hãy xem qua ví dụ PHP này.

class DatabaseLogger 

{ 

public function logError(string $message) { 

// .. 

} 

}


Ở đây chúng ta có một lớp ghi lại một số thông tin vào CSDL. Bây giờ sử dụng lớp này.

class MailerService

{

    private DatabaseLogger $logger;

 

    public function __construct(DatabaseLogger $logger)

    {

        $this->logger = $logger;

    }

 

    public function sendEmail()

    {

        try {

            // ..

        } catch (SomeException $exception) {

            $this->logger->logError($exception->getMessage());

        }

    }

}


Đây là lớp PHP sendEmail, trong trường hợp có lỗi, chi tiết lỗi sẽ được ghi vào cơ sở dữ liệu bằng trình ghi nhật ký mà chúng ta vừa thấy ở trên.

Nó phá vỡ nguyên tắc đảo ngược phụ thuộc . Dịch vụ gửi e-mail của chúng ta sử dụng triển khai trình ghi nhật ký cụ thể. Nếu chúng ta muốn ghi thông tin về lỗi vào một tệp hoặc Sentry thì sao? Chúng ta sẽ phải thay đổi MailerService . Đây không phải là một giải pháp linh hoạt, sự thay thế như vậy trở nên có vấn đề.

Vì vậy, nó nên trông như thế nào?

Theo nguyên tắc này, MailerService nên dựa trên sự trừu tượng hơn là triển khai chi tiết. Do đó, chúng ta đang thêm giao diện LoggerInterface.

interface LoggerInterface

{

    public function logError(string $message): void;

}

Và chúng ta sử dụng nó trong DatabaseLogger:

class DatabaseLogger implements LoggerInterface

{

   public function logError(string $message): void

   {

       // ..

   }

}

Bây giờ, chúng ta có thể tận dụng lợi thế của Symfony Dependency Injection .

class MailerService

{

    private LoggerInterface $logger;

 

    public function sendEmail()

    {

        try {

            // ..

        } catch (SomeException $exception) {

            $this->logger->logError($exception->getMessage());

        }

    }

}

Theo cách này, chúng ta có thể tự do thay thế nhật ký trong cơ sở dữ liệu bằng nhật ký ở bất cứ đâu chúng ta muốn, miễn là việc triển khai chi tiết thực hiện LoggerInterface . Thay đổi này sẽ không yêu cầu sửa đổi MailerService , bởi vì nó không phụ thuộc vào nó, nó chỉ phụ thuộc vào giao diện.