SOLID原则的坚实指南| Baeldung


在本教程中,我们将讨论面向对象设计的SOLID原则。
首先,我们将首先探讨它们出现的原因以及为什么在设计软件时应该考虑它们。然后,我们将概述每个原则以及一些示例代码以强调这一点。

SOLID原则的原因
SOLID原则首先由Robert C. Martin在2000年的论文“ 设计原则和设计模式”中概念化  。 这些概念后来由Michael Feathers构建,他们向我们介绍了SOLID的首字母缩略词。在过去的20年中,这5个原则彻底改变了面向对象编程的世界,改变了我们编写软件的方式。
那么,什么是SOLID以及它如何帮助我们编写更好的代码?简而言之,Martin和Feathers的设计原则鼓励我们创建更易于维护,易懂且灵活的软件。因此,随着我们的应用程序规模不断扩大,我们可以降低其复杂性,并为您节省更多的麻烦!

以下5个概念构成了我们的SOLID原则:

  1. Single Responsibility单一职责
  2. Open/Closed开闭原则
  3. Liskov Substitution
  4. Interface Segregation接口分离
  5. Dependency Injection依赖注射

虽然其中一些词听起来令人生畏,但可以通过一些简单的代码示例轻松理解它们。在接下来的部分中,我们将深入探讨每个原则的含义,以及一个快速的Java示例来说明每个原则。

1. 单一责任
让我们用单一的责任原则来解决问题。正如我们所预料的那样,这个原则规定一个类应该只有一个责任。此外,它只应该有一个改变的理由。
这个原则如何帮助我们构建更好的软件?让我们看看它的一些好处:

  1. 测试  - 具有一个职责的类将具有少得多的测试用例
  2. 较低的耦合  - 单个类中较少的功能将具有较少的依赖性
  3. 组织  - 较小,组织良好的类比单片类更容易搜索

举一个例如一个类来代表一本简单的书:

public class Book {
 
    private String name;
    private String author;
    private String text;
 
    //constructor, getters and setters
}

在此代码中,我们存储与Book实例关联的名称,作者和文本。
现在让我们添加几种查询文本的方法:

public class Book {
 
    private String name;
    private String author;
    private String text;
 
    //constructor, getters and setters
 
   
// methods that directly relate to the book properties
    public String replaceWordInText(String word){
        return text.replaceAll(word, text);
    }
 
    public boolean isWordInText(String word){
        return text.contains(word);
    }
}

现在,我们的Book  类运作良好,我们可以在应用程序中存储尽可能多的书籍。但是,如果我们无法将文本输出到控制台并阅读它,那么存储信息有什么用?
添加打印方法:

public class Book {
    //...
 
    void printTextToConsole(){
       
// our code for formatting and printing the text
    }
}

但是,此代码违反了我们之前概述的单一责任原则。为了修复我们的混乱,我们应该实现一个单独的类,只关注打印我们的文本:

public class BookPrinter {
 
    // methods for outputting text
    void printTextToConsole(String text){
       
//our code for formatting and printing the text
    }
 
    void printTextToAnotherMedium(String text){
       
// code for writing to any other location..
    }
}

真棒。我们不仅开发了一个新类来减轻  Book 的打印职责,而且还可以利用我们的  BookPrinter 类将文本发送到其他媒体。
无论是电子邮件,日志记录还是其他任何内容,我们都有一个专门针对这一问题的类。

2. 开闭原则
类应该是可以扩展的,但是对修改是关闭, 我们应该停止修改现有代码,否则会在经过测试的应用程序中引发潜在的新错误。当然,规则的一个例外是:修复现有代码中的错误。
让我们通过快速代码示例进一步探索这个概念,作为新项目的一部分,想象一下我们已经实现了一个  吉他 类。

public class Guitar {
 
    private String make;
    private String model;
    private int volume;
 
    //Constructors, getters & setters
}

我们推出这个应用程序,每个人都喜欢它。然而,几个月后,我们认为  吉他 有点无聊,可以用一个很棒的火焰模式让它看起来更“摇滚”。
在这一点上,可能很容易打开  Guitar 类并添加一个火焰模式 - 但是谁知道在我们的应用程序中可能会出现什么错误。
相反,让我们坚持开放式原则并简单地扩展我们的  吉他 类(banq注:扩展类不代表一定要继承,可以使用组合方式):

public class SuperCoolGuitarWithFlames extends Guitar {
 
    private String flameColor;
 
    //constructor, getters + setters
}

通过扩展  Guitar 类,我们可以确保我们现有的应用程序不会受到影响。


3. Liskov替代
这可能是5个原则中最复杂的。简单地说,如果A类是B类的子类型,那么我们应该能够用  A 替换  B 而不破坏我们程序的行为。让我们直接跳转到代码,以帮助我们围绕这个概念:

public interface Car {
 
    void turnOnEngine();
    void accelerate();
}

