A Solid Guide to SOLID Principles – SOLID原则的坚实指南

最后修改: 2019年 2月 5日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll be discussing the SOLID principles of object-oriented design.

在本教程中,我们将讨论面向对象设计的SOLID原则

First, we’ll start by exploring the reasons they came about and why we should consider them when designing software. Then we’ll outline each principle alongside some example code.

首先,我们将从探索它们产生的原因以及为什么我们在设计软件时应该考虑它们开始。然后,我们将在一些示例代码中概述每条原则。

2. The Reason for SOLID Principles

2.SOLID原则的原因

The SOLID principles were introduced by Robert C. Martin in his 2000 paper “Design Principles and Design Patterns.” These concepts were later built upon by Michael Feathers, who introduced us to the SOLID acronym. And in the last 20 years, these five principles have revolutionized the world of object-oriented programming, changing the way that we write software.

SOLID原则是由Robert C. Martin在其2000年的论文“设计原则和设计模式”中提出的。这些概念后来由Michael Feathers建立,他向我们介绍了SOLID的缩写。而在过去的20年里,这五条原则彻底改变了面向对象的编程世界,改变了我们编写软件的方式。

So, what is SOLID and how does it help us write better code? Simply put, Martin and Feathers’ design principles encourage us to create more maintainable, understandable, and flexible software. Consequently, as our applications grow in size, we can reduce their complexity and save ourselves a lot of headaches further down the road!

那么,什么是SOLID,它是如何帮助我们编写更好的代码的?简单地说,Martin和Feathers的设计原则鼓励我们创建更多的可维护、可理解和灵活的软件。因此,随着我们的应用程序规模的扩大,我们可以降低其复杂性,并在以后的道路上为自己节省很多麻烦

The following five concepts make up our SOLID principles:

以下五个概念构成了我们的SOLID原则。

  1. Single Responsibility
  2. Open/Closed
  3. Liskov Substitution
  4. Interface Segregation
  5. Dependency Inversion

While these concepts may seem daunting, they can be easily understood with some simple code examples. In the following sections, we’ll take a deep dive into these principles, with a quick Java example to illustrate each one.

虽然这些概念看起来令人生畏,但通过一些简单的代码例子就可以轻松理解。在下面的章节中,我们将深入探讨这些原则,用一个快速的Java例子来说明每个原则。

3. Single Responsibility

3.单一责任

Let’s begin with the single responsibility principle. As we might expect, this principle states that a class should only have one responsibility. Furthermore, it should only have one reason to change.

让我们从单一责任原则开始。正如我们所期望的那样,该原则指出一个类只应该有一个责任。此外,它应该只有一个改变的理由。

How does this principle help us to build better software? Let’s see a few of its benefits:

这一原则如何帮助我们构建更好的软件?让我们看看它的几个好处。

  1. Testing – A class with one responsibility will have far fewer test cases.
  2. Lower coupling – Less functionality in a single class will have fewer dependencies.
  3. Organization – Smaller, well-organized classes are easier to search than monolithic ones.

For example, let’s look at a class to represent a simple book:

例如,让我们看看一个代表简单书籍的类。

public class Book {

    private String name;
    private String author;
    private String text;

    //constructor, getters and setters
}

In this code, we store the name, author and text associated with an instance of a Book.

在这段代码中,我们存储了与实例相关的名称、作者和文本。

Let’s now add a couple of methods to query the text:

现在让我们添加几个方法来查询文本。

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, String replacementWord){
        return text.replaceAll(word, replacementWord);
    }

    public boolean isWordInText(String word){
        return text.contains(word);
    }
}

Now our Book class works well, and we can store as many books as we like in our application.

现在我们的Book类运行良好,我们可以在我们的应用程序中存储任意多的书。

But what good is storing the information if we can’t output the text to our console and read it?

但是,如果我们不能将文本输出到我们的控制台并阅读它,那么存储这些信息有什么用呢?

