Tìm hiểu nhanh về SOLID qua các ví dụ trong PHP

Hây zô xin chào tất cả các bạn. Có lẽ với những người đang đọc bài viết này của mình thì mình tin chắc rằng các bạn đã từng được học hay là tự tìm hiểu qua một khái niệm cơ bản và cũng không kém phần quan trọng trong lập trình đó là Ô Ô PÊ (OOP – lập trình hướng đối tượng) đúng không? Trong OOP thì có 4 khái niệm hay đúng hơn là tính chất cơ bản đó là:

  • Tính trừu tượng
  • Tính bao đóng
  • Tính kế thừa
  • Tính đa hình

4 tính chất trên trong OOP chính là nền tảng của những nguyên lý hay những pattern giúp cho những dự án được phát triển một cách khoa học. Hôm nay mình xin mạn phép giới thiệu một trong số đó. Đó chính là nguyên lý SOLID – có thể các bạn chưa từng nghe qua bao giờ hoặc đã từng nghe qua nhưng chưa hiểu rõ thì bài này chính là dành cho bạn rồi đó.

Cùng tìm hiểu nhé. Let’s go baby!

Vậy SOLID là cái gì?

SOLID là viết tắt 5 chữ cái đầu hay nói cách khác là tập hợp 5 nguyên tắc thiết kế trong lập trình hướng đối tượng. Tác giả của 5 nguyên tắc này là Robert C. Martin – một kỹ sư phần mềm người Mẽo.

Vậy SOLID sinh ra để làm gì?

Xin trả lời là nó giúp cho developer viết ra những đoạn code dễ đọc, dễ hiểu, dễ maintain. Tuy nói là như vậy nhưng việc theo sát và áp dụng 5 nguyên tắc này là điều không hề đơn giản một chút nào.

5 nguyên tắc của SOLID là gì?

  • S: Single-responsibility principle
  • O: Open-closed principle
  • L: Liskov substitution principle
  • I: Interface segregation principle
  • D: Dependency Inversion Principle

Single-responsibility Principle

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

Một lớp nên có một và chỉ có một lý do để thay đổi, nghĩa là một lớp chỉ nên có trách nhiệm duy nhất.

Nếu như một class của chúng ta mà đảm nhiệm quá nhiều trách nhiệm. Điều này khiến cho code của chúng ta liên kết quá sâu với nhau và có thể bị lỗi nếu có bất cứ sự thay đổi nhỏ nào.

Giả sử như chúng ta có một class như sau:

<?php
class Order {
    public function showOrderDetail() {
        //
    }

    public function processOrder() {
        //
    }
    
    public function storeToDB() {
        // 
    }
}

Các bạn có thể thấy là class Order này có tới 3 trách nhiệm đó là hiển thị ra thông tin của order, xử lý order và lưu order và trong database. Như vậy, sau này chúng ta muốn gửi email cho người order, thêm các thao tác xử lý order thì khiến cho class này bị phình ra rất to. Theo nguyên lý chúng ta nên tách riêng ra 3 class như sau:

<?php
class Order {
    public function showOrderDetail() {
        //
    }
}

class OrderProcessor {
    public function processOrder(Order $order) {
        // 
    }
}

class OrderDB {
    public function storeToDB(Order $order) {
        // 
    }
}

Open-closed Principle

Nguyên lý này có thể hiểu là các class có thể được mở rộng thoải mái nhưng tuyệt đối không được sửa đổi class cũ.

Mình có ví dụ sau đây để các bạn có thể hiểu rõ hơn. Mình muốn tính toán tổng diện tích của mảng gồm khối hình học như sau:

<?php
class Rectangle {
  
    private $width;
    private $height;
    
    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }
}
class Square {
  
    private $length;
    
    public function __construct($length) {
        $this->length = $length;
    }
}
class AreaCalculator {
  
    protected $shapes;
    
    public function __construct($shapes = array()) {
        $this->shapes = $shapes;
    }
    
    public function sum() {
        foreach($this->shapes as $shape) {
            if($shape instanceof Square) {
                $area[] = pow($shape->length, 2);
            } else if($shape instanceof Rectangle) {
                $area[] = $shape->width * $shape->height;
            }
        }
    
        return array_sum($area);
    }
}

Đoạn code cho phép chúng ta tính diện tích của hình chữ nhật và hình vuông. Nếu giờ mình muốn mở rộng chương trình, muốn hỗ trợ thêm hình tam giác nữa. Lúc này mình phải sửa đổi hàm sum của class AreaCalculator. Như vậy là vi phạm nguyên tắc rồi, để sửa lại chúng ta cần làm như sau:

<?php

interface Shape {
    public function area();
}

class Rectangle implements Shape {
  
    private $width;
    private $height;
    
    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }
    
    public function area() {
        return $this->width * $this->height;
    }
}

class Square implements Shape {
  
    private $length;
    
    public function __construct($length) {
        $this->length = $length;
    }
    
    public function area() {
        return pow($this->length, 2);
    }
}

class AreaCalculator {
  
    protected $shapes;
    
    public function __construct($shapes = array()) {
        $this->shapes = $shapes;
    }
    
    public function sum() {
        foreach($this->shapes as $shape) {
            $area[] = $shape->area();
        }
    
        return array_sum($area);
    }
}

Với đoạn code mới mà chúng ta đã sửa theo chuẩn nguyên lý sô 2 này, nếu bạn muốn mở rộng để tính toán thêm hình tròn, hình tam giác hay hình gì đi nữa thì bạn chỉ cần tạo THÊM một class và implement interface Shape là được, không cần phải sửa code đã có.

