<返回更多

深入设计原则-SOLID

2021-03-08  今日头条  架构即人生
加入收藏

介绍

SOLID是什么,它是如何帮助我们写更好的代码的?

SOLID原则由以下5个概念组成:

  1. Single Responsibility(单一职责)
  2. Open/Closed(开闭)
  3. Liskov Substitution(里氏替换)
  4. Interface Segregation(接口隔离)
  5. Dependency Inversion(依赖反转)

 

简单来说,这些原则帮助我们创造更易维护,易于理解和灵活的软件。因此,随着我们应用的扩大,我们可以降低其复杂性,并在以后为我们省去很多麻烦。

 

深入说明

单一职责原则

像我们从字面意思看到的一样,该原则指出,一个类仅应承担一种责任。此外,它应该只有一个改变的理由(不能同时有很多理由来修改它)。

那么,这个原则是怎么帮助我们写更好的软件的?让我们先看一下它的优点:

  1. 易测试 - 因为职责单一,所以拥有更少的测试用例
  2. 低耦合 - 单个类中较少的功能将具有较少的依赖关系
  3. 易于组织 - 更容易编写代码逻辑

 

下面我们以代码示例说明:

我们用Book类代表一本书,其属性包括:书名(name),作者(author)和内容(text)

/**
 *  实体:书
 */
public class Book {

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

    /**
     *  文本替换
     * @param word
     * @return
     */
    public String replaceWordInText(String word){
        return text.replaceAll(word, text);
    }

    /**
     *  是否包含指定的文本
     * @param word
     * @return
     */
    public boolean isWordInText(String word){
        return text.contains(word);
    }
}

现在我们的程序运行的很好,我们能存储任意的书的内容。但是我们无法将书的内容打印出来,无法阅读该怎么办?那么,我们就在新增一个打印内容的方法,如:

void printTextToConsole(){
    // 输出文本
}

好的,现在已经可以实现打印内容了。但是,我们这样就违背了“单一职责”(书本身不与打印有什么关系)。为了解决我们的问题,需要实现一个单独的类,该类仅与打印书籍有关:

/**
 * 书籍打印
 */
public class BookPrinter {

    /**
     *  打印到控制台
     * @param text
     */
    void printTextToConsole(String text){
        // printing the text
    }

    /**
     *  打印到其他媒介
     * @param text
     */
    void printTextToAnotherMedium(String text){
        // do something
    }
}

上面的类不仅与书籍本身解耦,而且能够实现通过各种媒介进行打印内容,本身又是一个职责单一的案例(打印)。

 

开闭原则

简单来说,一个类应对扩展是打开的,对修改是关闭的(open for extension, closed for modification)。这样一来,我们就可以避免修改现有代码,从而引入新的潜在的问题。当然有个特例,就是如果现有代码中存在已有bug,我们还是应该去解决它的。

 

比如现在有一个吉他,可以实现基本功能的弹奏:

/**
 *  吉他
 */
public class Guitar {
    private String make;
    private String model;
    private int volume;
}

但是用了一段时间后,觉得有点无聊想增加一些音节,让它用起来更加的摇滚。

我们如果直接在原有的Guitar类上修改,可能会把原有的功能破坏掉从而引入新的问题,所以根据“开闭原则”,我们应该在原有基础上进行扩展而不是修改:

/**
 *  更酷炫的音节
 */
public class SuperCoolGuitarWithFlames extends Guitar {
    private String flameColor;
}

通过扩展实现,我们可以保证现有的功能不会受到破坏。

里氏替换原则

这个原则字面意思较难理解,简单来说就是,如果一个类A是类B的子类,那么我们在不中断程序行为的情况下可以把B替换成A,而不影响程序原有的功能。

我们用代码示例说明:

/**
 *  车
 */
public interface Car {
    /**
     *  打开引擎
     */
    void turnOnEngine();

    /**
     *  加速
     */
    void accelerate();
}

我们定义了一个接口,里面有两个方法,可以实现引擎打开和车加速功能。

下面来看下具体的实现类:

/**
 *  摩托车
 */
public class MotorCar {
    private Engine engine;
    
    public void turnOnEngine() {
        //turn on the engine!
        engine.on();
    }
    public void accelerate() {
        //move forward!
        engine.powerOn(1000);
    }
}

可以看出摩托车属于车的一种,实现了打开引擎和加速能力,我们继续看其他实现:

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

可以看出,上面的电车虽然实现了Car,但是它没有引擎,所以不具有打开引擎的功能,那么这个就改变了程序的行为,违背了我们说的“里氏替换原则”。

接口隔离原则

简单来说,就是将大的接口切分为更小的接口,这样我们就可以确保实现类只需要关心它们感兴趣的方法。

举个例子,假如我们在动物园工作,具体是熊的“看护人”,那么可以这样定义:

public interface BearKeeper {
    /**
     *  给熊洗澡
     */
    void washTheBear();

    /**
     *  给熊喂食
     */
    void feedTheBear();

    /**
     *  抚摸熊
     */
    void petTheBear();
}

我们可以开心的喂养熊,但是抚摸熊可能有危险,但是我们的接口定义的相当大,我们别无选择。那么,现在我们将接口拆分一下:

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

通过拆分,我们就可以自由实现我们感兴趣的方法,

public class BearCarer implements BearCleaner, BearFeeder {
    public void washTheBear() {
        
    }
    public void feedTheBear() {
     
    }
}

我们可以将抚摸熊的“福利”给那么胆大或疯狂的人:

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

依赖反转

这个原则主要是为了解耦。代替低级模块依赖高级模块,二者都应该依赖抽象。

举个例子,假如我们现有一台windows98电脑:

public class Windows98machine {}

但是没有显示器和键盘对我们来说有什么用呢?于是我们给这台电脑新增显示器和键盘:

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

上面代码运行的很好,我们的电脑已经实现了显示器和键盘,那么问题解决了吗?没有,我们又把这三个类紧密结合在一起了。这不仅使Windows98Machine难以测试,而且还失去了将StandardKeyboard替换为其他类的能力。

让我们把Keyboard抽象出来:

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;
    }
}
public class StandardKeyboard implements Keyboard { }

现在Windows98Machine就与StandardKeyboard解耦了,通过依赖反转(依赖抽象而不是具体类)完成了解耦。

 

总结

在本文中我们深入研究了面向对象的SOLID原则,并通过代码示例说明其原理和实现。深入设计原则-SOLID

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>