设计模式学习感想

一、为什么要学习设计模式

设计模式一定要区分广义的定义和狭义的定义,广义上讲,设计模式包含所有软件工程中的设计模式,包括专业领域内的整体复杂系统设计;而狭义的设计模式是一些解决一般性问题而经过长时间经验总结得到共识的设计模式,我们常说的设计模式是指狭义的设计模式,而且我们大部分学习的书籍也是在讲狭义的设计模式。
其实这也和很多书一样,书中内容并不能包含现实遇到的所有具体问题,如果你发现一模一样的问题,那确实算你运气好。古话说举一反三,用现在话说就是学会抽象思维,虽然人类很自然地就会使用抽象思维,去想象和预测那些并没发生的事,但对于复杂事物的抽象就仍然属于专业领域,比如哲学家们,数学家们,以及计算机科学家。
软件设计中常说面向对象编程,其实用中国的一句古话来形容就是初期看山是山,后来看山不是山,最后看山还是山。面向对象最容易犯的错就是用常识和习惯去建立业务实体,也叫领域模型,当然为了方便其他人理解,还是会使用一些和业务实体相近的称呼来定义程序实体,比如共享汽车和私家车都是车,从属性上讲可能是一样的,但从行为来说就会有很大不同,如果一个管理车辆的平台,只管理私家车,那么它直接建立的一个车的实体代指私家车;而一个平台只管理共享汽车,他也会建立一个叫车的实体代指共享汽车。而如果一个平台两种车都要管理,甚至将来还可能扩展到其他业务类型的车,比如公交车,那么它可能会定义一个抽象的实体车类型作为子类型车共同的父类,这三种情况都有个称为车的实体,但是它代表的意义和本质都是不同的。其实我们就是要从常识出发,但根据具体的环境,又要跳出常识(但重新定义一个合适的实体名称实在是很难),去重新认识这个实体,为了找个既能让人理解又能尽量准确描述实体行为的名称,设计模式也帮助我们积累了很多类似前缀或后缀的词组来辅助描述实体的变体。特别是一些抽象的事物,如关系、策略、转换等,我们不是哲学家,还是用前人定义好的名词来描述更加准确和方便交流。
学习狭义的设计模式,我认为第一个目的是为了沟通上的方便,当我们对新的业务或功能讨论其设计时,设计模式就像一个初始模板,当所有人大脑里都具有这个统一的模板,设计就不用从白纸开始,而是有一些前提条件,会议的讨论就可以从这个基础进行增改,甚至可以很快地重新换一个类似的模式,并保留已经确定的一些结论。
而且在协同开发过程中,设计模式让我们通过一句话传达了很多信息,当然前提也是我们之间理解的设计模式是一致的。比如两个开发者A和B,A要求B使用工厂模式提供一个公共服务的使用,即使开发人员B由于不熟悉以前的业务,并不知道为什么要使用工厂模式(当然这很难保证信息传达的一致性),但仍可以按照A的预期去完成代码的编写,而这就省去了A去向B描述类甚至成员,以及他们之间关系的繁杂过程。

二、学习设计模式,我们在学习什么

在如同其他设计模式的文章那样开始给各种模式分类、细数他们的特征、并且区分那些相近的模式之间有何不同……之前,我认为了解设计模式出现的目的更为重要。设计模式的目的可以分为两个方面,保持软件设计原则,以及消除我们常说的坏气味(bad smell)。
当你认识并遵循这些软件设计的原则并努力减少那些阻碍原则的因素,即使你不知道什么设计模式,你也会想方设法去重构你的代码,朝着正确的方向前进。但如果熟悉设计模式,就相当于有很多重构的工具可用,借鉴前人的经验,快速地看清重构的路线。所以学习设计模式,更重要的是学习软件设计原则,并使之成为习惯,否则正如有人说过,只知道设计模式反而禁锢了你的思想,所有解决方案都想套用设计模式,其实反而是无必要的过度设计。

简介软件设计原则:

(1)封闭开放原则

其实这个原则在很多不经意的地方都有体现,核心就是把变化的部分和不变的部分隔离并保护不变的部分不受影响,同时也把不应相互影响的变化部分从代码层面进行隔离。最常见的例子就是把代码块抽离为方法,方法内的局部变量不会对调用者的环境产生影响,方法的入参可以通过传值(拷贝)的方式避免对外部的变量产生影响,那么当作为这个方法的抽象部分发生变化时,我们可以放心地修改这个方法内部的逻辑,而不会担心会把影响扩散到其他地方而导致不可预见的bug产生。开放封闭原则在设计模式中比较经典地体现在桥接、策略、模板等模式中,但总的来说就是为了把一段代码或一个类的影响限制在合适的范围内,称为有限影响或可控影响的代码。

