天天看点

《设计模式》——开闭原则

先扯两句

  人的惰性啊,总是无限的,一不小心偷懒一次,就会是好长时间的懒惰,也不知道从哪里来的当头棒喝叫醒了我,才发现竟然又是这么长时间没有进步了。不过想来能来看这篇文章的你肯定是不会懒惰的,那就让我们一同坚持下去吧。加油!!!

  炫耀一下已有成功激励一下自己《设计模式》——目录,然后让我们进入正题。

定义

什么是开闭原则

  一不小心就到了《设计模式之禅》中六大设计原则的最后一个设计原则——“开闭原则”。其实就开闭原则而言,我们看名字还是很容易理解的,就是讲述了在架构的时候,哪些需要开放、哪些需要关闭的问题,而至于是对什么开放,对什么关闭呢?优先看一下《设计模式之禅》的描述吧。

Software entities like classes, modules and functions should be open for extension but closed for modifications.(一个软件实体如类、模块和函数应该对扩展开放,对修改关闭)

  可以看到这句描述中,老头子我着重标注了两句话:

  • 对扩展开放
  • 对修改关闭

  对于这部分怎么解释,我不知道想掉了多少根头(这可都是程序猿的命根子啊),终于想到了一个例子,终于还是在动物界找到了些灵感。

  先说说修改,大家都知道鲸鱼是世界上最大的哺乳动物,也不知道多少万年前,陆地上活不下去了,鲸鱼的祖先就回到的水里。把四肢修改成了鱼鳍,先不说鲸鱼在水中生活的开心不开心,虽然达成了生存的目的,但是鲸鱼却失去了原本在陆地上生存的能力。

《设计模式》——开闭原则

  而拓展呢,暂时没有找到实际的例子,但是想必大家都听说过一次成语:“如虎添翼”,这不就是在虎的基础上拓展出了翼的功能,使得虎不仅是百兽之王,竟然还会飞,你谁它气不气人。

《设计模式》——开闭原则

  很显然,拓展不仅能够支持新的功能,还能够保留原有功能的特性。而反观修改,每有一个新功能,就去改一部分代码,如此修改下去,迟早会把我们的代码改成特修斯之船,到时候回去看我们早已退化成鱼鳍的四肢,只能徒劳迷茫:

《设计模式》——开闭原则

  说到这里,我就当你听懂了为什么对扩展开发,对修改关闭,下面我们来看看官方些的解释吧。

大牛说

  开闭原则的定义已经非常明确地告诉我们:软件实体应该对扩展开放,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。

那什么又是软件实体呢?软件实体包括以下几个部分:

  • 项目或软件产品中按照一定的逻辑规则划分的模块。
  • 抽象和类。
  • 方法。

  如果是小学作文的话,这里一定要这么说一:美国作家斯宾塞·约翰逊在《谁动了我的奶酪》中说过有这么一句话:

世界上唯一不变的是变化本身

  (怎么样,逼格是不瞬间高了好多?)

  这句话用在我们程序猿身上,简直不要太贴切,对于大公司来说,程序猿恨不得掐死的是产品经理。在小公司就更惨了,因为你要掐死的将是给你开工资的领。所以,要么忍、要么滚。。。

  说实在的,很难滚到一个不改需求的地方,唯一的区别不过就是改的频率高低、以及给你拿来改的时间长短罢了。既然怎么都避不开,所以就只能靠增加我们的代码的灵活性来解决这个问题了。

  我们来写个直男的成长史吧,首先是一个人都有个能力,那就是安慰别人,当然安慰之后是否起到积极的作用因人而异,但是大家都具备这个能力,而我们传统直男在女生肚子疼时安慰的话,想必大家都知道:

/**
 * 人
 */
interface IPerson {
    /**
     * 安慰人
     *
     * @param name 被安慰人的姓名
     */
    void comfort(String name);
}

/**
 * 直男
 */
class StraightMan implements IPerson {

    @Override
    public void comfort(String name) {
        System.out.println(name + "多喝热水");
    }
}

@Test
public void talkWithGirl() {
    IPerson zhangSan = new StraightMan();
    zhangSan.comfort("everyOne");
}
           
