+3

Nguyên tắc SOLID trong lập trình Java

Giới thiệu

Nguyên tắc SOLID được Robert C. Martin giới thiệu vào năm 2000, SOLID là viết tắt của 5 nguyên tắc, các nguyên tắc này được sử dụng trong lập trình hướng đối tượng(OOP) giúp hệ thống được tổ chức linh hoạt, dễ hiểu, dễ kiểm tra khi có vấn đề.

Nội dung

Nguyên tắc SOLID gồm:

  • Single Responsibility
  • Open/Closed
  • Liskov Substitution
  • Interface Segregation
  • Dependency Inversion

1. Single Responsibility

"There should never be more than one reason for a class to change." In other words, every class should have only one responsibility.

public class A {
    public void doSomeThing1() {
        //code
    }
    public void doSomeThing2() {
        //code
    }
}

Ví dụ đơn giản trên mình vừa đưa ra, Class A đã vi phạm nguyên tắc đầu tiên đó là thực hiện cùng lúc 2 nhiệm vụ. Đây là ví dụ đơn giản nhưng trong các dự án lớn sẽ khó quản lí, sử lí khi gặp lỗi.

Giải pháp

Để phù hợp với nguyên tắc, Class A nên được tách thành hai Class thực hiện từng nhiệm vụ

public class A1 {
     public void doSomeThing1() {
        //code
    }
}
public class A2 {
     public void doSomeThing2() {
        //code
    }
}

Như vậy, bất kì khi nào một hoạt động nào đó gặp lỗi hay cần sửa đổi, thêm chức năng, chúng ta sẽ giải quyết vấn đề dễ dàng hơn.

Open/Closed

"Software entities ... should be open for extension, but closed for modification."

public class Calculator {
    public double result(MathOperations mathOperation) {
        double calculationResults;
        if (mathOperation.type("Addition")) {
            // Calculate addition
        } else if (mathOperation.type("Subtraction")) {
            // Calculate subtraction
        }
        return calculationResults;
}

Phương thức result thực hiện tính toán và trả về kết quả theo từng phép toán MathOperations. Vấn đề là khi muốn tính toán một phép tính khác chúng ta phải sửa lại code trong class. Để tối ưu chúng ta nên tạo một Interface Calculator và các class tính toán kế thừa từ Interface đó.

public interface Calculator {
    double result();
}
public class Add implements Calculator {
   @Override
    public double result() {
         // Calculate addition
    }
}
public class Subtract implements Calculator {
    @Override
    public double result() {
         // Calculate subtraction
    }
}

Với cách làm trên chương trình trong tương lai sẽ "mở để mở rộng" khi cần thêm phương thức mới và "đóng để sửa đổi" khi sửa đổi một phương thức nào đó mà không làm ảnh hưởng đến chương trình.

Liskov Substitution

"Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it."

Các khái niệm mình lấy trên Wikipedia , theo mình hiểu nguyên tắc nói rằng ví dụ class A khi tham chiếu đến class B (base class) thì A cũng có thể thay thế cho B thực hiện các hoạt động mà B đang làm

public abstract class A {
    //Xử lí dữ liệu truyền vào và trả về một Object
    abstract Object task(Data data);
}
// Tuỳ vào mỗi class, logic code trong method task khác nhau.
public class B extends A {
    @Override
    public Object task(Data data) {

    }
}

public class C extends A {
    @Override
    public Object task(Data data) {
        //code
        return object;
    }
}

Ví dụ trên mình tạo một abstract class A có phương thức task, các class B,C khi kế thừa phải ghi đè phương thức task, nhưng trong class B có các thuộc tính không thể sử dụng để xử lý Data trong phương thức task vì vậy mình đã để trống nó, còn class C có thể xử lý Data để trả về Object.

Vấn đề sẽ xảy ra khi có một class X muốn sử dụng class A để lấy Ọbject sau khi xử lý Data. Class A chỉ là trừu tượng(abstract) các class B,C là các class cụ thể của A nên sẽ được dùng để phục vụ anh X

