+2

Tìm hiểu SOLID trong OOP: 5 nguyên tắc quan trọng & cách sử dụng đúng

Mở đầu

  • Trong bài viết trước chúng ta đã tìm hiểu về OOP - lập trình hướng đối tượng - một mẫu hình lập trình vô cùng phổ biến và được sử dụng rộng rãi.
  • Trải qua nhiều năm phát triển mã nguồn theo hướng đối tượng, các lập trình viên đã đúc kết và xây dựng nên một bộ các nguyên tắc chung(principles), giúp cho quá trình viết code về sau này trở nên linh hoạt, dễ mở rộng hơn.
  • Vậy các nguyên tắc này là gì? Và tại sao các nguyên tắc này lại quan trọng trong lập trình hướng đối tượng? Cùng mình tìm hiểu trong bài viết này nhé!

SOLID là gì?

  • SOLID là một tập hợp gồm 5 nguyên tắc thiết kế phần mềm giúp viết mã nguồn dễ bảo trì, dễ mở rộng và có tính linh hoạt cao trong lập trình hướng đối tượng (OOP - Object-Oriented Programming).

Nguồn gốc của SOLID

  • Vào năm 2000, Robert C. Martin (Uncle Bob) đã lần đầu tiên đề xuất đến các nguyên tắc này trong bài báo "Design Principles and Design Patterns".
  • Trong đó, có 2 nguyên tắc có nguồn gốc từ các nghiên cứu trước đó là:
  1. Liskov Substitution Principle (LSP) của Barbara Liskov vào năm 1987.
  2. Open/Closed Principle (OCP) của và Bertrand Meyer vào năm 1988.
  • Sau khi Robert C. Martin giới thiệu các nguyên tắc, Michael Feathers đã xếp lại chữ cái đầu tiên của từng nguyên tắc để tạo thành từ "SOLID", giúp nó trở nên dễ nhớ hơn và dễ quảng bá trong cộng đồng lập trình viên.
  • Từ đó, "SOLID principles" trở thành một thuật ngữ phổ biến, được sử dụng rộng rãi trong các tài liệu và khóa học về lập trình hướng đối tượng (OOP).

5 nguyên tắc cốt lỗi

  • Mỗi chữ cái trong từ khóa SOLID chính là kí tự bắt đầu cho mỗi nguyên tắc:

1. S - Single Responsibility Principle (SRP)

  • Single Responsibility Priciple - nguyên tắc đơn nhiệm: mỗi lớp chỉ nên chịu trách nhiệm về một chức năng duy nhất trong hệ thống.

"A class should have only one reason to change."

  • Ví dụ: Thiết kế một tính năng quản lý nhân viên gồm các chức năng sau: Quản lý thông tin nhân viên, tính toán lương nhân viên, lưu thông tin nhân viên vào cơ sở dữ liệu, xuất báo cáo về nhân viên.
csharp

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }

    public void CalculateSalary()
    {
        // Tính toán lương nhân viên
    }

    public void SaveToDatabase()
    {
        // Lưu thông tin nhân viên vào cơ sở dữ liệu
    }

    public void GenerateReport()
    {
        // Xuất báo cáo về nhân viên
    }
}
  • Ví dụ trên đã vi phạm quy tắc (SRP) - lớp Employee đã có nhiều hơn một nhiệm vụ:
Nhiệm vụ
Quản lý dữ liệu nhân viên (Id, Name)
Tính toán lương - CalculateSalary()
Lưu vào cơ sở dữ liệu - SaveToDatabase()
Xuất báo cáo - GenerateReport()
  • Cách áp dụng đúng quy tắc (SRP)
csharp

/ Lớp Employee chỉ giữ thông tin nhân viên
public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
}

// Lớp SalaryCalculator chỉ chịu trách nhiệm tính lương
public class SalaryCalculator
{
    public double CalculateSalary(Employee employee)
    {
        // Tính toán lương nhân viên
    }
}

// Lớp EmployeeRepository chỉ chịu trách nhiệm lưu dữ liệu vào DB
public class EmployeeRepository
{
    public void Save(Employee employee)
    {
        // Lưu vào database
    }
}

// Lớp ReportGenerator chỉ chịu trách nhiệm tạo báo cáo
public class ReportGenerator
{
    public void GenerateReport(Employee employee)
    {
        // Xuất báo cáo về nhân viên
    }
}

2. O - Open/Closed Principle (OCP)

  • Open/Closed Principle (OCP) - Nguyên tắc mở/đóng: có thể mở rộng một class/module mà không cần sửa đổi mã nguồn gốc của nó.