在上面,我们定义了一个简单的  Car 界面,其中包含了所有汽车应该能够实现的几种方法 - 打开引擎并加速前进。
让我们实现我们的接口并为方法提供一些代码:

public class MotorCar implements Car {
 
    private Engine engine;
 
    //Constructors, getters + setters
 
    public void turnOnEngine() {
       
//turn on the engine!
        engine.on();
    }
 
    public void accelerate() {
       
//move forward!
        engine.powerOn(1000);
    }
}

正如我们的代码描述的那样,我们有一个可以打开的引擎,我们可以增加功率。但等一下,2019年,埃隆马斯克一直是一个忙碌的人。
我们现在生活在电动汽车时代:

public class ElectricCar implements Car {
 
    public void turnOnEngine() {
        throw new AssertionError("I don't have an engine!");
    }
 
    public void accelerate() {
       
//this acceleration is crazy!
    }
}

将没有引擎的电车放进一个混合物对象中,就改变了我们程序的行为。这是对Liskov替换的公然违反,并且比我们之前的2条原则更难修复。
一种可能的解决方案是将我们的模型重新设计为考虑到无引擎状态的  汽车的接口。

4.接口隔离
SOLID中的“I”代表接口隔离,它只是意味着更大的接口应该分成更小的接口。通过这样做,我们可以确保实现类只需要关注它们感兴趣的方法。
对于这个例子,我们将尝试作为动物园管理员。更具体地说,我们将在熊圈中工作。
让我们从一个界面开始,概述我们作为熊守护者的角色:

public interface BearKeeper {
    void washTheBear();
    void feedTheBear();
    void petTheBear();
}

作为狂热的动物园管理员,我们非常乐意为我们心爱的熊洗净和喂食。然而,我们都非常清楚抚摸它们的危险。不幸的是,我们的接口相当大,我们别无选择,只能实施代码来承担责任。
让我们通过将我们的大型接口分成3个独立的接口来解决这个问题:

public interface BearCleaner {
    void washTheBear();
}
 
public interface BearFeeder {
    void feedTheBear();
}
 
public interface BearPetter {
    void petTheBear();
}

现在,由于接口隔离,我们可以自由地只实现对我们重要的方法:

public class BearCarer implements BearCleaner, BearFeeder {
 
    public void washTheBear() {
        //I think we missed a spot...
    }
 
    public void feedTheBear() {
       
//Tuna Tuesdays...
    }
}

最后,我们可以把危险的东西留给疯狂的人:

public class CrazyPerson implements BearPetter {
 
    public void petTheBear() {
        //Good luck with that!
    }
}

更进一步,我们甚至可以将我们的BookPrinter 类从之前的示例拆分  为以相同的方式使用接口隔离。通过使用单个打印 方法实现  Printer接口  ,我们可以实例化单独的  ConsoleBookPrinter 和  OtherMediaBookPrinter 类。

5.依赖注入
当有人提到“依赖注入”这个词时,可能会想到一些框架 - 谷歌的Guice,或者也许是Spring。但事实是,我们不需要复杂的框架来理解这个原则。
依赖注入只是在创建时注入类的依赖关系的技术,避免了危险的  新 关键字。
为了证明这一点,让我们使用代码实现Windows 98计算机:

public class Windows98Machine {}

但没有显示器和键盘的电脑有什么用?让我们在构造函数中添加其中一个,以便我们实例化的每个  Windows98Computer 都预装了一个Monitor 和一个  键盘:

public class Windows98Machine {
 
    private final Keyboard keyboard;
    private final Monitor monitor;
 
    public Windows98Machine() {
        monitor = new Monitor();
        keyboard = new Keyboard();
    }
 
}

这段代码可以使用,我们将能够在Windows98Computer 类中自由  使用键盘和监视器。问题解决了?不完全的。通过使用  new 关键字声明  Keyboard 和  Monitor ,我们将这3个类紧密耦合在一起。
这不仅使我们的  Windows98Computer 难以测试,而且我们也失去了在需要时用子类切换我们的Keyboard 类的能力  。我们也耦合使用了我们的Monitor类。
让我们看看当我们应用一些简单的依赖注入时,同一个例子是如何看的:

public class Windows98Machine{
 
    private final Keyboard keyboard;
    private final Monitor monitor;
 
    public Windows98Machine(Keyboard keyboard, Monitor monitor) {
        this.keyboard = keyboard;
        this.monitor = monitor;
    }
}

优秀!我们已经解耦了依赖关系,并可以使用我们选择的任何测试框架自由测试我们的  Windows98Machine 。(banq注:可以使用builder模式实现两个输入参数的注入)

结论
在本教程中,我们深入探讨了面向对象设计的SOLID原则。
我们从一小段SOLID历史开始,以及这些原则存在的原因。
我们逐字逐字地用一个违反它的快速代码示例来分解每个原则的含义。然后,我们了解了如何修复代码 并使其符合SOLID原则。

代码在GitHub可用