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

先扯两句

  人的惰性啊,总是无限的,一不小心偷懒一次,就会是好长时间的懒惰,也不知道从哪里来的当头棒喝叫醒了我,才发现竟然又是这么长时间没有进步了。不过想来能来看这篇文章的你肯定是不会懒惰的,那就让我们一同坚持下去吧。加油!!!
  炫耀一下已有成功激励一下自己《设计模式》——目录,然后让我们进入正题。

定义

什么是开闭原则

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

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:鸣谢

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • 设计模式 - 开闭原则即 对立与统一原则 什么是开闭原则 软件实体应该对扩展开放,对修改关闭,即实体应当通过扩展实...
    小小小8021阅读 201评论 0 0
  • 前言 设计模式六大原则网上资料比较多比较乱,本文将网上的一些好的资料做一下整理,以便随时翻阅。友情提示,设计模式虽...
    简单的土豆阅读 1,427评论 0 10
  • 设计模式六大原则 开闭原则 开闭原则,是说对于软件实体(类、模块、函数等等)应该可以拓展,但是不可修改 这句话有两...
    ChaLLengerZeng阅读 872评论 0 0
  • 01 宽敞的房间,到处布满灰尘,一张双人床,床上杂皱不堪,洁白的床单微微泛黄。 地板上一个永远停留在12点的钟,落...
    我一路往北阅读 1,193评论 16 8
  • 实现目标的动机:爱和安全人的一生都在渴望爱与归属感。 人类天生的需求:关联感、胜任力、自主权 关联感 建立人际和亲...
    全新的饭阅读 152评论 0 1