SOLID设计原则

191 阅读6分钟

SOLID设计原则

“SOLID”是五个面向对象程序设计和架构的基本原则的缩写,它有助于提高一个系统的可维护性和可扩展性。以下是这五个原则的简要介绍:

  1. S — 单一职责原则 (Single Responsibility Principle, SRP)

    这个原则认为一个类应该只有一个引起它变化的原因。换句话说,一个类应该只负责一项职责,这样可以使系统更容易理解和维护,也能降低类之间的耦合度。

    反例:

    在这个反例中,UserHandler 类既负责管理用户的信息,又负责将用户的信息保存到数据库中,这违反了单一职责原则。

    public class UserHandler {
       public void changeUserName(String userId, String newUserName) {
           // 修改用户的名字
       }
    
       public void saveUserToDatabase(String userId) {
           // 将用户信息保存到数据库
       }
    }
    

    正例:

    在这个正例中,我们将 UserHandler 类拆分成两个类,其中 UserManager 类负责管理用户的信息,而 UserDatabase 类负责将用户的信息保存到数据库中,从而满足单一职责原则。

    public class UserManager {
       public void changeUserName(String userId, String newUserName) {
           // 修改用户的名字
       }
    }
    
    public class UserDatabase {
       public void saveUserToDatabase(String userId) {
           // 将用户信息保存到数据库
       }
    }
    

    通过将不同的职责分配给不同的类,我们遵循了单一职责原则,从而使代码更加清晰和可维护。

  2. O — 开闭原则 (Open/Closed Principle, OCP)

    根据这个原则,软件实体(类、模块、函数等等)应该对扩展开放,对修改封闭。这意味着已有的代码可以被新的代码扩展,但是不应该修改已有的代码。

    反例

    在这个反例中,我们有一个Greeter类,它有一个greet方法。每当我们想要添加一个新的问候消息时,我们都需要修改Greeter类,违反了开闭原则。

    public class Greeter {
       public String greet(String language) {
           if ("English".equals(language)) {
               return "Hello";
           } else if ("Spanish".equals(language)) {
               return "Hola";
           } else {
               return "Hello";
           }
       }
    
       public static void main(String[] args) {
           Greeter greeter = new Greeter();
           System.out.println(greeter.greet("English"));  // Output: Hello
       }
    }
    

    正例

    在这个正例中,我们使用一个接口和多个实现来遵循开闭原则。如果我们想要添加一个新的问候消息,我们只需要添加一个新的Greeter实现,而不需要修改现有的代码。

    public interface Greeter {
       String greet();
    }
    
    public class EnglishGreeter implements Greeter {
       @Override
       public String greet() {
           return "Hello";
       }
    }
    
    public class SpanishGreeter implements Greeter {
       @Override
       public String greet() {
           return "Hola";
       }
    }
    
    public class Main {
       public static void main(String[] args) {
           Greeter greeter = new EnglishGreeter();
           System.out.println(greeter.greet());  // Output: Hello
    
           greeter = new SpanishGreeter();
           System.out.println(greeter.greet());  // Output: Hola
       }
    }
    

    在这个正例中,我们可以看到Greeter接口定义了一个greet方法,并且我们有多个类实现了这个接口,每个类提供了一种不同语言的问候。这样,我们可以轻松地添加更多的语言支持,而不需要修改现有的代码。

  3. L — 里氏替换原则 (Liskov Substitution Principle, LSP)

    这个原则是指程序中的对象应该可以被它的子类型所替换,而不会影响程序的正确性。它强调的是子类型应当能够替换掉它们的父类型。

    反例

    在这个反例中,我们违反了 LSP,因为当 Bird 类型被 Penguin 类型替换时,它不能正常飞行。

    public class Bird {
       public void fly() {
           System.out.println("Bird is flying");
       }
    }
    
    public class Penguin extends Bird {
       @Override
       public void fly() {
           throw new UnsupportedOperationException("Penguins can't fly");
       }
    }
    
    public class Main {
       public static void main(String[] args) {
           Bird bird = new Penguin();
           bird.fly(); // This will throw an exception, breaking the LSP
       }
    }
    

    在反例中,Penguin 类覆盖了 Bird 类的 fly 方法并抛出了一个异常,这违反了 LSP 原则,因为现在 Penguin 无法替换 Bird 而不改变程序的正确性。正因为如此,当我们尝试调用 fly 方法时,程序会抛出一个异常。

    正例

    在这个正例中,RectangleSquare 类均遵循了 LSP,因为 SquareRectangle 的一个子类,并且可以替换 Rectangle 类而不影响程序的正确性。

    public class Rectangle {
       protected int width;
       protected int height;
    
       public int getWidth() {
           return width;
       }
    
       public void setWidth(int width) {
           this.width = width;
       }
    
       public int getHeight() {
           return height;
       }
    
       public void setHeight(int height) {
           this.height = height;
       }
    
       public int getArea() {
           return width * height;
       }
    }
    
    public class Square extends Rectangle {
       public void setWidth(int width) {
           this.width = width;
           this.height = width;
       }
    
       public void setHeight(int height) {
           this.width = height;
           this.height = height;
       }
    }
    
    public class Main {
       public static void main(String[] args) {
           Rectangle rect = new Square();
           rect.setWidth(4);
           rect.setHeight(5);
           System.out.println("Area of rectangle: " + rect.getArea()); // Output: Area of rectangle: 25
       }
    }
    
  4. I — 接口隔离原则 (Interface Segregation Principle, ISP)

    该原则指出客户端不应该依赖它不使用的接口。换言之,一个类不应该被迫实现它不使用的方法,或者说,应该将臃肿的接口分割成更小的和更具体的接口,以满足客户端的特定需求。

    反例

    在这个反例中,我们有一个“臃肿”的接口,它包含了多个方法,而不是所有的实现类都需要所有的方法。

    interface Worker {
       void work();
       void eat();
    }
    
    class HumanWorker implements Worker {
       public void work() {
           System.out.println("Human working...");
       }
    
       public void eat() {
           System.out.println("Human eating...");
       }
    }
    
    class RobotWorker implements Worker {
       public void work() {
           System.out.println("Robot working...");
       }
    
       public void eat() {
           // Robots do not eat, this method is irrelevant for RobotWorker
           throw new UnsupportedOperationException("Robots do not eat");
       }
    }
    

    在这个反例中,RobotWorker类被迫实现一个它不需要的eat方法,违反了接口隔离原则。

    正例

    在正例中,我们将原先的臃肿接口拆分成两个更小的接口,每个接口只包含相关的方法。

    interface Worker {
       void work();
    }
    
    interface Eater {
       void eat();
    }
    
    class HumanWorker implements Worker, Eater {
       public void work() {
           System.out.println("Human working...");
       }
    
       public void eat() {
           System.out.println("Human eating...");
       }
    }
    
    class RobotWorker implements Worker {
       public void work() {
           System.out.println("Robot working...");
       }
    }
    

    在这个正例中,RobotWorker只实现了它真正需要的Worker接口,而没有实现Eater接口。这符合接口隔离原则,因为它避免了不必要的依赖关系。

  5. D — 依赖倒置原则 (Dependency Inversion Principle, DIP)

    该原则鼓励我们依赖于抽象而不是具体的实现。这有助于降低系统中不同模块之间的耦合度,并提高系统的灵活性。

    该原则在依赖,依赖注入与依赖反转中有详细解释与说明。

    反例

    在下面的反例中,HighLevelModule 类直接依赖于一个具体的 LowLevelModule 类,这违反了依赖倒置原则。

    class LowLevelModule {
       void operation() {
           System.out.println("Low level operation");
       }
    }
    
    class HighLevelModule {
       private final LowLevelModule lowLevelModule = new LowLevelModule();
    
       void task() {
           lowLevelModule.operation();
       }
    }
    
    public class Main {
       public static void main(String[] args) {
           HighLevelModule highLevelModule = new HighLevelModule();
           highLevelModule.task();
       }
    }
    

    正例

    在下面的正例中,我们创建了一个 Module 接口来实现依赖倒置原则。现在,HighLevelModule 依赖于抽象,而不是一个具体的实现。

    interface Module {
       void operation();
    }
    
    class LowLevelModule implements Module {
       @Override
       public void operation() {
           System.out.println("Low level operation");
       }
    }
    
    class HighLevelModule {
       private final Module module;
    
       HighLevelModule(Module module) {
           this.module = module;
       }
    
       void task() {
           module.operation();
       }
    }
    
    public class Main {
       public static void main(String[] args) {
           Module lowLevelModule = new LowLevelModule();
           HighLevelModule highLevelModule = new HighLevelModule(lowLevelModule);
           highLevelModule.task();
       }
    }
    

    在这个正例中,HighLevelModule 类依赖于 Module 接口而不是 LowLevelModule 类,因此我们成功地实现了依赖倒置原则。这种方式使得我们可以更灵活地扩展或修改低级模块的实现,而不会影响到高级模块。