0

Tổng quan về nguyên tắc Solid

Giới thiệu về SOLID

image.png

SOLID là từ viết tắt của năm nguyên tắc thiết kế hướng đối tượng (Object-Oriented Design - OOD) do Robert C. Martin (còn được gọi là Uncle Bob) đề xuất.

Lưu ý: Mặc dù các nguyên tắc này có thể áp dụng cho nhiều ngôn ngữ lập trình khác nhau, nhưng các ví dụ trong bài viết này sẽ sử dụng PHP.

Những nguyên tắc này giúp lập trình viên phát triển phần mềm dễ bảo trì và mở rộng khi dự án phát triển. Áp dụng chúng có thể giúp tránh các code smells, cải thiện khả năng tái cấu trúc và phát triển phần mềm theo hướng Agile hoặc Adaptive.

SOLID bao gồm:

S - Nguyên tắc đơn trách nhiệm (Single-responsibility Principle - SRP)

O - Nguyên tắc mở-đóng (Open-closed Principle - OCP)

L - Nguyên tắc thay thế Liskov (Liskov Substitution Principle - LSP)

I - Nguyên tắc phân tách giao diện (Interface Segregation Principle - ISP)

D - Nguyên tắc đảo ngược sự phụ thuộc (Dependency Inversion Principle - DIP)

Nguyên tắc đơn trách nhiệm (SRP)

Nguyên tắc SRP phát biểu rằng:Một lớp chỉ nên có một lý do để thay đổi, nghĩa là một lớp chỉ nên có một trách nhiệm duy nhất.

Ví dụ, giả sử chúng ta có một ứng dụng tính tổng diện tích của một tập hợp các hình như hình tròn và hình vuông. Trước tiên, chúng ta tạo các lớp hình học và định nghĩa constructor để thiết lập các tham số cần thiết.

class Square
{
    public $length;

    public function __construct($length)
    {
        $this->length = $length;
    }
}

class Circle
{
    public $radius;

    public function __construct($radius)
    {
        $this->radius = $radius;
    }
}

Sau đó, tạo lớp AreaCalculator để tính tổng diện tích các hình.

class AreaCalculator
{
    protected $shapes;

    public function __construct($shapes = [])
    {
        $this->shapes = $shapes;
    }

    public function sum()
    {
        $area = [];
        foreach ($this->shapes as $shape) {
            if ($shape instanceof Square) {
                $area[] = pow($shape->length, 2);
            } elseif ($shape instanceof Circle) {
                $area[] = pi() * pow($shape->radius, 2);
            }
        }
        return array_sum($area);
    }
}

Khi cần xuất kết quả, chúng ta sử dụng phương thức output():

public function output()
{
    return "Tổng diện tích các hình: " . $this->sum();
}

Vấn đề ở đây là AreaCalculator vừa tính toán diện tích vừa đảm nhiệm việc xuất kết quả. Nếu sau này cần xuất kết quả ở định dạng khác (ví dụ JSON), chúng ta phải chỉnh sửa lớp này, vi phạm nguyên tắc SRP.

Giải pháp: Tạo một lớp riêng để xử lý đầu ra:

class SumCalculatorOutputter
{
    protected $calculator;

    public function __construct(AreaCalculator $calculator)
    {
        $this->calculator = $calculator;
    }

    public function JSON()
    {
        return json_encode(['sum' => $this->calculator->sum()]);
    }

    public function HTML()
    {
        return "Tổng diện tích các hình: " . $this->calculator->sum();
    }
}

Bây giờ, việc tính toán và hiển thị đã được tách biệt, tuân thủ nguyên tắc SRP.

Nguyên tắc mở-đóng (OCP)

OCP phát biểu rằng: Một lớp nên được mở rộng mà không cần sửa đổi mã nguồn ban đầu.

Tiếp tục với AreaCalculator, giả sử chúng ta muốn hỗ trợ thêm các hình khác như tam giác, lục giác...

Nếu cứ thêm các khối if-else, mã sẽ ngày càng phức tạp, vi phạm OCP.

Giải pháp: Chúng ta đưa logic tính diện tích vào từng lớp hình và sử dụng giao diện ShapeInterface:

interface ShapeInterface
{
    public function area();
}

class Square implements ShapeInterface
{
    public $length;

    public function __construct($length)
    {
        $this->length = $length;
    }

    public function area()
    {
        return pow($this->length, 2);
    }
}

class Circle implements ShapeInterface
{
    public $radius;

    public function __construct($radius)
    {
        $this->radius = $radius;
    }

    public function area()
    {
        return pi() * pow($this->radius, 2);
    }
}

Bây giờ, AreaCalculator có thể gọi area() mà không cần quan tâm đến từng loại hình:

public function sum()
{
    $area = [];
    foreach ($this->shapes as $shape) {
        if ($shape instanceof ShapeInterface) {
            $area[] = $shape->area();
        }
    }
    return array_sum($area);
}

Điều này đảm bảo rằng chúng ta có thể thêm hình mới mà không cần chỉnh sửa AreaCalculator, tuân thủ OCP.

Nguyên tắc thay thế Liskov (LSP)

LSP phát biểu rằng: Một lớp con phải có thể thay thế lớp cha của nó mà không làm thay đổi tính đúng đắn của chương trình.

Ví dụ, giả sử chúng ta mở rộng AreaCalculator thành VolumeCalculator:

class VolumeCalculator extends AreaCalculator
{
    public function sum()
    {
        return $summedData; // Giá trị đơn thay vì mảng
    }
}

Điều này giúp VolumeCalculator có thể thay thế AreaCalculator mà không gây lỗi khi xuất kết quả, tuân thủ LSP.

Nguyên tắc phân tách giao diện (ISP)

ISP phát biểu rằng: Một giao diện không nên ép các lớp triển khai những phương thức mà chúng không sử dụng.

Giả sử chúng ta có một giao diện ShapeInterface với phương thức volume(), nhưng hình vuông không có thể tích.

Giải pháp:Tách giao diện:

interface ShapeInterface
{
    public function area();
}

interface ThreeDimensionalShapeInterface
{
    public function volume();
}

Bây giờ, hình khối ba chiều như Cuboid sẽ triển khai ThreeDimensionalShapeInterface, còn hình phẳng chỉ cần ShapeInterface, tuân thủ ISP.

Nguyên tắc đảo ngược sự phụ thuộc (DIP)

DIP phát biểu rằng:Các module cấp cao không nên phụ thuộc vào các module cấp thấp. Cả hai nên phụ thuộc vào abstraction.

Ví dụ, PasswordReminder phụ thuộc vào MySQLConnection:

class MySQLConnection
{
    public function connect()
    {
        return 'Database connection';
    }
}

class PasswordReminder
{
    private $dbConnection;

    public function __construct(MySQLConnection $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}

Nếu thay đổi database, chúng ta phải chỉnh sửa PasswordReminder, vi phạm DIP.

Giải pháp: Tạo interface DBConnectionInterface:

interface DBConnectionInterface
{
    public function connect();
}

Và sử dụng interface trong PasswordReminder:

class PasswordReminder
{
    private $dbConnection;

    public function __construct(DBConnectionInterface $dbConnection)
    {
        $this->dbConnection = $dbConnection;
    }
}

Bây giờ, PasswordReminder không bị ràng buộc vào một loại database cụ thể, tuân thủ DIP.

Kết luận

Bằng cách phân tích từng chữ cái một trong SOLID, chúng ta đã đi sâu vào ý nghĩa của từng nguyên tắc cũng như những cách áp dụng phù hợp được minh họa qua ví dụ dễ hiểu.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí