说道设计模式,还是当初入行没多久看的,记得当初看的是秦小波的《设计模式之禅》。起初不明白这个为什么要字一个禅字。直到看了第三遍才明白,就算是同一个人也会因为经验和知识增长得到不同的结论。最近在重构之前的代码,恰好有用到设计模式,所以就准备重写回顾一下,记录一些现在的个人见解和实际工作中的体会。按实际开发实际我还是个不足2年经验的小猿,若有考虑不周,还请见谅!
要说设计模式,那就离不开六点设计原则。那么就从设计原则讲起。
一、单一原则。
单一原则这个原则有很多争议,它讲的是一个类要控制其粒度大小,规定一个类应该只有一个发生变化的原因,也就是一个类要尽量只负责一件事情(个人感觉在实际开发中,好多类都没有严格遵守这一原则,或者说很难做到这样)。为什么要这样呢,我讲一个工作中遇到的反面例子,在之前的公司做数据处理,根据客户的要求要根据不同来源的数据做不同的映射,映射大体方式类似。于是为了方便大家开发便绞尽脑汁封装了一个公共处理类供大家使用。开始项目初期数据来源少,并没有发生什么问题,大家也很满意,(大家注意转折来了)但是随着项目的推进,数据来源越来越多,随之而来的数据映射方式也越来越多。为了适应各种映射方式大家开始在公共类中加入一些可以覆写的空方法,用子类继承公共类覆写某些方法来实现改变(模板设计模式)。后来就算这样也无法解决映射逻辑的不同,除非覆写主要的映射逻辑方法,这样和重写一个类没什么区别。于是有人开始修改原始公共逻辑来适配自己的映射。这一改各种莫名其妙的bug接踵而至,把搞得大家焦头烂额。这件事让我明白,一个类承担的责任相对单一,那它被改动的概率和范围就越小,那这个类出现问题的概率就越小。这也许就是单一原则的期望之一吧。


二、里氏替换原则。
在面向对象语言中有一种机制叫做继承,继承的优点必我来说了吧,比如代码复用啊,提高代码的可扩展性啊,现实多态啊等等。缺点也不言而喻继承是入侵性质的代码耦合太高。而里氏替换法则要说的就是只要父类能出现的地方,子类就可以出现并且替代父类正确的运行。但是反过来却不行,也就是说应该是子类的地方,父类未必能给运行。《设计模式之禅》一书中说道:里氏替换法则为良好的继承定义了一个规范,一句简单的定义包含了4层含义。

  1.子类必须完全实现父类。举一个很简单的栗子,我有写过一个对json串进行排序和比较地小工具,我在里面写了一个预处理json串的类,他要做的工作就是预先处理下json串,比如去掉首位大括号前的一些莫名其妙的字符和去掉转译符,突然有一天我发现它已经不能胜任我新的需求了但是我又不打算改变以前的代码,因为这样会造成很多奇怪的问题第一点中已经提到过了。所以我打算写个类继承这个处理类,然后用新的其子类来代替父类对json串进行处理。但是因为疏忽,我忘记在新的子类中调用父类的方法,这就给自己挖了一个巨坑,如果别人输入的json串包含转译符或者首尾大括号前后有奇奇怪怪的字符就会导致程序解析json时报错,无法得到预期结果。

  2.子类可以有自己的个性。通俗来讲那就是子类是父类的拓展,可以完成一些父类做不到的事情。打比方你有一个父类是Birds(鸟类),这个类中有一个方法叫做flight(飞行),你可以写一个子类Swan(天鹅), 天鹅是鸟类所以它继承自Birds拥有飞行方法,但是天鹅也有自己独特的地方它会游泳,所以它也有自己有的方法Swimming(游泳)。

  3.覆盖或实现父类的方法时输入参数必须等于或者大于父类方法的参数。关于这点是因为java自己重载原因。决定一个方法是否是同一个方法的判断条件是方法名和入参。入参改变了的话编译器认为是重载方法而不是覆写。根据重载和静态分派原则JVM会优先选择参数类型和自己静态类型一致的方法运行。如果子类的方法参数静态类型,窄于父类的类型,当子类被当做父类传入时,可能会导致子类没有覆写父类的方法,却执行了子类的方法,引起逻辑混乱。(代码示例如下)

import java.util.HashMap;
public class Father {
    public void func(Map m){
        System.out.println("执行父类...");
    }
}
 
import java.util.Map;
public class Son extends Father{
    public void func(HashMap m){//方法的形参比父类的更宽松
        System.out.println("执行子类...");
    }
}
 
import java.util.HashMap;
public class Client{
    public static void main(String[] args) {
        Father f = new Son();//引用基类的地方能透明地使用其子类的对象。
        HashMap h = new HashMap();
        f.func(h);
    }
}
运行结果:执行子类...

  4.覆写或者实现父类方法时输出结果可以相等或者缩小(这点如果是变大是通不过编译的)。


三、依赖倒置原则。
定义:模块之间的依赖,都应该依赖对方的抽象接口,而不发生实现的直接依赖,并且实现应该依赖抽象接口。这个相信大家在日常使用中应该经常用到。说的简单点就是面向接口编程,就拿运行游戏来说吧,你有一台游戏主机,它有一个方法叫做function(运行),这个方法接受一个参数游戏。假设我们现在固定传入的参数写的是实现类比如XX游戏,那下次如果出现了新的YY游戏,你的主机的运行方法竟然无法接受并运行它这明显是不合理的。也许你会说,那再加一个方法呗,入参写上YY游戏。如果这样那是不是每发行一个游戏,主机就要写一个方法,并且编译安装一次这是不是太麻烦了也大大增加了系统的不稳定性(你每发布一个游戏就要修改一次系统,这样的系统能稳定吗?)。为什么会造成这样的结果,其原因是因为两个模块之前互相依赖的是实现类,耦合太过紧密,如果一个发生变化那另一个也要为这个改变做出适配。如何解决这个问题,那就是上面谈到的面向接口编程。接口就是一个契约,所有的游戏厂商和主机生产商坐在一起商量下,定个规范,比如游戏接口,规定游戏应该怎么怎运行。所有的游戏厂商就按这个契约开发,主机厂商也按这个契约来进行系统开发和适配。这样就皆大欢喜,只是要按这个规范来开发的游戏就可以运行在所有的主机上,而按这个规范开发的主机也可以运行所有该规范下的游戏。皆大欢喜,大家都不要为这个适配做重复的工作,世界也变得美好了,我们也不用加班了。多好。