(2)单一职责原则

从字面意思上似乎很容易理解,类或方法从名称上来判断他的功能及职责范围,这有点业务边界的意思,也是从可读性角度要求的原则。但很多时候一旦涉及到边界,就会慢慢把边界模糊,从开发效率及粘滞性的影响中,慢慢就把过多的功能集中在单个方法或类中。比如构造函数,一个类的构造函数创建他自己,看起来很明确的职责划分,但是当这个类引用了很多其他类的时候,他怎么知道其他类是如何初始化的呢?所以我们开始在构造函数中传入其他类的实例、或者各种复杂的配置符号,让这个根类型去初始化自己的成员,然后你会发现成员类也很类似地需要各种配置和组装……所以有时候一个功能很简单的时候,也许不存在单一职责原则的使用条件,但是当功能不断复杂的时候,职责细分的需要就出现了,我们需要一个专门的类来打理构造逻辑,而不仅仅是一个函数。工厂模式和建造者模式可以作为上述问题一个解决方案,虽然他们还有其他更合适的适用场景。遵循单一职责原则重构那些庞然大物,会让你重新认识业务的边界。

(3)Liscov替换原则

这个原则主要在于检验继承的边界,同时也是比较面向验收和单元测试的原则,简单说就是所有子类都要满足父类或接口的约定。初看这个要求不高,但当需求逐渐发生变化时,之前的约定也发生了变化,但是否所有子类都在严格遵守这个约定呢?当你试图逐一修改这些类族的实现时,也许就发现永远也无法满足需求了,那么解决方法是什么呢,只能解除继承关系,为了某些上层接口不受影响,你新增了兄弟类型,不过总之这让你之前的设计收到重大挫折,为了在几个很相似的类中满足之前的统一接口,你需要做个兼容功能,于是适配模式、代理模式似乎可以派上用场了。替换原则由于面向测试更多,很多细微的实现差别将会导致类型的分支。

(4)依赖倒置原则

其实和开放封闭原则很像,只是角度是从顶向下设计时,底层的实现不会影响上层,而所谓的上层设计就是接口化和抽象化,所有涉及到实现的代码,包括new和malloc等实例化的代码,都通过类和方法遮蔽,当然有时候这样做会产生大量类型,这就需要你判断顶层与底层,甚至中间的过渡层的划分,以及使用这一原则的程度。

(5)接口隔离原则

这个原则也是与继承有关,只不过与替换原则不同,这次问题不是出在子类,而是在于父类。父类的变化持续影响子类,导致不能遵守单一职责以及替换原则,这时只能再次从父类分家。很多时候继承很容易违背这些原则,所以更多的模式采用组合的方式来复用功能,比如组合模式、装饰模式等,当然这就需要付出更多显示构造的代价。

简介常见的坏气味:

(1)僵化性

常见依赖关系的不合理,无必要的耦合导致,一处改动,很多个项目需要重新编译,需要审视是否真的如此重要。

(2)脆弱性

需要频繁维护的代码,即代表脆弱,一方面是减少耦合,另一方面就是遵循开放封闭、依赖倒置的原则,让影响有限

(3)牢固性

功能的通用性较低,依赖性较高,导致如果要复用,需要付出较大的代价(包含库的臃肿性和开发人员的操作复杂度),一般利用多态的特性让用户做简单配置即能使用

(4)粘滞性

一句话描述:想做正确的事很难,做错误的事很容易。看起来是废话,其实说的是开发环境和开发框架是否足够友好、自动化,让人不会有选择更快更短但错误路径的趋势。比如有自动化打包脚本就会增加测试的频率,而不是等一批功能做好后再测试。

(5)复杂性

一般来说就是过度设计,当然这个和有预见性要区别,但真正能区别的只有领域专家。如无必要,勿增实体,对业务的熟悉度,以及与领域专家的有效沟通,是避免复杂性的唯一方法。当然也包括很多误用、滥用设计模式的情形。

(6)重复性

这个概念理解很简单,但相似的代码要处理是个大问题,你甚至很难给相似的两个类或方法取个好名字,以及给提取出的公共方法想个更恰当的名字,而且因为抽象的改变,整体代码的可读性也会受影响。同时常伴随产生抽象层次的复杂性。

(7)晦涩性

注释、方法名、类名,把复杂性隐藏,以及把复杂逻辑简化,这是个从简到繁,又从繁到简的过程,需要对业务和算法相当熟悉,又能通过语言巧妙传达,总之要做好比较难,还是从坚持维护注释的习惯开始吧!

未完待续,下一篇预告:

三、一些有体会的模式
四、其他

作者:gofun成都技术中心-郭径遂

你可能感兴趣的:(设计模式学习感想)