SOLID principles qua hình ảnh

Xin chào tất cả các bạn! Với các bạn đang làm backend như mình, có lẽ các bạn đã nghe về SOLID. Đặc biệt là khi đi phỏng vấn sẽ thường xuyên được hỏi về OOP và SOLID Principles, DRY Principle. Vậy SOLID Principles là gì? SOLID Principles là 5 nguyên tắc phát triển phầm mềm giúp cho lập trình viên khi xây dựng phần mềm sẽ dễ dàng mở rộng và bảo trì hơn.

Nếu Google 1 bài viết về SOLID, có rất nhiều bài viết về SOLID nhưng mình thấy các bài viết chỉ có chữ là chữ, và hiếm khi thấy bất kỳ ví dụ nào có hình ảnh minh họa. Điều này gây khó hiểu cho người học. Vì vậy, mục đích chính của bài viết này là để hiểu rõ hơn về các nguyên tắc này bằng cách sử dụng hình ảnh minh họa và nhấn mạnh đến mục tiêu cho từng nguyên tắc.

Các bạn hãy cùng mình khám phá về thế giới SOLID nhé!

SOLID principles qua hình ảnh

SOLID là tập hợp của 5 nguyên tắc và mỗi nguyên tắc ứng với 1 ký tự trong nó:

  1. S: Single responsibility principle.
  2. O: Open/Closed principle.
  3. L: Liskov Substitution Principle.
  4. I: Interface segregation principle.
  5. D: Dependency Inversion Principle.

S: Single responsibility principle

Một class chỉ nên giữ 1 trách nhiệm duy nhất (Chỉ có thể sửa đổi class với 1 lý do duy nhất).
SOLID principles - S: Single responsibility principle

Nếu 1 class có quá nhiều chức năng, quá cồng kềnh, việc thay đổi code sẽ rất khó khăn, mất nhiều thời gian, còn dễ gây ảnh hưởng tới các module đang hoạt động khác.

Ví dụ 1 class vi phạm nguyên tắc này:

public class MasterRobot()
{
   public void Cooking();
   public void Garden();
   public void Paint();
   public void Drive();
}

Class này đang chịu trách nhiệm cho 4 chức năng: nấu ăn, làm vườn, vẽ tranh và lái xe. Giả sử, nếu chức năng nấu ăn của robot bị lỗi thì sao. Ta sẽ phải sửa cả con Robot, điều này có thể gây ảnh hưởng đến các chức năng khác.

Theo đúng nguyên tắc, ta phải tách class này ra làm 4 class riêng. Tuy số lượng class nhiều hơn nhưng việc sửa chữa sẽ đơn giản hơn, class ngắn hơn nên cũng ít bug hơn.

public class ChefRobot()
{
   public void Cooking();
}

public class GardenerRobot()
{
   public void Garden();
}

public class PainterRobot()
{
   public void Paint();
}

public class DriverRobot()
{
   public void Drive();
}

Mục tiêu:

Nguyên tắc này nhằm mục đích phân tách nhỏ các hành vi để nếu có lỗi phát sinh do thay đổi của bạn, nó sẽ không gây ảnh hưởng đến các hành vi không liên quan khác.

O: Open/Closed principle

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).
SOLID Principles - O: Open/Closed principle

Việc thay đổi hành vi hiện tại của 1 Class sẽ ảnh hưởng đến hệ thống đang sử dụng Class đó. Nếu bạn muốn Class thực hiện nhiều chức năng hơn, cách tiếp cận lý tưởng là viết Class mới mở rộng từ Class cũ (bằng cách kế thừa hoặc sở hữu Class cũ).

Mục tiêu:

Nguyên tắc này nhằm mục đích mở rộng hành vi của Class mà không làm thay đổi hành vi hiện có của Class đó. Điều này là để tránh gây ra lỗi ở bất cứ nơi nào Class đang được sử dụng.

L: Liskov Substitution Principle

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 hay không ra lỗi của chương trình.
SOLID Principles - L: Liskov Substitution Principle

Khi 1 Class con không thể thực hiện các hành động giống như Class cha của nó, điều này có thể gây ra lỗi.

Nếu bạn có 1 Class A và tạo 1 class B từ class A đó, thì Class A sẽ trở thành Class parent (Class cha mẹ), Class BClass child (Class con). Các Class con nên có thể làm tất cả những gì mà Class cha mẹ có thể làm. Quá trình này được gọi là Kế thừa.

Các Class con nên có thể xử lý các yêu cầu tương tự và cung cấp những kết quả tương tự như Class cha.

Liên tưởng đến hình ảnh trên, Robot tên là Sam (Class cha) cung cấp cafe (có thể là bất kỳ loại cafe nào). Có thể chấp nhận cho Robot tên Eden (Class con) để cung cấp Cappuchino vì nó là 1 loại cụ thể của cafe, nhưng không thể chấp nhận khi Robot tên Eden cung cấp 1 chai nước được.