Let’s throw caution to the wind and add a print method:

让我们谨慎行事,增加一个打印方法。

public class Book {
    //...

    void printTextToConsole(){
        // our code for formatting and printing the text
    }
}

However, this code violates the single responsibility principle we outlined earlier.

然而,这段代码违反了我们前面概述的单一责任原则。

To fix our mess, we should implement a separate class that deals only with printing our texts:

为了解决我们的麻烦,我们应该实现一个单独的类,只处理打印我们的文本。

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..
    }
}

Awesome. Not only have we developed a class that relieves the Book of its printing duties, but we can also leverage our BookPrinter class to send our text to other media.

真棒。我们不仅开发了一个类,解除了Book的打印任务,而且我们还可以利用我们的BookPrinter类将我们的文本发送到其他媒体。

Whether it’s email, logging, or anything else, we have a separate class dedicated to this one concern.

无论是电子邮件、记录,还是其他任何事情,我们都有一个单独的班级专门负责这一个关注点。

4. Open for Extension, Closed for Modification

4.可以延期,不可以修改

It’s now time for the O in SOLID, known as the open-closed principle. Simply put, classes should be open for extension but closed for modification. In doing so, we stop ourselves from modifying existing code and causing potential new bugs in an otherwise happy application.

现在是SOLID中的O,也就是所谓的开放-封闭原则。简单地说,类应该对扩展开放,但对修改关闭。这样做,我们阻止自己修改现有的代码,在一个原本快乐的应用程序中造成潜在的新错误

Of course, the one exception to the rule is when fixing bugs in existing code.

当然,规则的一个例外是在修复现有代码的错误时。

Let’s explore the concept with a quick code example. As part of a new project, imagine we’ve implemented a Guitar class.

让我们通过一个快速的代码例子来探索这个概念。作为一个新项目的一部分,设想我们已经实现了一个吉他类。

It’s fully fledged and even has a volume knob:

它是完全成熟的,甚至有一个音量旋钮。

public class Guitar {

    private String make;
    private String model;
    private int volume;

    //Constructors, getters & setters
}

We launch the application, and everyone loves it. But after a few months, we decide the Guitar is a little boring and could use a cool flame pattern to make it look more rock and roll.

我们推出了这个应用程序,每个人都喜欢它。但几个月后,我们决定吉他有点无聊,可以使用一个很酷的火焰图案,使它看起来更摇滚。

At this point, it might be tempting to just open up the Guitar class and add a flame pattern — but who knows what errors that might throw up in our application.

在这一点上,我们可能很想直接打开吉他类并添加一个火焰模式–但谁知道这在我们的应用程序中会出现什么错误。

Instead, let’s stick to the open-closed principle and simply extend our Guitar class:

相反,让我们坚持开放-封闭原则,简单地扩展我们的Guitar class

public class SuperCoolGuitarWithFlames extends Guitar {

    private String flameColor;

    //constructor, getters + setters
}

By extending the Guitar class, we can be sure that our existing application won’t be affected.

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

5. Liskov Substitution

5.利斯科夫替代物

Next on our list is Liskov substitution, which is arguably the most complex of the five principles. Simply put, if class A is a subtype of class B, we should be able to replace with without disrupting the behavior of our program.

在我们的名单上,接下来是Liskov替换,这可以说是五个原则中最复杂的一个。简单地说,如果类A是类B的一个子类型,我们应该能够用A替换B,而不会破坏我们程序的行为。

Let’s jump straight to the code to help us understand this concept:

让我们直接跳到代码中,帮助我们理解这个概念。

public interface Car {

    void turnOnEngine();
    void accelerate();
}

Above, we define a simple Car interface with a couple of methods that all cars should be able to fulfill: turning on the engine and accelerating forward.

上面,我们定义了一个简单的汽车接口,其中有几个所有汽车都应该能够实现的方法:开启引擎和加速前进。

Let’s implement our interface and provide some code for the methods:

让我们来实现我们的接口,并提供一些方法的代码。

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);
    }
}

As our code describes, we have an engine that we can turn on, and we can increase the power.

正如我们的代码所描述的那样,我们有一个可以打开的引擎,我们可以提高功率。

But wait — we are now living in the era of electric cars:

但是等等–我们现在生活在电动汽车的时代。

public class ElectricCar implements Car {

    public void turnOnEngine() {
        throw new AssertionError("I don't have an engine!");
    }

    public void accelerate() {
        //this acceleration is crazy!
    }
}

By throwing a car without an engine into the mix, we are inherently changing the behavior of our program. This is a blatant violation of Liskov substitution and is a bit harder to fix than our previous two principles.

通过把一辆没有发动机的汽车扔进组合中,我们在本质上改变了我们程序的行为。这是对Liskov替代法的公然违反,比我们之前的两个原则更难解决。

One possible solution would be to rework our model into interfaces that take into account the engine-less state of our Car.

一个可能的解决方案是,将我们的模型重新加工成考虑到我们的汽车无引擎状态的接口。

6. Interface Segregation

6.界面隔离

The I  in SOLID stands for interface segregation, and it simply means that larger interfaces should be split into smaller ones. By doing so, we can ensure that implementing classes only need to be concerned about the methods that are of interest to them.

SOLID中的I代表接口隔离,它只是意味着较大的接口应该被分割成较小的接口。通过这样做,我们可以确保实现类只需要关注它们感兴趣的方法。

For this example, we’re going to try our hands as zookeepers. And more specifically, we’ll be working in the bear enclosure.

在这个例子中,我们将尝试作为动物园管理员的工作。更具体地说,我们将在熊的围栏里工作。

Let’s start with an interface that outlines our roles as a bear keeper:

让我们从一个界面开始,概述我们作为养熊人的角色。

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

As avid zookeepers, we’re more than happy to wash and feed our beloved bears. But we’re all too aware of the dangers of petting them. Unfortunately, our interface is rather large, and we have no choice but to implement the code to pet the bear.

作为狂热的动物园管理员,我们非常乐意清洗和喂养我们心爱的黑熊。但是我们都很清楚抚摸它们的危险性。不幸的是,我们的界面相当大,我们别无选择,只能实施抚摸熊的代码。

Let’s fix this by splitting our large interface into three separate ones:

让我们通过将我们的大界面分成三个独立的界面来解决这个问题

public interface BearCleaner {
    void washTheBear();
}

public interface BearFeeder {
    void feedTheBear();
}

public interface BearPetter {
    void petTheBear();
}

Now, thanks to interface segregation, we’re free to implement only the methods that matter to us:

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

public class BearCarer implements BearCleaner, BearFeeder {

    public void washTheBear() {
        //I think we missed a spot...
    }

    public void feedTheBear() {
        //Tuna Tuesdays...
    }
}

And finally, we can leave the dangerous stuff to the reckless people:

最后,我们可以把危险的东西留给那些鲁莽的人。

public class CrazyPerson implements BearPetter {

    public void petTheBear() {
        //Good luck with that!
    }
}

Going further, we could even split our BookPrinter class from our example earlier to use interface segregation in the same way. By implementing a Printer interface with a single print method, we could instantiate separate ConsoleBookPrinter and OtherMediaBookPrinter classes.

再进一步,我们甚至可以将我们的BookPrinter类从前面的例子中拆分出来,以同样的方式使用接口隔离。通过实现一个具有单一print 方法的Printer接口,我们可以实例化独立的ConsoleBookPrinterOtherMediaBookPrinter类。

7. Dependency Inversion

7.依赖性反转

The principle of dependency inversion refers to the decoupling of software modules. This way, instead of high-level modules depending on low-level modules, both will depend on abstractions.

依赖性反转原则是指软件模块的解耦。这样一来,高级模块不再依赖于低级模块,而是两者都将依赖于抽象概念。