《设计模式》——开闭原则

  很显然,这样的直男是找不到女朋友的,所以有朋友告诉他,对喜欢的女孩(Monica)换个说法。

  这里有几种实现的方案:

  1. 修改Iperson,添加comfortMonica()方法

  不过这就意味着所有所有人安慰Monica的时候都改变了,先不说其他人愿不愿意,我们的直男就不愿意啊,大家安慰Monica的方式都与自己一样,自己还怎么追女孩。所以这个方法肯定不行。

  2. 修改StraightMan,修改comfort()方法

  这个方法可以实现直男见到Monica的时候安慰的方式与其他人都不同,同时也能实现说的不是“多喝热水”这种作死的回答。但是一旦修改了这里,就相当于直男需要穿越回到过去,把每一句与Monica的安慰都进行替换,直男也很想,可惜实力不允许啊,也只能无奈放弃。

  3. 拓展一个StraightManForGirlFriend的类

  拓展一个想要找女朋友的直男,继承直男的所有特点,不过安慰人的时候添加了一个注意事项,那就是遇到“Monica”的时候,换一种说法。这样直男想找女朋友的时候,就换安慰方式,而且与其他人也都不一样。不想找的时候,还是可以保持原本的样子,也不累,直男很开心的接受了。

/**
 * 人
 */
interface IPerson {
    /**
     * 安慰人
     *
     * @param name 被安慰人的姓名
     */
    void comfort(String name);
}

/**
 * 直男
 */
class StraightMan implements IPerson {

    @Override
    public void comfort(String name) {
        System.out.println(name + "多喝热水");
    }
}

/**
 * 想找女朋友的直男
 */
class StraightManForGirlFriend extends StraightMan {

    @Override
    public void comfort(String name) {
        if ("Monica".equals(name)) {
            System.out.println(name + "你哪有肚子啊");
        } else {
            System.out.println(name + "多喝热水");
        }
    }
}

@Test
public void talkWithGirl() {
    IPerson zhangSanForGirlFriend = new StraightManForGirlFriend();
    zhangSanForGirlFriend.comfort("Monica");
    zhangSanForGirlFriend.comfort("otherOne");
}
           
《设计模式》——开闭原则

  可以看到,对于其他人来说,还是“多喝热水”,但是对于直男心仪的Monica,却换了一种说法“你哪有肚子啊”。

  可是直男也就是一时冲动,见改变一句安慰的话并没有追到Monica,直男直接放弃找女朋友了,变回了自己原本的样子。

/**
 * 人
 */
interface IPerson {
    /**
     * 安慰人
     *
     * @param name 被安慰人的姓名
     */
    void comfort(String name);
}

/**
 * 直男
 */
class StraightMan implements IPerson {

    @Override
    public void comfort(String name) {
        System.out.println(name + "多喝热水");
    }
}

@Test
public void talkWithGirl() {
    IPerson zhangSan = new StraightMan();
    zhangSan.comfort("Monica");
}
           
《设计模式》——开闭原则

  删掉想要找女朋友的直男后,当Monica来找直男说自己肚子疼的时候,得到的安慰,也变成了“多喝热水”。

  这里有一个注意事项,那就是:

  注意:开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。

  其实说起来也很简单,虽然我们添加了一个StraightManForGirlFriend,或者去掉了他,但是在talkWithGirl()测试方法中都需要添加或者删除StraightManForGirlFriend才能实现具体的功能。如果直男只是自己拓展了功能,但却不去表达,那对方怎么能知道直男的心意。而既然有了表达,就意味着两人交流结果的改变。所以受到被拓展功能实现时的代码还是要修改的,只是修改的比较少。而且由于是调整了子类,也能够与父类进行很好的区分,一旦真的不需要的时候,只需要干掉子类,将所有实现调整会父类即可。

为什么使用开闭原则

1. 开闭原则对测试的影响

  开发过程中,很少有功能模块是能够做到完全独立的,而这些都是经过详细测试的,或者已经在线上跑了很久,经受住了实际考验的。而再看我们要调整的内容呢,或多或少会对原功能造成影响,而这些影响都是要经过完整测试,才能够发布上线的,而且即便测试了,也很难模拟出来所有的实际环境。很难确定发布后不会造成其他的隐患。

  而且不是所有公司的调整都是经过严谨的评估分析的,我们很难保证领导一拍大腿想到的调整方案能够活过比较长的时间,所以我们现在调整时所有经历的测试流程,难保在领导反悔的还得再经历一遍,耗费的时间是完全没有必要的。(参见例子中的直男)

2. 开闭原则可以提高复用性

  其实这个部分就比较好理解了,毕竟直男的好奇心是很重的,不是单纯的想要尝试找女朋友,说不好哪天又想找男朋友了呢?说不好哪天又想养宠物了呢(想多的自觉面壁去)?如果针对每一个都在直接改变直男的兴趣爱好,会造成很大的影响。尤其当直男左手拉着女朋友,右手拉着男朋友去追狗的时候,绝对是个灾难。而当将直男的每一面都使用一个独立的类去刻画的时候,就可以大大增加复用的可能性。直男想要左手拉着女朋友,右手拉着男朋友去追猫的时候,修改起来也能比较轻松。