Nếu Class con không đáp ứng được nhu cầu này, điều này có nghĩa là Class con bị thay đổi hoàn toàn và vi phạm quy tắc này.

Mục tiêu:

Nguyên tắc này nhằm mục đích thực thi tính nhất quán để Class cha hoặc Class con của nó có thể được sử dụng theo cùng 1 cách mà không có bất kỳ lỗi nào.

I: Interface segregation principle

Một class không nên thực hiện một interface mà nó không dùng đến một phương thức hoặc không nên phụ thuộc vào một phương thức mà nó không sử dụng. Để làm điều này, thay vì sử dụng một interface lớn bao trùm chúng ta tách thành nhiều interface khác nhau.
SOLID Principles - I: Interface segregation principle

Như đã biết, 1 Class implement từ 1 interface sẽ phải thực hiện override lại tất cả các phương thức method() của interface này, và có thể có những phương thức method() trong interface mà class này không dùng đến. Ví dụ:

interface IWorkable
{
    string CanSpinAround();
    string CanRotateArm();
}

class RobotA : IWorkable
{
    public string CanSpinAround()
    {
        return "Robots that can spin around";
    }
    
    public string CanRotateArm() 
    {
        return "Robots that can rotate arm";
    }
}

class RobotB : IWorkable
{
    public string CanSpinAround()
    {
        throw new NotImplementedException();
    }
    
    public string CanRotateArm() 
    {
        return "Robots that can rotate arm";
    }
}

Sự dư thừa ở ví dụ trên đã hiện lên rõ ràng, và chúng ta sẽ tối ưu lại bằng cách tách interface tổng thành các interface nhỏ hơn:

interface ISpinAround()
{
    string CanSpinAround();
}

interface IRotateArm()
{
    string CanRotateArm();
}

class RobotA : ISpinAround, IRotateArm
{
    public string CanSpinAround()
    {
        return "Robots that can spin around";
    }
    
    public string CanRotateArm() 
    {
        return "Robots that can rotate arm";
    }
}

class RobotB : IRotateArm
{
    public string CanRotateArm() 
    {
        return "Robots that can rotate arm";
    }
}

Mục tiêu:

Nguyên tắc này nhằm mục đích là chia 1 tập hợp các hành động thành các tập nhỏ hơn để Class CHỈ thực hiện tập hợp các hành động mà nó yêu cầu.

D: Dependency Inversion Principle

- 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 sự trừu tượng (abstraction).
- Trừu tượng (Interface/Abstraction) không nên phụ thuộc vào chi tiết, mà ngược lại.
SOLID Principles - D: Dependency Inversion Principle

Đầu tiên, nên hiểu vài thuật ngữ sau:

  • 1 module cấp cao (Class) là 1 module phụ thuộc vào module khác.
  • Trừu tượng (Abstract) là 1 cái gì đó không hoàn toàn cụ thể. Nó chỉ là 1 bộ khung của 1 cái gì đó mà không có bản triển khai cụ thể. Vì vậy, sự trừu tượng trong lý thuyết có nghĩa là tạo ra 1 interface hoặc 1 abstract class không cụ thể.

Nguyên tắc này có thể hiểu:

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 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ới hình ảnh ví dụ trên, thay vì gắn trực tiếp dao cắt pizza cho robot (module cấp cao là robot sẽ phụ thuộc vào module cấp thấp là dao cắt pizza) thì chúng ta sẽ gắn một cái adapter cho robot. Adapter này không chỉ cho phép gắn dao cắt pizza cho robot mà có thể gắn bất cứ thứ gì, vd: tua vít, dao cắt bánh, con lăn sơn nước... miễn là chúng được thiết kế phù hợp để gắn vào adapter.

Trong code cũng như vậy, các bạn có thể xem qua ví dụ này nhé:

interface IRobotRepository
{
    IList<Robot> GetList();
    Robot GetById(int id);
}

class RobotRepository : IRobotRepository
{
    public IList<Robot> GetList()
    {
        //code
    }
    
    public Robot GetById(int id)
    {
        //code
    }
}

class RobotService
{
    private readonly IRobotRepository _robotRepository;
    
    public RobotService(IRobotRepository robotRepository)
    {
        _robotRepository = robotRepository;
    }
}

Bạn có thể sử dụng IoC để quản lý sự phụ thuộc, nó sẽ giúp bạn khởi tạo thể hiện cho các phụ thuộc và quản lý vòng đời cho chúng.

Mục tiêu:

Nguyên tắc này nhằm mục đích giảm sự phụ thuộc của Module cấp cao vào Module cấp thấp bằng việc sử dụng interface.

Kết luận

Cuối cùng mình cũng giải thích xong được 5 nguyên tắc của SOLID priciples. Qua bài viết này, mình hy vọng sẽ giúp bạn hiểu rõ về SOLID priciples và áp dụng thành công vào dự án của bạn.

Tham khảo

https://medium.com/backticks-tildes/the-s-o-l-i-d-principles-in-pictures-b34ce2f1e898

PrinciplesLập Trình C#