SOLID là tập hợp năm nguyên tắc thiết kế phần mềm hướng đối tượng được giới thiệu bởi Robert C. Martin (còn được gọi là Uncle Bob). Các nguyên tắc này giúp phát triển phần mềm dễ dàng mở rộng, bảo trì, và linh hoạt hơn. Áp dụng đúng các nguyên tắc SOLID giúp mã nguồn trở nên sạch sẽ, dễ hiểu và tránh được các vấn đề phức tạp trong dài hạn. SOLID là viết tắt của năm nguyên tắc sau:

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

Nguyên tắc SRP quy định rằng một lớp chỉ nên có một lý do duy nhất để thay đổi, tức là một lớp chỉ nên thực hiện một nhiệm vụ duy nhất hoặc chịu trách nhiệm cho một chức năng cụ thể trong hệ thống. Mỗi lớp nên chỉ tập trung vào một trách nhiệm và làm tốt nhiệm vụ đó.

Ví dụ: Nếu bạn có một lớp Employee, lớp này không nên vừa chịu trách nhiệm về tính toán lương, vừa chịu trách nhiệm về lưu trữ dữ liệu nhân viên vào cơ sở dữ liệu. Thay vào đó, bạn nên tách riêng hai chức năng này ra thành các lớp khác nhau, ví dụ lớp PayrollCalculator để tính toán lương và lớp EmployeeRepository để lưu trữ dữ liệu.

class Employee {
    private String name;
    private String position;
    private int salary;

    // Trách nhiệm duy nhất là quản lý thông tin nhân viên
}

class PayrollCalculator {
    public int calculateSalary(Employee employee) {
        // Trách nhiệm duy nhất là tính toán lương
        return employee.getSalary();
    }
}

class EmployeeRepository {
    public void save(Employee employee) {
        // Trách nhiệm duy nhất là lưu trữ dữ liệu nhân viên
    }
}

Open/Closed Principle (OCP) – Nguyên tắc mở rộng nhưng đóng cho sửa đổi

Nguyên tắc OCP quy định rằng các thực thể phần mềm (lớp, module, hàm) nên mở rộng được nhưng đóng cho việc sửa đổi. Điều này có nghĩa là bạn có thể mở rộng chức năng của hệ thống bằng cách thêm mã mới, nhưng không nên thay đổi mã hiện có. Điều này giúp mã hiện tại không bị ảnh hưởng bởi việc thêm tính năng mới và tránh gây ra lỗi không mong muốn.

Ví dụ: Khi bạn muốn thêm loại hình phương tiện mới trong ứng dụng quản lý phương tiện, bạn nên tạo lớp mới mà không thay đổi mã của lớp hiện tại.

abstract class Vehicle {
    abstract void drive();
}

class Car extends Vehicle {
    @Override
    void drive() {
        System.out.println("Driving a car");
    }
}

class Bicycle extends Vehicle {
    @Override
    void drive() {
        System.out.println("Riding a bicycle");
    }
}

// Thêm phương tiện mới mà không thay đổi lớp Vehicle hiện tại
class Truck extends Vehicle {
    @Override
    void drive() {
        System.out.println("Driving a truck");
    }
}

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

Nguyên tắc LSP yêu cầu rằng các lớp con phải có thể thay thế cho các lớp cha mà không làm thay đổi tính đúng đắn của chương trình. Nói cách khác, nơi nào sử dụng lớp cha, bạn có thể thay thế bằng lớp con mà chương trình vẫn hoạt động chính xác.

Ví dụ: Nếu bạn có một lớp cha Bird và lớp con Penguin, thì mọi hành vi của lớp Penguin cũng nên có ý nghĩa tương đương khi được sử dụng thay cho lớp Bird.

class Bird {
    public void fly() {
        System.out.println("Bird is flying");
    }
}

class Sparrow extends Bird {
    @Override
    public void fly() {
        System.out.println("Sparrow is flying");
    }
}

// Penguin không thể bay, nếu kế thừa Bird thì sẽ vi phạm LSP
class Penguin extends Bird {
    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguin can't fly");
    }
}

Trong ví dụ trên, việc lớp Penguin không thể bay nhưng lại kế thừa lớp Bird vi phạm LSP, vì mọi đối tượng Penguin sẽ không thể thay thế cho lớp Bird một cách hợp lệ.

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

Nguyên tắc ISP quy định rằng các lớp không nên bị ép buộc thực thi các phương thức mà chúng không sử dụng. Thay vì có một giao diện lớn, nên chia thành các giao diện nhỏ hơn, phù hợp với các nhu cầu cụ thể của từng lớp.

Ví dụ: Thay vì có một giao diện Machine với nhiều phương thức mà không phải tất cả lớp đều cần, bạn nên tách thành các giao diện nhỏ hơn.

interface Printer {
    void print();
}

interface Scanner {
    void scan();
}

class MultiFunctionPrinter implements Printer, Scanner {
    public void print() {
        System.out.println("Printing...");
    }

    public void scan() {
        System.out.println("Scanning...");
    }
}

class SimplePrinter implements Printer {
    public void print() {
        System.out.println("Printing...");
    }
}

Trong ví dụ này, SimplePrinter chỉ cần thực hiện chức năng in ấn và không phải kế thừa phương thức scan từ một giao diện lớn.

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

Nguyên tắc DIP quy định 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 các abstraction (giao diện hoặc lớp trừu tượng). Đồng thời, các abstraction không nên phụ thuộc vào chi tiết cụ thể, mà các chi tiết cụ thể nên phụ thuộc vào abstraction.

Ví dụ: Thay vì một lớp Worker cụ thể phụ thuộc vào lớp ProjectManager, cả hai nên phụ thuộc vào một giao diện chung.

interface Workable {
    void work();
}

class Developer implements Workable {
    public void work() {
        System.out.println("Developer is working");
    }
}

class Tester implements Workable {
    public void work() {
        System.out.println("Tester is working");
    }
}

class Project {
    private Workable worker;

    public Project(Workable worker) {
        this.worker = worker;
    }

    public void startWorking() {
        worker.work();
    }
}

Trong ví dụ này, Project không phụ thuộc vào lớp Developer hoặc Tester cụ thể, mà phụ thuộc vào abstraction Workable. Điều này giúp mã dễ mở rộng hơn, vì bạn có thể thêm các lớp mới mà không cần thay đổi Project.

Kết luận

SOLID là tập hợp các nguyên tắc giúp thiết kế phần mềm dễ bảo trì, mở rộng và tránh những vấn đề phức tạp trong phát triển phần mềm. Áp dụng SOLID trong thiết kế hệ thống giúp mã dễ đọc, dễ kiểm tra và dễ bảo trì hơn, từ đó cải thiện hiệu suất làm việc của các nhóm phát triển phần mềm trong dài hạn.