"A module should be open for extension but closed for modification."

  • Ví dụ: Thiết kế một tính năng cho hỗ trợ người dùng thanh toán bằng 2 hình thức ví điện tử: ZaloPay, MoMo.
csharp

public class PaymentProcessor
{
    public void ProcessPayment(string paymentMethod)
    {
        if (paymentMethod == "ZaloPay")
        {
            // Xử lý thanh toán qua ZaloPay
        }
        else if (paymentMethod == "MoMo")
        {
            // Xử lý thanh toán qua MoMo
        }
    }
}
  • Lúc này khách hàng yêu cầu có thêm 1 hình thức thanh toán là: VNPay.
  • Ví dụ trên đã vi phạm quy tắc (OCP) - lớp PaymentProcessor lúc này cần phải chỉnh sửa thêm 1 statement else để xử lý thanh toán VNPay.
  • Cách áp dụng đúng quy tắc (OCP):
csharp

public interface IPaymentMethod
{
    void ProcessPayment();
}

public class ZaloPayMethod: IPaymentMethod
{
    public void ProcessPayment() { /* Xử lý thanh toán qua ZaloPay */ }
}

public class MoMoMethod: IPaymentMethod
{
    public void ProcessPayment() { /* Xử lý thanh toán qua MoMo */ }
}

// tạo một lớp mới cho chức năng thanh toán qua VnPay
public class VnPayMethod: IPaymentMethod
{
    public void ProcessPayment() { /* Xử lý thanh toán qua VnPay */ }
}

public class PaymentProcessor(IPaymentMethod paymentMethod)
{
    private readonly IPaymentMethod _paymentMethod = paymentMethod;

    public void Process()
    {
        _paymentMethod.ProcessPayment();
    }
}

3. L - Liskov Substitution Principle (LSP)

  • Liskov Substitution Principle (LSP) - Nguyên tắc thay thế Liskov: Một lớp con phải có thể thay thế được lớp cha của nó mà không làm thay đổi tính đúng đắn của chương trình.

"Subclasses should be substitutable for their base classes."

  • Ví dụ: Thiết kế một chương trình mô tả về tiếng kêu, khả năng bay của các loài chim sau: chim đại bàng, chim cánh cụt
csharp

public class Bird
{
    public virtual void Sound() 
    {
        Console.WriteLine("Bird sound...");
    }

    public virtual void Fly()
    {
        Console.WriteLine("Flying...");
    }
}

public class Eagle : Bird
{
    public override void Sound() 
    {
        Console.WriteLine("Eagle sound...");
    }

    public override void Fly()
    {
        Console.WriteLine("Eagle is flying.");
    }
}

public class Penguin : Bird
{
    public override void Sound() 
    {
        Console.WriteLine("Penguin sound...");
    }
    
    // Penguin không thể bay, vi phạm LSP nếu kế thừa phương thức Fly()
    public override void Fly()
    {
        throw new InvalidOperationException("Penguin cannot fly!");
    }
}
  • Ví dụ trên đã vi phạm quy tắc (LSP) - Lớp Penguin kế thừa phương thức Fly từ lớp Bird nhưng chim cánh cụt không thể bay.
  • Điều này cho thấy lớp Peguin không thể thay thế lớp Bird và đã làm thay đổi hành vi của hệ thống.
  • Cách áp dụng đúng quy tắc (LSP):
csharp

public interface IFlyable
{
    void Fly();
}

public class Bird
{
    public virtual void Sound() 
    {
        Console.WriteLine("Bird sound...");
    }
}

public class Eagle : Bird, IFlyable
{
    public override void Sound() 
    {
        Console.WriteLine("Penguin sound...");
    }

    public void Fly()
    {
        Console.WriteLine("Eagle is flying.");
    }
}

public class Penguin : Bird
{
    public override void Sound() 
    {
        Console.WriteLine("Penguin sound...");
    }
    
    // Penguin không cần implement IFlyable vì nó không bay
}

4. I - Interface Segregation Principle (ISP)

  • Interface Segregation Principle (ISP) - Nguyên tắc phân tách giao diện: Các lớp không nên bị ép buộc phụ thuộc vào những phương thức mà chúng không sử dụng.

"Many client specific interfaces are better than one general purpose interface."

  • Ví dụ: Thiết kế một chương trình mô tả các chức năng lái xe, đổ xăng, sạc pin cho các loại xe: xe điện và xe hơi.
csharp

public interface IVehicle {
    void Drive();
    void Recharge();
    void Refuel();
}

public class Car: IVehicle {
    public void Drive() 
    {
        Console.WriteLine("Driving car...");
    }
    