  • Khi X dùng C, không có vấn đề vì C có thể thay thế A xử lý Data.
  • Khi X dùng B, do thuộc tính của B không thể dùng trong phương thức task để xử lý dữ liệu cho nên B không trả về object, X dùng B sẽ bị lỗi. Điều này đã vi phạm nguyên tắc do B không thể thay thế A làm nhiệm vụ

Để phù hợp với nguyên tắc class B nên được kế thừa sang một abstract khác

public abstract class A {
    abstract Object task(Data data);
}
public abstract class D {
    abstract void doSomething();
}
public class C extends A {
    @Override
    public Object task(Data data) {
        //code
        return object;
    }
}
public class B extends D {
    @Override
    public void doSomething() {
        // code
    }

Tổng quát, nguyên tắc này khá trừu tượng theo mình thấy nguyên tắc này bị vi phạm khi chúng ta kế thừa một class đến một class khác, nhìn bề ngoài nó có thể là một class con, có thể kế thừa nhưng có thể nó không thay thế được class cha thực hiện các nhiệm vụ.

Interface Segregation

"Clients should not be forced to depend upon interfaces that they do not use."

Theo nguyên tắc này chúng ta nên thiết kế chương trình với các interface nhỏ, các class kế thừa một interface chỉ sử dụng các method cần thiết của interface đó.

public interface ShapeCalculator {
    double calculatorArea(); //Tính diện tích
    double calculatorVolume(); //Tính thể tích
    double calculatorSurroundingArea(); //Tính diện tích xung quanh
    ...
}
public class Square implements ShapeCalculator {
    @Override
    public double calculatorArea() {
        //code
        return area;
    }
    @Override
    public double calculatorVolume() {

    }
    ...
}

Nếu "ShapeCalculator" có rất nhiều method, class "Square" sẽ bị phình to do chỉ cần sử một method để tính diện tích các method còn lại không được dùng đến. Để khắc phục lỗi này ta sẽ tạo ra các interface khác nhau chứa các method phục vụ từng mục đích như ICalculatorArea, ICalculatorVolume, ICalculatorSurroundingArea, ... Khi cần các class có thể implement các interface đó.

Dependency Inversion

"High-level modules should not import anything from low-level modules. Both should depend on abstractions."
"Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions."

public class ReadFile {
    private PdfFile pdfFile;
    private WordFile wordFile;
    ... //Có thể có nhiều dạng file khác

    public ReadFile(PdfFile file) {
        this.pdfFile = file;
    }
     public ReadFile(WorldFile file) {
        this.wordFile = file;
    }
    //Với mỗi loại file chương trình phải có các method theo từng loại để đọc nội dung file
    public String pdfFileContent() {
        return pdfFile.getData();
    }
    public String wordFileContent() {
        return wordFile.getData();
    }
}

public class PdfFile {
    private String data;
    public PdfFile(String input) {
         this.data = input;
    }
    public String getData() {
        return this.data;
    }
}

public class WordFile {
    private String data;
    public WordFile(String input) {
         this.data = input;
    }
    public String getData() {
        return this.data;
    }
}

Chương trình trên class ReadFile với level cao hơn đang phải phụ thuộc vào các class PdfFile, WordFile ở level thấp hơn và khi chương trình muốn đọc một loại file khác, ta phải sửa code, thêm method và làm chương trình trở nên phức tạp. Để phù hợp các bạn tạo một interface để các class chi tiết(details) theo nguyên tắc giao tiếp với nhau qua đó.

public interface FileContent {
    public String export();
}

public class ReadFile {
    private FileContent fileContent;

    public ReadFile(FileContent file) {
        this.FileContent = file;
    }

    public String content() {
        return fileContent.export();
    }
}

public class PdfFile implements FileContent {
     private String data;

    public PdfFile(String input) {
         this.data = input;
    }
    @Override
    public String export() {
        return this.data;
    }
}

public class WordFile implements FileContent {
     private String data;

    public WordFile(String input) {
         this.data = input;
    }
    @Override
    public String export() {
        return this.data;
    }
}

public class Test {
    public static void main() {
        FileContext file = new PdfFile();
        ReadFile readFile = new ReadFile(file);
        System.out.print(readFile.content());
    }
}

PdfFileWordFile bây giờ có thể giao tiếp với ReadFile thông qua interface FileContent, đồng thời bây giờ ReadFile chỉ phụ thuộc vào interface không phải các class cụ thể như trước.

Tổng kết

SOLID là một nguyên tắc quan trọng, rất cần thiết trong lập trình hướng đối tượng, hi vọng bài viết của mình giải đáp được một số thắc mắc của các bạn.


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í