设计模式之基础-6大基本原则

3/5/2021 架构设计模式

# 单一职责原则

单一职责原则SRP(Single Responsibility Principle): 一个类只干一件事。

有人不爽了,我就是要一个类干多件事怎么着,当然可以啊,如果是你自己一个人写代码,随便写,只要你能保证后面修改的时候不出bug,并且改的快,就行。比方说,一个类A,我既做了条件判断,又弹了提示,比如下面代码:

public static boolean checkAge(int age){
    if(age < 18) {
        ToastTool.showShort("未成年人不得观看");
        return false;
    }
    return true;
}
1
2
3
4
5
6
7

简直美滋滋,任何时候只要直接调checkAge(age)就行,判断了还自带提示,突然有一天,产品说有些地方不要提示了,如果不满18岁,直接跳转到另一个页面,怎么办,这个函数调了就提示,如果去掉提示,其他需要提示的地方也没有了,说白了就是: 有的地方需要提示,有的地方不需要提示,现在要把不需要提示的地方的提示去掉,只能重写一个不带提示的方法,最简单的就是: 把原来里面的逻辑判断剥离出来,抽成一个方法,专门用来判断,原来带提示的方法调用新方法去判断,然后自己内部弹出提示。

public static boolean checkAge(int age) {
    if(!checkAgeInner(age)) {
        ToastTool.showShort("未成年人不得观看");
        return false;
    }
    return true;
}