Liskov Substitution Principle

Các instance của lớp con có thể thay thế được instance lớp cha mà vẫn đảm bảo tính đúng đắn của chương trình.

Nguyên tắc này được hiểu đơn giản như sau. Một công ty có nhiều 2 loại nhân viên: nhân viên chính thức, thực tập sinh không lương. Với nhân viên chính thức thì cần nộp thuế thu nhập còn thực tập sinh thì không cần. Bạn có 1 lớp NhanVien để là class cha cho các nhân viên trong công ty như sau:

<?php

class NhanVien
{
    public function dongThue()
    {
        //
    }
}

class ChinhThuc extends NhanVien
{
    public function dongThue()
    {
        //
    }
}

class ThucTapSinh extends NhanVien
{
    public function dongThue()
    {
        throw new Exception('Thuc tap sinh khong phai dong thue');
    }
}

Như vậy là đã vi phạm nguyên tắc này vì khi gọi hàm dongThue() của lớp ThucTapSinh sẽ khiến cho chương trình bị lỗi. Để giải quyết vấn đề này thì bạn cần tạo 1 interface NhanVienCanDongThue ra và những loại nhân viên nào cần đóng thuế thì chúng ta sẽ implement nó.

<?php

interface NhanVienCanDongThue
{
    public function dongThue();
}

class NhanVien
{
    //
}

class ChinhThuc extends NhanVien implements NhanVienCanDongThue
{
    public function dongThue()
    {
        //
    }
}

class ThucTapSinh extends NhanVien
{
    //
}

Interface Segregation Principle

Một client không nên phụ thuộc vào các interface với nhiều những phương thức mà nó không cần thiết phải dùng tới. Chúng ta nên tách nhỏ các interface ra để cho việc code trở nên trong sáng và ko bị phình to.

Ví dụ như bạn có một interface chung dành cho các developer.

interface Developer
{
    public function codePHP();
    public function codeRuby();
    public function codeIOS();
}

class PHPDeveloper implements Developer
{
    public function codePHP()
    {
        //
    }

    public function codeRuby()
    {
        throw new Exception('PHP Developer khong the code Ruby');
    }

    public function codeIOS()
    {
        throw new Exception('PHP Developer khong the code IOS');
    }
}

Đó, bạn sẽ thấy nó thật sự vô lý khi 1 PHPDeveloper implements interface Developer này lại phải đi codeRuby hay là codeIOS đúng ko. Trong trường hợp này chúng ta nên tách ra 3 interface nhỏ hơn thì PHPDeveloper chỉ implements interface PHPDeveloper thôi.

<?php

interface PHPDeveloper
{
    public function codePHP();
}

interface RubyDeveloper
{
    public function codeRuby();
}

interface IOSDeveloper
{
    public function codeIOS();
}

Dependency Inversion Principle

High-level module không nên phụ thuộc vào low-level module. Cả 2 nên phụ thuộc vào những abstractions (High-level modules should not depend on low-level modules. Both should depend on abstractions.)
Những Abstractions không nên phụ thuộc vào Details. Details nên phụ thuộc vào Abstractions. (Abstractions should not depend on details. Details should depend on abstractions)

Ví dụ đơn giản là bạn có một class UserDB dùng để lưu thông tin user vào trong database.

<?php

class MySQLConnection
{
  
    public function connectToMySQL()
    {
        // Return the MySQL connection...
    }
}

class MongoDBConnection
{
  
    public function connectToMongoDB()
    {
        // Return the MongoDB connection...
    }
}

class UserDB {
  
    private $dbConnection;
    
    public function __construct(MySQLConnection $dbConnection)
    {
        $this->$dbConnection = $dbConnection->connectToMySQL();
    }
}

Lúc đầu thì UserDB dùng MySQL nhưng về sau dự án của bạn lại cần chuyển sang MongoDB thì sao? Bạn sẽ phải sửa class UserDB, điều này có nghĩa là UserDB đang bị phụ thuộc vào dependency của mình. Điều này là sai nguyên tắc cuối cùng. Để sửa lại, chúng ta cần làm như sau:

<?php

interface Database
{
    public function connectToDatabase();
}

class MySQLConnection implements Database
{
  
    public function connectToDatabase()
    {
        // Return the MySQL connection...
    }
}

class MongoDBConnection implements Database
{
  
    public function connectToDatabase()
    {
        // Return the MongoDB connection...
    }
}

class UserDB {
  
    private $dbConnection;
    
    public function __construct(Database $dbConnection)
    {
        $this->$dbConnection = $dbConnection->connectToDatabase();
    }
}

Như vậy thì dù bạn có dùng bất cứ loại database nào đi chăng nữa thì class UserDB của bạn cũng ko phải sửa lại.

Tổng kết

Trên đây là những gì mình tìm hiểu về nguyên lý SOLID trong phát triển phần mềm. Thực ra ở thời điểm hiện tại mình cũng chưa thực sự hiểu cũng như áp dụng được hiệu quả và trọng vẹn những nguyên lý trên vào trong việc coding dự án. Hy vọng mình và các bạn trong tương lai có thể áp dụng SOLID một cách hiệu quả để việc xây dựng phần mềm trở nên tốt hơn nhé. ^^

Thân ái và quyết thắng.

Tham khảo:

https://levelup.gitconnected.com/solid-principles-simplified-php-examples-based-dc6b4f8861f6

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *