+15

SOLID - nguyên lý bạn nên biết để trở thành dev có tâm, có tầm

Nguyên tắc SOLID trong lập trình là một tập hợp các nguyên tắc thiết kế phần mềm. Các nguyên tắc này giúp tạo ra mã nguồn linh hoạt, dễ bảo trì và mở rộng về sau. Vậy SOLID là gì cùng mình phân tích phía dưới nhé:

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

  • Nguyên tắc này nói rằng: Mỗi lớp nên chỉ có một lý do để thay đổi và chỉ thực hiện một chức năng cụ thể. Điều này giúp giữ cho mã nguồn dễ đọc, bảo trì và hiểu.
  • Ví dụ:
class Employee {
    private String name;
    private double salary;
    
    // methods to get/set employee details
}

class EmployeeDatabase {
    void saveEmployee(Employee emp) {
        // save employee to database
    }
}

Khi bạn có một lớp Employee, nó chỉ chịu trách nhiệm quản lý thông tin về một nhân viên cụ thể mà không thực hiện các tác vụ không liên quan như việc lưu trữ dữ liệu vào cơ sở dữ liệu. Lớp EmployeeDatabase mới là lớp thực hiện điều đó.

2. O - Open/Closed Principle (Nguyên tắc mở rộng)

  • Nguyên tắc này là : Phần mềm nên mở rộng được mà không cần phải sửa đổi mã nguồn hiện tại. Mở rộng có thể được thực hiện thông qua kế thừa hoặc sử dụng interfaces, không làm thay đổi mã nguồn đã tồn tại.
  • Ví dụ:
interface Shape {
    double area();
}

class Circle implements Shape {
    private double radius;

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    private double width;
    private double height;

    @Override
    public double area() {
        return width * height;
    }
}

Sử dụng kế thừa và interface để mở rộng chức năng của một hệ thống mà không cần sửa đổi mã nguồn hiện có.

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

  • Các đối tượng của lớp con nên có thể thay thế được cho các đối tượng của lớp cha mà không làm thay đổi tính đúng đắn của chương trình. Điều này đảm bảo tính nhất quán khi thay thế các đối tượng.
  • Ví dụ:
class Bird {
    // Chim thì có lông, chim nào không có lông không ta :v
    public boolean areThereFeathers() {
        return true;
    }
    
    public void fly() {
        // logic for flying
    }
}

class Sparrow extends Bird {
    // Sparrow can fly
}

class Ostrich extends Bird {
    public void fly() {
        throw new UnsupportedOperationException("Ostrich cannot fly");
    }
}

Như chúng ta thấy con chim sẻ (Sparrow) là con chim có thể bay nhưng con đà điểu (Ostrich) là một họ nhà chim nhưng không thể bay được, nên khi chúng ta khai báo là:

Bird sparrow = new Sparrow();// gọi phương thức sparrow.fly() thì bay được
Bird ostrich = new Ostrich();// còn gọi phương thức ostrich.fly() thì không bay được @@

Thường thì chúng ta sẽ làm như vậy đúng không, nhưng như vậy thì sẽ vi phạm nguyên tắc liskov rồi đó, vì vốn dĩ 2 con chim này là 2 loại khác nhau nên kế thừa từ Bird có phương thức fly() là không đúng, để sửa lại cho đúng với nguyên tắc này thì chúng ta có thể làm như sau:

class Bird {
    // Chim thì có lông
    public boolean areThereFeathers() {
        return true;
    }
}
class FlyingBird extends Bird {
    public void fly() {
        System.out.println("I can Fly");
    }
}
class RunningBird extends Bird {
    public void run() {
        System.out.println("I can Run");
    }
}
class Sparrow extends FlyingBird {
    // Sparrow can fly
}

class Ostrich extends RunningBird {
   // Ostrich can run
}

Vì vốn dĩ họ nhà chim có 3 loại:

  • Chim chạy (Running bird)
    
  • Chim bay (Flying bird)
    
  • Chim bơi (Swimming bird)
    

nên việc không chọn ra được những thuộc tính chung nhất để làm class cha sẽ rất dễ vi phạm nguyên tắc này như ví dụ lúc đầu.

4. I - Interface Segregation Principle (Nguyên tắc tách biệt giao diện)

  • Một lớp không nên bắt buộc triển khai các phương thức mà nó không sử dụng. Các interfaces nên được chia thành các interfaces nhỏ hơn để đảm bảo rằng lớp chỉ triển khai những gì cần thiết cho nhiệm vụ của mình.
  • Ví dụ:
interface Printer {
    void print();
    void scan();
}
// thay vì dùng 1 interface Printer thì tạo ra 2 interface riêng biết rồi triển khai cả 2

interface SimplePrinter {
    void print();
}

interface Scanner {
    void scan();
}

Một cái máy in đơn giản thì chỉ nên có chức năng in thôi, chức năng quét thì máy có máy không :v

5. D - Dependency Inversion Principle (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 chi tiết cấp thấp. Cả hai nên phụ thuộc vào abstraction. Điều này giúp giảm độ ràng buộc và tạo ra hệ thống linh hoạt, có thể thay đổi cài đặt mà không ảnh hưởng đến module khác.
  • Ví dụ:
interface DataAccess {
    void save();
}

class Database implements DataAccess {
    @Override
    public void save() {
        // save to database
    }
}

class Customer {
    private DataAccess dataAccess;

    public Customer(DataAccess dataAccess) {
        this.dataAccess = dataAccess;
    }

    public void save() {
        dataAccess.save();
    }
}

Trong ví dụ trê, Customer không phụ thuộc trực tiếp vào Database, mà thay vào đó phụ thuộc vào một interface DataAccess.

Tóm tắt về SOLID

Tên Định nghĩa
Nguyên tắc Đơn trách nhiệm (SRP) Mỗi lớp nên chỉ có một lý do để thay đổi và chỉ thực hiện một chức năng cụ thể. Điều này giúp giữ cho mã nguồn dễ đọc, bảo trì và hiểu.
Nguyên tắc Mở rộng (OCP) Phần mềm nên mở rộng được mà không cần phải sửa đổi mã nguồn hiện tại. Mở rộng có thể được thực hiện thông qua kế thừa hoặc sử dụng interfaces, không làm thay đổi mã nguồn đã tồn tại.
Nguyên tắc Thay đổi không ảnh hưởng đến Người dùng (LSP) Các đối tượng của lớp con nên có thể thay thế được cho các đối tượng của lớp cha mà không làm thay đổi tính đúng đắn của chương trình. Điều này đảm bảo tính nhất quán khi thay thế các đối tượng.
Nguyên tắc Tách Biệt (ISP) Một lớp không nên bắt buộc triển khai các phương thức mà nó không sử dụng. Các interfaces nên được chia thành các interfaces nhỏ hơn để đảm bảo rằng lớp chỉ triển khai những gì cần thiết cho nhiệm vụ của mình.
Nguyên tắc Phụ thuộc vào Abstraction, không phải Implementations (DIP) Các module cấp cao không nên phụ thuộc vào chi tiết cấp thấp. Cả hai nên phụ thuộc vào abstraction. Điều này giúp giảm độ ràng buộc và tạo ra hệ thống linh hoạt, có thể thay đổi cài đặt mà không ảnh hưởng đến module khác.

Lời kết

Thực ra phấn đấu chính là bình thản sống mỗi ngày, làm thật tốt những việc mình đang phải làm, không trì hoãn, không than phiền, không thoái thác, không lười biếng. Mỗi ngày cố gắng thêm một chút xíu, mới có thể gộp thành hàng ngàn dũng khí, mang theo sự kiên trì, dẫn bạn đến nơi bạn muốn đến. Chúc bạn năm mới thành công ❤️


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.