//新方法,只做逻辑判断
public static boolean checkAgeInner(int age) {
    if(age < 18) return false;
    return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这虽然只是一个方法的剥离,但是体现了单一职责的好处,在需求细化的时候,单一职责的作用体现的更明显,简言之就是需求的粒度与单一职责的优势成正比,我们要尽量避免大类,避免"多功能类",因为一个类的功能越多,那么使用到它的地方也越多,就会导致任何一个部分都离不开它,都要依赖它,A模块需要,B模块也需要,在将A模块拆出来的时候,不得不把这个多功能类也拷贝过来,而这个多功能类里面又有B模块的逻辑,导致A模块不干净,被污染,所以我们要避免多功能类,或者说要尽量去中心化,多功能类是耦合的罪魁祸首,一定要尽量避免。

单一职责不仅限于类,还限于方法、模块等,适用于天地万物,宇宙洪荒,这是一种思想,一定要掌握。

# 里氏置换原则

里氏置换原则LSP(Liskov Substitution Principle): 所有使用基类的地方都必须能透明的使用子类。

什么意思呢,就是说凡是用到父类的地方,替换成子类不会改变原有逻辑,我们知道面向对象编程的三个基本原则: 封装,继承和多态。里氏置换原则就是继承的体现,有的人说这不是废话吗,子类继承了父类,用到父类的地方替换成子类肯定没问题啊,不一定!我们来看demo:

public abstract class Car {
    abstract void run();
}

public class ToyCar extends Car {
    @Override
    void run() {
        System.out.println("玩具车不会跑");
    }
}

public class Bike extends Car {
    @Override
    void run() {
        System.out.println("自行车靠腿蹬");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

我们定义了一个抽象汽车类,然后创建了两个子类,一个玩具汽车,一个自行车,然后看使用场景:

public void toBeijing(Car car) {
    car.run();
}
1
2
3

我们直接驾车出发,这里可以替换成Bike,因为可以骑自行车出发,但是能替换成ToyCar吗,明显不能,因为ToyCar不会跑。这就明显破坏了原来的逻辑,或者我们改写Bike,run()什么都不做,也会破坏原有逻辑,那么这里明显不能再用继承关系了,可以改用组合等模式,或者扩大父类,添加接口等:

# 扩大父类

// 泛指车
public abstract class Car {
}

// 带发动机的车
public abstract class EngineCar {
    abstract void run();
}

//玩具车
public class ToyCar extends Car{

}

//自行车
public class Bike extends EngineCar {
    @Override
    void run() {
        System.out.println("自行车靠腿蹬");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

添加接口就简单了,直接定义一个Run接口,里面定义一个run()方法,会跑的车实现Run接口,不会跑的不实现。可以看到,里氏置换原则是对java继承的校验,不适合的继承关系就不满足里氏置换,所以如果我们在纠结是否应该用继承关系的时候,可以套用套里氏置换来看效果。

# 依赖倒置原则

依赖倒置原则DIP(Dipendence Inversion Principle): 面向接口编程。

依赖倒置的官方定义很晦涩: 高层不应该依赖底层,两者都应该依赖抽象;抽象不应该依赖细节;细节应该依赖抽象。说白了就是: 面向接口编程,再广义一点就是: 面向抽象编程。也就是说,我们使用一个类的时候,尽量依赖接口,定义函数的时候,参数尽量传递接口,返回值也尽量返回接口,所有对外部类的依赖尽量都是接口,我们看个例子:

private void loadImage(String url, ImageView imageView) {
    Picasso.get().load(url).resize(50, 50).centerCrop().into(imageView)
}
1
2
3

代码很简单,就是用Picasso加载图片到ImageView,相信很多同学都这么写过,假设我们的项目中到处都是这样的代码,突然有一天,老大发话了,说要把Picasso替换成Glide,我一听美滋滋,这个简单,直接在gradle中删除Picasso的依赖并添加Glide的依赖,然后找报错的地方改成Glide不就行了,改着改着感觉有点不对劲,为啥不直接写个工具类,把加载图片的逻辑封装进去呢,这样后面改的话只要改那个工具类就行了,于是就写了个工具类:

public class PicassoUtils {
    //从网络加载
    public static void loadByUrl(String url, IamgeView imageView){
        Picasso.get().load(url).resize(50, 50).centerCrop().into(imageView)
    }

    //从文件加载
    public static void loadByPath(String path,ImageView imageView) {

    }
    //....从其他地方加载
}
1
2
3
4
5
6
7
8
9
10
11
12

改完后感觉爽歪歪,直接在这个类里面替换Picasso相关Api为Glide的就行了,爽啊!改完后,老大说,能不能让我可以选择性的使用Glide还是Picasso,比如对华为手机用Picasso,对小米使用Glide?WTF! 怎么办,此时就要用接口了,定义顶层接口,代码中使用图片加载的地方都使用接口,至于怎么实现,就是下层的逻辑了,不管用Picasso还是Glide,顶层逻辑都不需要改变:

interface ImageLoader {
    //从网络加载
    void loadByUrl(String url, ImageView imageView);
    //从文件加载
    void loadByPath(String path, ImageView imageView);
    //...从其他地方加载
}

//使用Picasso
public class PicassoLoader implements ImageLoader {
    ...实现相关逻辑
}

//使用Glide
public class GlideLoader implements ImageLoader {
    ...实现相关逻辑
}

//业务层使用,imageLoader是什么就看需求,随便创建,当然你可以定义一个全局管理类,里面视需求自定义全局PicassoLoader或GlideLoader
imageLoader.loadByUrl(url, imageView);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

从这个demo可以看到面向接口的好处: 能够减少耦合,代码容易维护,容易拓展,因为接口是抽象的,所以不用纠结于具体的逻辑,想怎么实现都可以;又因为接口是顶层的,所以下层就多,就更容易扩展,就更灵活。

# 接口隔离原则

接口隔离原则ISP(Interface Segregation Principle): 接口尽量小,功能尽量单一,说白了就是接口粒度要细。

接口隔离要求接口的功能尽量单一,而单一职责也是要求一个类只干一件事,他们有什么区别?

单一职责针对的是"职责",一个职责可能有多个功能,可能由多个接口完成;而接口隔离针对的是"接口",一个接口应该只负责"一个"功能,而不是"一块"功能,举个例子,我要实现一个音乐播放器,我只定义了一个接口:

interface IMusicPlayer {
    //开始
    void start(); 
    //停止
    void stop();
    //暂停
    void pause();
    //复原
    void resume();
    //获取歌曲时长
    String getSongLength();
}
1
2
3
4
5
6
7
8
9
10
11
12

这个满足了单一职责,但是却不满足接口隔离,假如我们现在有个歌曲展示器SongDisplayer,需要展示歌曲时长,那么我们也应该有个getSongLength()函数,我们直接实现IMusicPlayer接口吗,实现这个接口就必须实现里面的start()等方法,但是这些方法肯定不是我需要的,也不是我应该有的,这就是问题,因为接口不够小,不干净,不纯粹,明显违背了接口隔离原则,我们就可以对接口进行拆分:

//音乐播放器就仅限于对播放的控制
interface IMusicPlayer {
    //开始
    void start(); 
    //停止
    void stop();
    //暂停
    void pause();
    //复原
    void resume();
    ...
}

//歌曲展示器就仅限于对歌曲信息的展示
interface ISongDisplayer {
    //获取歌曲时长
    String getSongLength();
    //获取歌曲名字
    String getSongName();
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

此时我们创建的播放器就可以同时实现两个接口,只展示歌曲的话就只实现ISongDisplayer即可。

总之一句话: 接口要尽量小,尽量单一,尽量干净,尽量偏科。

# 最少知识原则

最少知识原则LKP(Least Knowledge Principle)也叫迪米特法则(LOD): 一个对象应该对其他对象有最少的了解,说白了就是:只关联自己需要的。

废话不说,我们来看demo:

public class MainActivity {
    //定义一个Button
    private Button btnOK;
    
    protected void onCreate(){
        setContentView(R.layout.main);

        //初始化并设置点击事件
        btnOk = findViewById(R.id.btn_ok);
        btnOk.setOnClickListenter(v->{
            Log.d(TAG,"hello world")
        })
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

代码完美运行,突然有一天,产品说Button不好看,要替换成别的控件,好啊,我们直接替换成TextView,然后一运行,直接崩了,为啥?因为TextView不是Button,我们还要把:

private Button btnOk;
1

改成

private TextView btnOk;
1

如果代码中只有这一个还好,如果很多呢,脾气不好的立刻就炸了;如果再后来又要改成一个ImageView呢?此时我们不仅想到:其实我们并不关心这个玩意儿是什么,只知道它能设置点击事件就行,因为我们只调用了它的setOnClickListener(),而这个方法只要是个View就能设置的,也就是说:我们只关心我们使用到的,这就是最少知识原则,知道的越少越好,只关心自己需要的。于是最终我们改成了:

privagte View btnOk;
1

这样以来,后面随便你怎么改xml文件,都无所谓了,岂不美哉!此时有人说了,我要是用setText()函数怎么办,那就声明为TextView啊,因为setText()的最直接定义者就是TextView,也就是与它直接相关的,Button虽然也有setText()函数,但是是继承自TextView的,不是直接相关的了,而且Button还有我们不需要的其他属性,我们并不关心。

所以我们应该只关心自己直接关联的,不关心那些不需要的,这样,那些发生在我们不关心的区域内的事,不会引起我们的任何改变,从而大大提升了代码的鲁棒性。

# 开放闭合原则

开放闭合原则OCP(Open Close Principle): 一个类应该对扩展开放,对修改关闭。

开闭原则是最基础的原则,也是最完美的原则,很难百分百实现,他要求我们的任何修改都是不改变老代码,只增加新代码,这样不会引起老逻辑的改变,使得代码更加安全。

举个例子,我们来写个计算器,只需要支持短整型的加法和减法就行,很简单,我们直接这么写:

public class Calculator {
    public static int calculate(int left, int right, String option) {
        //加法
        if("+".equals(option)) return left + right;
        //减法
        if("-".equals(option)) return left - right;
        new IllegalArgumentException("不支持的运算符");
    }
}
1
2
3
4
5
6
7
8
9

这样写怎么样,当然没问题,但是如果后期要支持其他的各种各样的运算,都需要在这里面添加if分支,缺点很多:

  • 1 可读性差,那么多if语句,每次都要从上往下找感兴趣的运算符,肯定不行。
  • 2 效率低,if太多,比如现在有100个if,我要计算的运算在最后一个if,肯定效率低。
  • 3 不容易维护,因为都在一个类一个函数中,同时只能一个人来修改,维护成本太高
    我们第一步优化就是拆函数:
public class Calculator {
    public static int calculate(int left, int right, String option) {
        //加法
        if("+".equals(option)) return plus(left, right);
        //减法
        if("-".equals(option)) return sub(left, right);
        new IllegalArgumentException("不支持的运算符");
    }

    //加法
    public static int plus(int left, int right){
        return lefft + right;
    }

    //加法
    public static int sub(int left, int right){
        return lefft - right;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这一步优化,仅仅是提出了函数,后面如果有需要改的地方,直接添加函数,然后添加if分支,但是还是费劲,既然提出函数还不行,那么再进一步,提出个类怎么样?

//定义计算器抽象类
public abstract Calculator {
    //左操作数
    protected String leftOpt;
    //右操作数
    protected String rightOpt;
    //操作符
    protected String operator;

    //设置左操作数
    public void setLeftOpt(String leftOpt) {
        this.leftOpt = leftOpt;
    }

    //设置右操作数
    public void setRightOpt(String rightOpt) {
        this.rightOpt = rightOpt;
    }

    //计算,提供一个模版函数,供子类实现
    protected abstract int calculate();

    //对外公开的获取结果的Api
    public String getResult(){
        //计算结果
        String result = calculate();
        //清空操作数
        clear();
        //返回结果
        retrun result;
    }

    //清空操作数
    public void clear(){
        leftOpt = null;
        rightOpt = null;
    }
}

//加法器
public class PlusCalculator extends Calculator {

    public static String OPERATOR = "+";

    public PlusCalculator() {
        super();
        this.operator = OPERATOR;
    }

    //加法器就实现加法
    @Override
    public String calculate() {
        return String.valueOf(Integer.parseInt(leftOpt) + Integer.parseInt(rightOpt));
    }
}

//减法器
public class SubCalculator extends Calculator {
    public static String OPERATOR = "-";

    public SubCalculator() {
        super();
        this.operator = OPERATOR;
    }

    //减法器就实现减法
    @Override
    public String calculate() {
        return String.valueOf(Integer.parseInt(leftOpt) - Integer.parseInt(rightOpt));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

代码很简单,根据不同的运算符定义不同的计算器实现类,每个类负责实现自己的计算逻辑,如果将来需要支持其他运算符,直接再添加一个对应的类即可,而且支持多人同时修改,比如A来做乘法器,B来做除法器,完事后merge一下代码即可。我们看下使用:

//加法
Calculator calculator = new PlusCalculator();
calculator.setLeftOpt("10");
calculator.setRightOpt("20");
calculator.cal();

//减法
calculator = new SubCalculator();
calculator.setLeftOpt("10");
calculator.setRightOpt("20");
calculator.cal();
1
2
3
4
5
6
7
8
9
10
11

可以看到,这里想使用什么运算就创建对应的计算器,不用再走if语句,提升了效率。而且此代码还可以扩展,通过使用工厂模式,我们可以将对应的计算器缓存起来,避免反复创建:

//定义一个计算器工厂,这里直接依赖的是Calculator这个抽象类,典型的面向接口编程
public abstract class CalFactory {
    public abstract Calculator create(String operator);
}

//工厂的实现类
public class ConcreteFactory extends CalFactory {

    //这一级缓存用来存放计算器的class文件
    private static final HashMap<String, Class<? extends Calculator>> map = new HashMap<>();
    //这一级缓存用来存放创建出来的计算器
    private static final HashMap<String, Calculator> calculatorHashMap = new HashMap<>();

    //静态加载,提前存放计算器对应的类,如果将来有其他计算器,也可以在这里添加
    static {
        map.put(PlusCalculator.OPERATOR, PlusCalculator.class);
        map.put(SubCalculator.OPERATOR, SubCalculator.class);
    }

    @Override
    public Calculator create(String operator) {
        Calculator calculator = null;

        //从缓存中取
        calculator = calculatorHashMap.get(operator);
        if (calculator != null) return calculator;

        //create
        Class<? extends Calculator> aClass = map.get(operator);
        try {
            calculator = aClass.newInstance();
            //放入缓存
            calculatorHashMap.put(operator, calculator);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return calculator;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

看下使用了工厂模式后的使用:

CalFactory factory = new ConcreteFactory();
String operator = "+";
Calculator calculator = factory.create(operator);
calculator.setLeftOpt("10");
calculator.setRightOpt("20");
String result = calculator.getResult();
1
2
3
4
5
6

使用很简单,直接传入运算符就行,不用在逻辑层直接创建具体的计算器,而且自带缓存,效率更加提升。

本章讲了六大设计原则,这是所有设计模式的基础,根据这六大设计原则领悟到23种设计模式,再由23种设计模式归纳到这六大设计原则,闭环的走上一遍,那就真的是领会贯通,达到第十层境界了。

Last Updated: 1/28/2022, 2:19:00 PM