关于设计模式

一、概述

设计模式是解决某些具体情景的问题而抽象设计出的一种通用的模式,也可以理解成是针对特定上下文的特定问题给出的解决方案,这些解决方案大多经过相当长的一段时间的试验和错误总结出来的,往往代表了最佳的实践;

二、目的

设计原则是面向对象设计为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则(摘自设计模式的艺术)。

在针对某个特定场景下具体问题设计一种适用的模式时,需要参考设计原则,避免设计出的模式不符合可维护性复用的目标,换句话说,从另一个角度理解,结合具体场景设计得出的二十三种设计模式进行抽象后应该可以得到设计原则。

而设计原则就是可维护性复用这个最终目标结合某些具体的手段或者面向对象特性所得到的的产物,而这个具体的手段就包括解耦等,下面是设计模式的一个梗概。

(图不是我的,其他是我自己总结出来的,这样梳理会更好理解,先理解设计模式最终极的目的,再理解抽象层次稍低一点的设计原则,最后,理解了设计原则后再学习设计模式,会事半功倍,具体的手段并不仅仅是解耦,应该还有其他)。

三、开闭原则

开闭原则的定义是软件实体应该对扩展开放,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化的。

这个原则应该是比较特殊的,因为相比较于其他设计原则而言的话,我感觉它的抽象程度要更高一些,其他设计原则都比较具体一些,可以像下面这种来理解。

开闭原则是最基础的设计原则其它的五个设计原则都是开闭原则的具体形态,也就是说其它的五个设计原则是指导设计的工具和方法,而开闭原则才是其精神领袖。

思考:开闭原则也仅仅是告诉我们应该对扩展开放,修改关闭,但是为什么我们不希望修改已有的代码,而是更希望在其基础之上做增加;

  1. 原先稳定的代码在被修改之后可能就会变得不稳定,同时,若修改原功能的代码,依赖于原本功能模块的其他上层模块可能也会因此变得不稳定,这会增加风险和成本,如果我们通过某种方式进行增强,而非修改的话,至少不会造成原本功能的异常;
  2. 可以保证原先功能模块的可复用性,如果直接修改原先模块的代码,为其增强功能的话,相当于是将原先功能单一的模块的粒度变大了,不利于此模块的复用,也就是满足开闭原则有助于保证功能的可复用性
  3. 修改原先模块的代码需要我们对功能模块的内部实现充分了解才能保证修改是正确的,这就增加了成本,并且即使读懂了代码,在内部修改代码增强功能,越往后,代码的可读性也会变得越低,可读性可维护性会变得越来越差,如果是通过不修改代码的方式进行扩展的话,我们仅仅需要了解原先功能模块的输出输出与副作用的基础之上,再包装一下就可以了

总结:开闭原则是面向对象设计中最基础的设计原则,它要求我们要尽可能的通过保持原有代码不变添加新代码而不是通过修改已有的代码来实现软件产品的变化,但是开闭原则可能是设计模式六项原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们

最后,开闭原则的具体实现还是要面向接口构建框架,用实现扩展细节,可能其他原则都有向这个目标靠拢实现的意思在里面。

参考https://www.jianshu.com/p/ed8dd289dd05  这篇博客写的还不错,有参考价值

四、单一职责原则

单一职责原则的定义是不要存在多于一个导致类变更的原因,更通俗的说,即一个类只负责一项职责,此处应有思考,为什么要满足单一职责原则呢?为了什么目标,如果不满足单一职责原则会有什么后果呢

问题由来:假设类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。

解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。

优点:一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多,有助于提升类的可读性,系统的可维护性,另外,如果我们把功能都耦合在一起的话,这种粒度过大的设计也不利于复用

总结:首先,目标就是可维护性复用,这个设计模式的终极目标,提升可维护性与复用性,如果不遵守单一职责的话,一方面可读性不好,另一方面可能存在修改一个职责导致所有功能都无法正常执行,这种牵一而动全身的问题;

从另一个角度来看的话,单一职责原则是通过解耦的方式来实现可维护复用这个最终目标,单一职责原则是实现高内聚、低耦合的指导方针,根据职责进行更细粒度的划分。

在具体实践的过程中,我们通常严格保证接口与方法的单一职责,对于类而言的话,由于面向对象语言中的类本身就可能对应现实世界中复杂的事物或者业务等,所以,对于类的职责分离可以将他的粒度放的更大一些,职责范围并不仅仅只是一个小的功能,这个可以参考领域驱动DDD的设计来实现(比较烧脑壳)。

参考https://www.jianshu.com/p/4b7921691a5c

      设计模式六大原则(1):单一职责原则 - 简书

      软件开发中的单一职责 - 简书

      面向对象设计原则之单一职责原则_刘伟技术博客-CSDN博客

五、依赖倒置原则

依赖倒置原则的定义是高层模块不应该依赖于底层模块,两者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象,更具体的说,面向接口就完事了。

什么是依赖倒置:要理解依赖倒置,首先我们需要理解什么是依赖不倒置,依赖没有倒置的话应该是什么?应该是正向依赖

结合定义来理解的话,不可分割的逻辑操作我们将其理解为底层模块,而通过组装等方式将底层模块结合起来的模块就是高层模块,那么,高层模块依赖于底层模块这就是正向依赖(依赖于具体实现,结合现实生活中人坐车到达某目的地这个例子可以更好的解释);

但是依赖倒置说的并不是模块的依赖关系发生变化,底层模块反而依赖于高层模块,并非如此,而是说的是两者都应该依赖于抽象,此处抽象指的是对底层模块的抽象,在Java中通过接口来实现这种抽象,就相当于高层模块与底层模块之间加入一个抽象层。

我们为什么要这么做呢?意义何在

存在的问题:我们说的依赖放在面向对象语言Java里面就是引用关系,比如某类中具有其他类的域,这种就是我们所说的依赖关系,至于正向依赖就是某类具有某个具体实体类的引用,但这种正向依赖存在一个问题,如果需求发生变化,在原有功能的基础上还要做横向的扩展的话,就一定需要修改上层模块或者底层模块的源代码,这是不符合开闭原则,出现这种问题的原因就是抽象的程度不够高,换句话说,这就是底层模块与高层模块之间的强耦合导致的问题,因为依赖的是具体实现。

解决方案:通过依赖倒置可以降低类之间的耦合度,更具体一点就是通过对底层模块进行抽象设计成接口作为中间层,对上层而言,我只需要告诉中间层我需要什么样的功能,什么样的输入输出与副作用,对于下层而言,我只需要按照中间层想要的功能去实现即可,将接口理解为契约,约定双方,这样的话,上层不必依赖具体实现。

好处:降低模块之间的耦合度,提升后期的扩展性。

总结:依赖倒置从定义上来说是指高层模块不应该依赖于底层模块,两者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象,倒置是相对于我们现实生活中正向的依赖,映射到程序中就是某个类依赖于某一个具体的实现类,这种做法会导致后期做功能扩展时必须要修改代码,破坏开闭原则

而通过依赖倒置,我们将实体依赖转换为对抽象的依赖,上层模块与下层模块仅仅依赖于约定,而非具体的实现类,通过这种方式实现解耦,目标是它们的耦合关系需要达到当一个对象依赖的对象作出改变时,对象本身不需要更改任何代码。

依赖倒置这个原则告诉我们应该要依赖抽象,通过抽象去构架框架,易于切换组合的实现去填充细节这也是保证开闭原则的前提(很重要),更简单的说面向接口开发。

举个例子来说的话,Spring通过文件配置Bean实体与其关系,并实现依赖注入,注入对象到指定域中,而域本身的类型通常是某接口,这就是满足依赖倒置原则的,后期若有变化,我们通过配置文件切换注入的域对象即可,连代码都不需要更改,甚至服务器也不用重启,当然Spring在容器初始化的时候读取配置文件,要实现这样的效果可能还需要额外的增加动态更新的功能。

参考:https://segmentfault.com/a/1190000012929864 这篇还行,将就

六、里氏替换原则

