面向对象设计的 SOLID 原则

1. 概述

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

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

2. SOLID 原则的原因

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

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

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

  1. 单一责任
  2. 开放/闭合
  3. Liskov替代
  4. 接口隔离
  5. 依赖反转

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

3. 单一责任

让我们从单一责任原则开始。正如我们所料,这个原则规定一个类应该只有一个责任。此外,它应该只有一个改变的理由。

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

  1. 测试——一个负责一个的类的测试用例要少得多。
  2. 较低的耦合 – 单个类中的功能越少,依赖项就越少。
  3. 组织 – 较小、组织良好的类比整体类更容易搜索。

例如,让我们看一个类来表示一本简单的书:

代码语言:javascript代码运行次数:0运行复制
public class Book {

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

    //constructor, getters and setters
}Copy

在此代码中,我们存储与Book 实例关联的名称、作者和文本。

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

代码语言:javascript代码运行次数:0运行复制
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);
    }
}Copy

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

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

让我们谨慎行事,并添加一个打印方法:

代码语言:javascript代码运行次数:0运行复制
public class BadBook {
    //...

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

但是,此代码违反了我们前面概述的单一责任原则。

为了解决我们的混乱,我们应该实现一个单独的类,它只处理打印我们的文本:

代码语言:javascript代码运行次数:0运行复制
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..
    }
}Copy

棒。我们不仅开发了一个类来减轻 Book 的打印职责,而且还可以利用我们的BookPrinter类将我们的文本发送到其他媒体。

无论是电子邮件、日志记录还是其他任何内容,我们都有一个单独的类专门针对这个问题。

4. 开放扩展,关闭修改

现在是固体中的O的时候了,称为开闭原理。简单地说,类应该开放扩展,但关闭修改。通过这样做,我们阻止自己修改现有代码并在原本满意的应用程序中导致潜在的新错误。

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

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

它功能齐全,甚至还有一个音量旋钮:

代码语言:javascript代码运行次数:0运行复制
public class Guitar {

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

    //Constructors, getters & setters
}Copy

我们启动了该应用程序,每个人都喜欢它。但几个月后,我们认为吉他有点无聊,可以使用很酷的火焰图案来让它看起来更摇滚。

在这一点上,打开Guitar类并添加火焰模式可能很诱人 - 但谁知道我们的应用程序中可能会抛出什么错误。

相反,让我们坚持开闭原则,简单地扩展我们的吉类:

代码语言:javascript代码运行次数:0运行复制
public class SuperCoolGuitarWithFlames extends Guitar {

    private String flameColor;

    //constructor, getters + setters
}Copy

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

5.Liskov

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

让我们直接跳到代码来帮助我们理解这个概念:

代码语言:javascript代码运行次数:0运行复制
public interface Car {

    void turnOnEngine();
    void accelerate();
}Copy

上面,我们定义了一个简单的Car接口,其中包含所有汽车都应该能够实现的几种方法:打开发动机并加速前进。

让我们实现我们的接口并为这些方法提供一些代码:

代码语言:javascript代码运行次数:0运行复制
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);
    }
}Copy

正如我们的代码所描述的,我们有一个可以打开的引擎,我们可以增加功率。

但是等等——我们现在生活在电动汽车时代:

代码语言:javascript代码运行次数:0运行复制
public class ElectricCar implements Car {

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

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

通过将没有引擎的汽车投入其中,我们本质上改变了程序的行为。这是对Liskov替代的公然违反,比我们之前的两个原则更难修复。

一种可能的解决方案是将我们的模型重新设计为考虑到我们汽车无引擎状态的界面。

6. 接口隔离

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

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

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

代码语言:javascript代码运行次数:0运行复制
public interface BearKeeper {
    void washTheBear();
    void feedTheBear();
    void petTheBear();
}Copy

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

让我们通过将大型接口拆分为三个单独的接口来解决此问题:

代码语言:javascript代码运行次数:0运行复制
public interface BearCleaner {
    void washTheBear();
}

public interface BearFeeder {
    void feedTheBear();
}

public interface BearPetter {
    void petTheBear();
}Copy

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

代码语言:javascript代码运行次数:0运行复制
public class BearCarer implements BearCleaner, BearFeeder {

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

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

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

代码语言:javascript代码运行次数:0运行复制
public class CrazyPerson implements BearPetter {

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

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

7. 依赖反转

依赖反转原理是指软件模块的解耦。这样,高级模块不再依赖于低级模块,而是两者都依赖于抽象。

为了演示这一点,让我们走老派,用代码使Windows 98计算机栩栩如生:

代码语言:javascript代码运行次数:0运行复制
public class Windows98Machine {}Copy

但是没有显示器和键盘的计算机有什么用呢?让我们将其中一个添加到我们的构造函数中,以便我们实例化的每台Windows98 计算机都预打包了一个监视器和一个标准键盘

代码语言:javascript代码运行次数:0运行复制
public class Windows98Machine {

    private final StandardKeyboard keyboard;
    private final Monitor monitor;

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

}Copy

这段代码将起作用,我们将能够在Windows98Computer类中自由使用标准键盘显示器

问题解决了?差一点。通过使用new关键字声明标准键盘监视器,我们将这三个类紧密耦合在一起。

这不仅使我们的Windows98Computer难以测试,而且我们还失去了在需要时将StandardKeyboard类切换为其他类的能力。我们也坚持使用监视器类。

让我们通过添加一个更通用的键盘接口并在我们的类中使用它来将我们的机器与标准键盘分离:

代码语言:javascript代码运行次数:0运行复制
public interface Keyboard { }Copy
代码语言:javascript代码运行次数:0运行复制
public class Windows98Machine{

    private final Keyboard keyboard;
    private final Monitor monitor;

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

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

让我们也修改我们的StandardKeyboard类来实现键盘接口,使其适合注入到Windows98Machine类中:

代码语言:javascript代码运行次数:0运行复制
public class StandardKeyboard implements Keyboard { }Copy

现在,我们的类已解耦,并通过键盘抽象进行通信。如果需要,我们可以通过不同的接口实现轻松切换机器中的键盘类型。我们可以对监视器类遵循相同的原则。

非常好!我们已经解耦了依赖关系,并且可以自由地使用我们选择的任何测试框架来测试我们的Windows98Machine

8. 结论

在本文中,我们深入探讨了面向对象设计的 SOLID 原则。

我们从快速了解一下 SOLID 历史以及这些原则存在的原因开始。

 我们逐个字母地分解了每个原则的含义,并提供了一个违反它的快速代码示例。然后,我们看到了如何修复我们的代码并使其符合 SOLID 原则。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2012-03-20,如有侵权请联系 cloudcommunity@tencent 删除接口软件设计架构教程