     public void Refuel() 
    {
        Console.WriteLine("Refueling...");
    }
    
    // xe hơi không thể sạc điện - vi phạm ISP
    public void Recharge() 
    {
         throw new InvalidOperationException("Car cannot recharge!");
    }
}

public class ElectricCar: IVehicle {
    public void Drive() 
    {
        Console.WriteLine("Driving electric car...");
    }
    
     public void Recharge() 
    {
        Console.WriteLine("Recharging...");
    }
    
    // xe điện không thể đổ xăng - vi phạm ISP
    public void Refuel() 
    {
         throw new InvalidOperationException("ElectricCar cannot Refuel!");
    }
}

  • Ví dụ trên đã vi phạm quy tắc (ISP) - Lớp CarLớp ElectricCar đều chứa các phương thức không được sử dụng lần lượt là Refuel()Drive().
  • Vì xe điện không thể nạp xăng và xe hơi không thể sạc điện.
  • Cách áp dụng đúng quy tắc (ISP):
csharp

// Interface cho phương tiện có thể di chuyển
public interface IVehicle
{
    void Drive();
}

// Interface cho phương tiện có thể đổ xăng
public interface IRefuelable
{
    void Refuel();
}

// Interface cho phương tiện có thể sạc điện
public interface IRechargeable
{
    void Recharge();
}

// Lớp xe hơi chỉ cần Drive và Refuel, không cần Recharge
public class Car : IVehicle, IRefuelable
{
    public void Drive()
    {
        Console.WriteLine("Driving car...");
    }

    public void Refuel()
    {
        Console.WriteLine("Refueling...");
    }
}

// Lớp xe điện chỉ cần Drive và Recharge, không cần Refuel
public class ElectricCar : IVehicle, IRechargeable
{
    public void Drive()
    {
        Console.WriteLine("Driving electric car...");
    }

    public void Recharge()
    {
        Console.WriteLine("Recharging...");
    }
}

5. D - Dependency Inversion Principle (DIP)

  • Dependency Inversion Principle (DIP) - Nguyên tắc đảo ngược sự phụ thuộc: 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.

"Depend upon Abstractions. Do not depend upon concretions."

  • ví dụ: xây dựng một hệ thống gửi thông báo qua Email, SMS.
csharp

public interface NotificationService
{
    void Send();
}

public class SMSNotification : NotificationService
{
    public void Send()
    {
        Console.WriteLine("Send SMS notification...");
    }
}

public class EmailNotification : NotificationService
{
    public void Send()
    {
        Console.WriteLine("Send Email notification...");
    }
}


public class NotificationProcess
{
    // vi phạm (DIP) - Lớp NotificationProcess phụ thuộc trực tiếp vào SMSNotification
    private readonly NotificationService _notificationService = new SMSNotification();

    public void ProcessNotification()
    {
        _notificationService.Send();
    }
}
  • Ví dụ trên đã vi phạm quy tắc (DIP) - Lớp NotificationProcess phụ thuộc trực tiếp vào SMSNotification. Nếu muốn đổi sang EmailNotification, ta phải sửa đổi trực tiếp code trong NotificationProcess, gây khó bảo trì và mở rộng.
  • Cách áp dụng đúng quy tắc (DIP):
csharp

// Lớp NotificationProcess không phụ thuộc vào implementation cụ thể, mà nhận qua Dependency Injection
public class NotificationProcess(INotificationService notificationService)
{
    private readonly INotificationService _notificationService = notificationService;

    public void ProcessNotification()
    {
        _notificationService.Send();
    }
}

// Không còn phụ thuộc cứng giữa NotificationProcess và SMSNotification
INotificationService notificationService = new SMSNotification(); // Hoặc new EmailNotification()
NotificationProcess process = new NotificationProcess(notificationService);
process.ProcessNotification();

Kết luận

  • SOLID là một khái niệm quan trọng đối với các các lập trình viên, hầu hết các dự án khi chúng ta đi làm thực tế đều áp dụng lập trình hướng đối tượng - OOP bên trong.
  • Việc nắm rõ về nguyên lý SOLID giúp việc lập trình đạt được hiệu suất cao, dễ dàng mở rộng, tái sử dụng và bảo trì hơn.
  • Hy vọng qua bài viết, với các thông tin mình cung cấp kèm theo các ví dụ minh họa sẽ giúp các bạn có một cái nhìn rõ nét hơn về SOLID trong OOP, từ đó có thể áp dụng vào các dự án thực tế và dễ dàng nhận biết được tín hiệu của các nguyên tắc này khi đọc và review code 🥳

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í