里氏替换原则的定义是任何基类出现的地方都可以透明得使用子类去替换这应该很好理解,父类出现的地方都可以使用子类的对象去替换,最初我对这个原则是有一点疑惑的,因为在面向对象语言Java中,在编译期间子类对象替换父类引用是没有任何问题的,这根据没有必要单独提出,但是,其实里氏替换更多的是在说继承的规范

虽然在编译期间,可以使用子类对象替换父类引用,运行期间,由多态找到实际子类并执行相应方法,而里氏替换说的是,我们在应该要保证实现子类替换后的程序仍然可以满足之前最基本的功能,子类仅仅是通过继承实现了代码复用与扩展增强功能,子类在继承时应该要满足这一点,不应该重写父类已实现的非抽象方法,破坏原有的功能,而前面提到的继承的规范指的是子类在继承父类时应该遵守的一些规则。

里氏替换给出了详细的规范:

1. 子类可以实现父类的抽象方法,但是不能重写父类的非抽象方法(这里需要区分下重写和重载的概念,重写是指函数名相同函数的签名或者说参数类型相同;而重载是函数名相同,函数的签名或者说参数类型不同,面向对象语言的多态性会在运行时根据传入的实参类型自动匹配最适合的函数进行调用)

2. 子类可以增加自己的特性

3. 子类的在重载父类方法时,方法的前置条件(形参)要比父类方法的输入参数更宽松(这是因为如果子类方法的参数范围更小的话,在运行调用函数的时候,可能就会先调用子类的重载函数,这是违反里氏替换原则的)

4. 重载父类方法时输出结果(返回值)可以被缩小

与多态是否矛盾?联想到面向对象语言特效之一的多态,多态保证我们通过子类对父类方法的重写在不更新原有代码的基础之上更新程序内部的逻辑,所以,我们应该要重写父类方法才可以,这似乎与里氏替换所表达的有些不太一样,我是这么理解的,里氏替换是对继承具体实施规范的一个补充,对于抽象方法而言,这是子类一定要重写的,对于已实现方法而言,子类不应该去重写,而是应该要保证其不被修改,确保原有的功能不发生变化。

子类如何增加自己的特性?一般来说是子类定义自己的方法,但是这样在替换父类后,自定义的方法如何被调用呢?这需要修改代码才行,我理解的是,通过模板模式结合策略模式,父类中通过非抽象方法定义逻辑主要执行流程,但是会通过抽象方法定义扩展点,这需要考虑到后期需求可能发生变化的一些点,提前将其定义为抽象方法由子类去扩展,然后结合策略模式实现在不修改原有代码的基础之上对其进行更新。

总结:首先,我们之前提到过开闭原则,这是一个目标,而其他的原则大多都是实现开闭原则的一个具体的规范,里氏替换也不例外,实现开闭原则需要通过抽象构建架构,实现填充细节,关键步骤就是抽象化,而依赖倒置指导我们将模块之间通过抽象解耦,以便通过子类的实现去填充细节,而基类与子类的继承关系就是抽象化的具体实现,里氏替换说的就是子类应该如何定义,实现子类的规范,里氏代换原则是对实现抽象化的具体步骤的规范,这就是里氏替换与开闭原则的关系

七、接口隔离原则

接口隔离原则的定义是客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上,简单的说,就是一个类所依赖的接口中不应该包括该类不需要的方法,这种方法对该类而言是没有意义的,还必须要求重写。

简而言之:尽量细化接口,不要建立臃肿的接口接口的方法尽可能的少

这么去理解的话,是非常简单的,不过需要注意的是接口隔离原则更多的应该是指导我们在设计接口时的一个原则,它要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。

那么,为什么我们需要按照这个原则去设计原则呢。

理解:先从面向对象的角度来看,我们说类是现实世界中一个实体的抽象,而接口则应该是某种类型的行为的抽象,接口的抽象程度比类更高,但是范围却应该要比类更窄一些,类对应的是实体,而接口对应的是行为。虽然,接口在实际项目中可能也会是某个实体的抽象,但我个人认为这并非真正正确的用法。

上面,我提到了接口应该是行为的抽象,并且是同一个类型的行为的抽象,我们肯定不应该将很多行为方法都放在一个接口里面,对吧,面向对象就是这么要求的。那么,接口隔离原则其实也是这么个意思,使用接口隔离原则对接口展开设计的话,具有以下优势

1)将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性可维护性不至于因为某个行为的变化导致整个接口都需要更新(重点,这一点和单一职责原则是非常类似的,提高了系统的内聚性,降低了接口之间的耦合性

2)如果接口的粒度大小定义合理,能够保证系统的稳定性;但如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码重点

优势总结:接口就应该是某一类型的行为的抽象,合理的粒度设计,如果接口中掺杂了很多其他的行为,那么,将会导致继承类实现冗余的代码,同时,一个行为方法的变更将会导致整个接口都需要更新,那么,接口的变化也会导致所有实现类都需要变化,而这个变化的方法可能只是这个类的冗余代码而已,这就令人很不爽了,不满足高内聚低耦合,可维护性与可扩展性都会很差。

其实从接口隔离原则本身可以说的东西并不多,但是可以借鉴这种思想,借助这种思想去思考其他的问题。

参考 设计模式-接口隔离原则-》面向对象设计原则 - 知乎

小话设计模式原则之:接口隔离原则ISP - 知乎 接口隔离与单一职责区别

设计模式之美学习笔记18: 理解接口隔离原则(ISP)中的接口 - 架构小白|青蛙小白|关注程序开发、互联网技术、云原生 更多的思考与类比

七、迪米特原则

迪米特原则的定义是一个对象应该对其他对象有最少的了解

直接从这句话来理解,比较抽象,怎么才算是对其他对象有最少的了解呢,可以先看比较形式化的定义,该原则强调了以下两点:

第一要义:从被依赖者的角度来说:只暴露应该暴露的方法或者属性

第二要义:从依赖者的角度来说:只依赖应该依赖的对象,也就是说方法内部所使用的对象只应该是该方法所属类自身的域或者该方法的参数所带入的对象,而不应该是凭空new出来的那种对象,我们应该避免自己凭空new出新对象;

我自己来理解的话,可以分为两个阶段,下面是我理解得一个递进记录。

  1. 最开始的时候,我所理解的迪米特原则仅仅只是第二要义,即我们方法内部不应该直接New出新对象,这种对象并非方法参数所带入的,这是一种让人不舒服的耦合,因为New的方式太僵硬,参考Spring的依赖倒置与控制反转,一旦写死了的话,就无法像方法参数那样可以通过接口替换实际类对象,所以说,从依赖者的角度来说,依赖只依赖应该依赖的对象,那种凭空出现在方法内部的对象我们应该避免
  2. 后面的话,我认识到了第一要义,第一要义很多时候会被认为成第二要义,有这么一个情况是我从参数对象中取出了该参数对象的某个域,并根据这个域做了一些逻辑处理,那么从第二要义来看的话,貌似凭空出现了接口引用,但其实是允许的,因为并没有凭空New出来,这样的话,应该就是允许的,毕竟方法返回的对象也可能是其他的接口引用

那么什么是第一要义不允许的呢?

在方法functionA内部,获取参数对象的域,并对域做一系列的逻辑处理,但是这部分逻辑处理从面向对象的角度来看的话,其实应该是属于参数对象所属类的业务,而不是这个方法functionA里面去进行这些处理,回到该原则的基本定义。

一个对象应该对其他对象有最少的了解,方法不应该插手属于其他类的业务逻辑,不应该是我这个方法取出你这个对象中的域来做完成属于本应该属于你的业务逻辑,这是不应该的,并且,最好是方法根本不会去获取参数对象内部的任何属性,这样才是最好的,如果需要获取内部属性,说明方法极可能插手了参数对象内部的业务。

参考文献说的很明白:迪米特法则与重构 - 知乎 讲得不错,最好看下

参考:设计之道——揭秘迪米特法则 - 知乎  设计之道——揭秘迪米特法则

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值