设计模式
构成
设计模式由原则和方法两部分组成,原则代表了基本思路,而方式则是其的具体实现。另外设计模式是一种经验主义,所以重在理解而非模仿,以此思考并因地制宜的做出决策才是最佳的用法。
- 原则
这是必须要记住的内容,但要注意的是原则是一种思想,仅靠死记硬背一点用都没有,必须要真正理解后才能使用。 - 方法
方法在各个不同的开发环境中可能会有所区别,所以仅是一种参考,具体还需要配合相关原则因地制宜的使用,若不知其意却强行装模做样结果只会东施效颦。而且只要真正理解了设计原则,并且对编程语言足够熟悉的话,设计方法是可以自己推导出来的。所以若是特意要去背着玩意我觉得有点吃力不讨好。
目标
设计原则之间是相关的,是可以逐步推导出来的,因为其最终目的都是为实现“高内聚,低耦合”,实现这一目的就要求功能“模块化”。模块化的基本设计思路是“单项依赖,逐层精化”,将其放置到具体的面对对象编程中便引出了名为“设计模式”的软件开发方式。
- 模块化
将程序中的所有功能划分为一个一个相互独立不可分割的功能部件。其中模块内的功能特定又集中,具有着内聚的特性。而模块间则基本独立无关,但也可能存在着少量的依赖关系,这便又拥有着耦合的特性。一个优秀的模块化设计应尽可能做到“模块中高内聚,模块间低耦合”。 - 在模块中高内聚
若你开始使用诸如大段复制粘贴之类的方式编写程序,那你的程序恐怕就没有实现高内聚。内聚代表着强关联性,比如去掉某模块中的一个函数,则可能整个模块都会无法运行,因为其中的每个函数间相互关联相互依赖的。 - 在模块间低耦合
耦合代表着存在依赖关系,如去掉模块A,模块B则无法使用,说明B模块依赖于A模块。若去掉B模块,A模块也无法运行,则代表着A和B存在着双向依赖,这种情况应该避免。利用接口事件等方式将其调整为单向依赖,否则考虑将其合并为同一个模块
允许模块间依赖,但应该尽可能减少这种关系。模块之间应尽可能不相关不依赖,如去掉某模块,程序可能会缺失部分功能但大部分功能应该依然能正常运行。这样不仅程序出错的概率被降低,而且通用性强移植性好,也便于团队协作。 - 平衡内聚和耦合
模块化中的内聚和耦合是相互制衡的关系,模块的高内聚必然会导致模块间低耦合,反之同理。为了让软件开发更加便捷更加健壮,我们当然希望程序能尽可能的做到高内聚,但过于极端的追求会导致模块数量激增,项目变的过于复杂度,开发成本过高从而导致结果也适得其反。所以高内聚究竟要达到什么水平,这是一个权重值,需要根据具体场景进行调整,建议依据哈夫曼压缩原理进行决策。 - 单向依赖与逐层精化
上述有关耦合的描述中已经说明了一些有关单向依赖的问题,无法做到单向依赖会导致功能调整变得复杂且困难,项目的结构最终也会变成难以理解的网状。理想的项目结构应该类似树形拥有明显的层次关系。
从开发者的角度,我们知道一个模块可能要依赖于其他模块才能运行,但这对使用者来说却可能是透明的,因为每个模块都代表一种独立的功能,这就要求它最终要把这种依赖关系转变成封装关系,这也是为什么项目会产生层次性的原因。每一层都是对下层内容的封装,反过来每一层又都是对上层内容的细化,这便是逐层精华的由来。
设计原则
为实现上述目标,设计模式提出了7点基本原则,网上也有说6点的,主要是因为“单一职责原则”和“接口隔离原则”可以理解为同一原则,不过下述还是归类为7点,但不做细致讲解。
根本问题
-
开闭原则
如何以扩展的方式调整功能而不是直接对源代码进行修改?
一个程序需要拥有很好的适应性才能应对各种需求,因此应该具备扩展原有功能的能力,但这种扩展功能的方式不应该是直接修改原代码。
每一次修改代码都可能导致出错,若为一个新功能而修改代码,结果往往是得不偿失导致旧功能因此受牵连出问题,而且这种方式对团队协作也很不友好。
那么如何在不修改原代码的前提下实现功能扩展呢?由此引出了下述原则。
如何解决
-
里氏替换原则
通过实现“接口”来完成功能扩展(此接口非彼接口!)
确保任何基类可以出现的地方,子类一定可以出现,用派生类替换基类时,原功能应不受影响。这样扩展功能时就可以利用继承重写的方式,实现开闭原则。
该条其实不单单是指接口和继承,包括委托事件这些都应该算在类,因为它们有一个共同特点就是都有将算法的一部分参数化的功能。
但为何只提到了继承和接口呢?如果使用的是java很容易理解了,因为java中不存在委托类型,所以到处使用的都只有接口和匿名类。实现细节
-
依赖倒转原则:
通过里氏代换原则的指导,我们要学会一种全新的编程方式,面向接口编程。如果某功能存在扩展的需求,那我们应当将这部分代码接口化,从而隐藏具体细节,让功能依赖于接口而非实现。
因为实现不再被依赖,所以它可以成为一个独立自由的个体,我们可以很方便很安全的进行开发和修改,且不会干扰到原有模块。 -
接口隔离原则
面向接口编程时,在设计上应尽可能保证提供多个相互独立的接口,而非臃肿的一整块,这可以减少耦合性,不然这些接口反而会成为累赘,逼着别人违反单一职责原则。
-
-
开闭原则中的扩展
通过组合多个模块来完成功能扩展
扩展意味着原功能依旧保持独立,这不单单是指代码上的修改,这里其实还隐藏着开闭原则对功能模块化的要求。
-
单一职责原则:
每个模块都应该仅负责少量且专精的功能,这是模块化中对高内聚的基本要求,不多赘述。
-
最少知道原则(迪米特原则):
一个模块应尽可能减少对其他模块的使用,使系统功能相对独立,这是模块化中对低耦合的基本要求,也不多赘述。
-
-
合成复用原则
里氏替换原则中主要讲述了有关面向接口的编程方案,这种方式是利用了继承的特性。继承仍然会产生耦合性,比如子类将无法摆脱父类而存在,再加上一些语言特性和开发框架等影响,实际用起来很可能会出现水土不服的影响。
这时开闭原则对模块化的要求给我们带来了另一种解决方案“组合”,组合是直接通过增设更多的功能模块从而实现功能的扩展,它不受一些继承问题带来的限制,而且产生的耦合性也小的多。其实这也是早期软件开发的常见方式,毕竟一开始没有面向对象编程。
这两种方案都各有优势,各有适合的场景,因地制宜,取长补短,相辅相成便能实现一加一大于二的效果,而这也正是开发软件的正确姿势。
具体例子
Unity框架中,所有碰撞体都继承了Collider类,通过里氏替换原则,子类得以可以自由扩展功能,且Unity还不需要做任何额外工作,一套旧代码就能处理任何新子类。
不同碰撞体间的差异需求导致产生了诸如BoxCollider,SphereCollider等子类,这些子类符合单一职责原则和最少知道原则,仅额外简单的实现了些形状上的差异,所以开发和使用时也不会相互影响。
但一个刚体往往需要的形状是非常复杂的,上述这些碰撞体由于过于简单都无法满足需求,此时Unity便采用组合的方式,将不同碰撞体的功能合并起来聚沙成塔,这样不需要写任何代码就能随意搭配出各种自己想要的碰撞形状来。
如果不采用上述方案,那每一个子类都要单独写处理的代码,每一个新需求又会导致要重新写一个新子类,一两个还好,要是几十几百个,妈耶,累死。结果刚写完,项目经理跑过来说,旧的需求要改一下......
不用想了,这下肯定得有一个人要进医院。
设计方法
累了不写了,我直接复制粘贴,以后再看。
创建型模式(5):工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式(7):适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式(11):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。