3. 开闭原则可以提高可维护性

  作为程序猿来说,想必我们之间应该有一个共识,那就是其他人写的代码都是“shit!”,前段时间公司有个APP页面要重构,同事说想要重新写,当时我那小暴脾气,我写的多好啊,怎么就得重写啊!可一想自己有多少次想要重写他的代码,瞬间就平衡了。所以在这个时候,坚持修改已有代码,对于改代码的和被改代码的来说都是一种折磨(代码被改以后,可能会从一个人看不懂,演变成双方都看不懂),所以还是直接写自己的拓展类吧,大家都维护自己的内容,轻松加愉快。腹黑点说,到时候发现看不懂总不能说别人写的不好不是。

如何使用开闭原则

  其实作为一个Android开发的小菜鸟,能够使用到的设计模式,其实并没有后台的多,因此对于设计模式的理解实际也是比较有限的,不然也不会这么长时间了,博客才写到开闭原则。因此在看开闭原则的时候,还是有些吃力的,尤其是“如何使用”这部分,其中举了一个login的例子,说是简单的例子,我反复看了5遍,也买看懂是怎么实现的元数据模块行为。好吧,其实元数据我是百度好久才看懂的(见PS1)。所以这部分先暂时按照我个人的理解去写了,后面大神们还是建议直接去看书,如果跟我水平差不多的,大家也可以看一看,不过要多找其他资料印证一下,后续对这部分有深入了解后,会回来重新调整(当时会认为自己现在归纳的特别精辟也说不定呢)

1. 需求优先

  框架搭建之初详细了解具体的需求都有哪些,并依据需求,详细列举所有的功能模块、数据类等,以及相互之间的交互关系,依据这些交互关系规划接口、抽象方法、实现类、数据类等内容,且制定后基类最好就不要再调整了。

2. 字段规范

  公司前段时间接口返回字段,“公司地址”,在A接口中是“projectAddress”,B接口中是“address”,在写接收的数据类时就需要添加两个接收的字段,且为了使显示部分的代码不至于混乱,因此在get方法中添加了逻判断。这样显然是不符合设计规范的,可是却由于字段规范问题导致不得不编写这些冗余代码。

3. 封装变化:

  其实这个名词我还是比较陌生的,查了N多文档,也没敢说完全了解,至少书中的“相同的变化”与“不同的变化”我就没理解是什么意思。所以这里就采用最浅显的理解说明了:

  对变化我这里的个人理解是有两种,第一种是可预测的变化,第二种则是不可预测的变化,而上面的安慰的话语打印就是可预测的,我们知道对方究竟会传入什么信息,以及接受到信息后,我们需要作出什么样的反馈。而不可预测的变化,就是我们可能根本不知道对方会传入什么东西,而且接收到了以后,也完全搞不懂对方会用来做什么。因此就要在封装的时候作出灵活的应对:

class StraightMan {
    /**
     * 可预测变化
     */
    public void comfort(String name) {
        System.out.println(name + "多喝热水");
    }
}
           

  上面的方法其实就是对于可预测变化的封装,因为我们并不知道需要打印的name具体是什么,所以就使用封装方法的形式,无论传入的是谁的名字,我们打印的结果都是让其多喝热水。

class StraightMan<T> {
    private OnStraightManComfort<T> onStraightManComfort;

    public StraightMan(OnStraightManComfort<T> onStraightManComfort) {
        this.onStraightManComfort = onStraightManComfort;
    }

    /**
     * 不可预测变化
     */
    public void comfort(T t) {
        if (null != onStraightManComfort) {
            onStraightManComfort.comfort(t);
        }
    }

    public interface OnStraightManComfort<T> {
        void comfort(T t);
    }
}
           

  而当我们的变化并不可预测的时候,则可以通过回调的方式,将信息回传,并依据当时的实际情况作出对应的设计。

  如此在明确的时候,可以实现简单明了的封装,又可以在不可预测的时候增加框架的可扩展性,也同时避免不可预测时猜测可能传入的数据类型,以及回传的内容,而一一列举所导致的过度设计。

PS1:元数据:鸣谢什么是元数据?为何需要元数据?

《设计模式》——开闭原则
  元数据(meta data)——“data about data” 关于数据的数据,一般是结构化数据(如存储在数据库里的数据,规定了字段的长度、类型等)。
  一个基本的元数据由元数据项目和元数据内容的构成。这里,“题名”就是它的元数据项目,“史蒂夫·乔布斯传 (美) 沃尔特·艾萨克森著 = Steve Jobs Walter Isaacson eng”就是元数据内容。

PS2:鸣谢

  • 如虎添翼:李逢君画虎,取自可怜“天下第一飞虎”,如今已成绝唱
  • 鲸鱼:Image by Pexels from Pixabay
  • “我是谁”:选自武林外传

继续阅读