To demonstrate this, let’s go old-school and bring to life a Windows 98 computer with code:

为了证明这一点,让我们走老路,用代码将一台Windows 98电脑变成现实。

public class Windows98Machine {}

But what good is a computer without a monitor and keyboard? Let’s add one of each to our constructor so that every Windows98Computer we instantiate comes prepacked with a Monitor and a StandardKeyboard:

但没有显示器和键盘的计算机有什么用呢?让我们在构造函数中各添加一个,这样我们实例化的每台Windows98Computer都预装了MonitorStandardKeyboard

public class Windows98Machine {

    private final StandardKeyboard keyboard;
    private final Monitor monitor;

    public Windows98Machine() {
        monitor = new Monitor();
        keyboard = new StandardKeyboard();
    }

}

This code will work, and we’ll be able to use the StandardKeyboard and Monitor freely within our Windows98Computer class.

这段代码将发挥作用,我们将能够在我们的Windows98Computer类中自由使用StandardKeyboardMonitor

Problem solved? Not quite. By declaring the StandardKeyboard and Monitor with the new keyword, we’ve tightly coupled these three classes together.

问题解决了吗?并非如此。通过用new关键字声明StandardKeyboardMonitor,我们已经将这三个类紧密地耦合在一起。

Not only does this make our Windows98Computer hard to test, but we’ve also lost the ability to switch out our StandardKeyboard class with a different one should the need arise. And we’re stuck with our Monitor class too.

这不仅使我们的Windows98Computer难以测试,而且我们也失去了在需要时用不同的StandardKeyboard类切换的能力。而且我们也被困于我们的Monitor类。

Let’s decouple our machine from the StandardKeyboard by adding a more general Keyboard interface and using this in our class:

让我们通过添加一个更通用的键盘接口,将我们的机器与StandardKeyboard解耦,并在我们的类中使用它。

public interface Keyboard { }
public class Windows98Machine{

    private final Keyboard keyboard;
    private final Monitor monitor;

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

Here, we’re using the dependency injection pattern to facilitate adding the Keyboard dependency into the Windows98Machine class.

在这里,我们使用依赖注入模式来促进将键盘依赖性添加到Windows98Machine类。

Let’s also modify our StandardKeyboard class to implement the Keyboard interface so that it’s suitable for injecting into the Windows98Machine class:

我们也来修改我们的StandardKeyboard类,以实现Keyboard接口,这样它就适合注入到Windows98Machine类。

public class StandardKeyboard implements Keyboard { }

Now our classes are decoupled and communicate through the Keyboard abstraction. If we want, we can easily switch out the type of keyboard in our machine with a different implementation of the interface. We can follow the same principle for the Monitor class.

现在我们的类已经解耦,并通过键盘抽象进行通信。如果我们愿意,我们可以用不同的接口实现轻松地切换我们机器中的键盘类型。我们可以对Monitor类遵循同样的原则。

Excellent! We’ve decoupled the dependencies and are free to test our Windows98Machine with whichever testing framework we choose.

很好!我们已经解除了依赖关系,可以自由地用我们选择的任何测试框架来测试我们的Windows98Machine。我们已经解除了依赖关系,可以自由地用我们选择的任何测试框架来测试我们的Windows98Machine

8. Conclusion

8.结论

In this article, we’ve taken a deep dive into the SOLID principles of object-oriented design.

在这篇文章中,我们对面向对象设计的SOLID原则进行了深入的研究。

We started with a quick bit of SOLID history and the reasons these principles exist.

我们从快速的SOLID历史和这些原则存在的原因开始。

Letter by letter, we’ve broken down the meaning of each principle with a quick code example that violates it. We then saw how to fix our code and make it adhere to the SOLID principles.

一个字母一个字母地,我们用一个违反原则的快速代码例子分解了每个原则的含义。然后我们看到了如何修复我们的代码并使其遵守SOLID原则。

As always, the code is available over on GitHub.

像往常一样,代码可在GitHub上获得