高内聚低耦合是设计的目标,但无论你怎么做都无法消除软件模块之间的耦合。所以掌握内聚和耦合的度是设计好坏的关键。
内聚是指模块使用者不可见的逻辑,开闭原则、单一职责、接口隔离都是提高内聚的准则。
耦合是指软件模块之间互相关联的部分。继承、全局变量、硬编码、程序员约定这些都属于高耦合的关键因素。
内聚和耦合发生在软件的各个阶段
重构并不是在某个时间段内对代码进行重新梳理和修改,当我们发现代码中有任何值得重构的地方时我们都应该对其进行重构。重构与修改的规模并无任何关系。所有的重构方法都是极小的修改,例如:
也有较为重度的重构,例如:
什么时候重构取决于程序员什么时候嗅到 ”代码的坏味道“ 这大多数取决于程序员对设计的敏感,以及对修改后收益的估算。针对 ”代码的坏味道“ 的另一种说法叫做《反模式》
反模式经常有以下根源
重构前我们需要先对需要重构的部分编写测试代码,测试代码是非常好的解耦合试金石。这基于一个事实,如果需要单独测试某个模块,则它必须先切断与其他模块的联系。在代码设计和编码阶段,先考虑如何测试可以提升设计的内聚程度。
当你决定要重构某个系统的时候最好要先吃透原系统的代码和控制流程,并使用桩模块构建重构部分的边界。对一些设计问题比较多的模块进行重构过程并非能一步到位,大多数情况下需要几次,甚至几十次的小规模重构才能达到重构的最终目标。
如果一个类的职责过多,相当于把这些职责耦合在一起了。导致的结果可能是一个职责的变化会削弱或者抑制这个类完成其他职责的能力。当变化发生时,设计会遭受到意想不到的破坏。
常见的例子:
MVC中,在Module的部分耦合Control或View部分的逻辑。
如果程序中的一处改动会产生连锁反应,导致一系列相关模块的改动,那么设计就存在僵化性的臭味。正确使用OCP可以使今后进行相似的改动时只需要添加新的代码而不改动已经正常运行的代码。
OCP的两个特征是:
经典的设计模式中,有多个都符合OCP原则。设计的关键在于抽象,通过语义来定义对象和接口是是常用的方法。
典型的违反OCP原则的案例是switch - case 和 if - else 语句的使用。
为避免过度设计,可以在一定程度上接受违反OCP的情况,直到有需求刺激情时再进行重构。一但重构完成,则不必再对正常运行的代码进行重构。
OCP相关的模式有:策略、桥接、模板方法等
在许多方面,OCP都是面向对象设计的核心所在。遵循这个原则可以带来面向对象所生成的巨大好处(灵活性、可扩展性及可维护性)OCP原则适用于频繁变化的部分,对于较为稳定的部分可以适当放宽。所以拒绝滥用OCP和使用OCP同样重要。
子类型必须要能够替换掉他们的基类型。确切的说是对于每个类型S的对象O1,都存在一个类型T的对象的O2,使得在所有针对O1编写的程序P中,用O1替换O2后,程序P的功能不变(注意,不是行为),则S是T的子类型(并不要求继承)。
从定义上来看,等量代换的实质是指基于抽象的语义描述类型T或S,从而使其接口保持一致。一般情况下违反LSP原则的同时也会违反OCP原则。较为常见的情况是通过if-else判断类型值来决定后续的调用方法。
LSP的前提是从使用者的角度进行合理假设,其难点是如何通过抽象找到正确的合理假设。此外需要重申的一点,LSP 的对象关系并非 IS-A的关系,LSP注重的是行为方式的一致性,而非对象的逻辑关系。
明确合理假设的一个有效方法是在做抽象前先规定该对象的契约(隐喻,例如对于Shape对象,其行为是Paint),并在编写代码的过程中通过该契约获悉可以依赖的行为方式。
契约有两个关键点:
也就是说,对象使用前后,并不会改变代码的语义。从而,使我们必须假设,任何基类的派生类,只能使用约束更弱的前置条件(换句话说,派生类必须接受基类所能接受的一切)。
一个例子:
之前在做游戏的时候,有一个现成的网络库可以用。但当时评估下来,我们认为这个网络库在性能和可扩展性上有很严重的问题。所以我使用Adapter模式对其做了一层封装。后来我们自己重写了网络库,并实现了另一个Adapter。结果新的网络库工作的非常好,而我们并未对上层代码进行任何修改。
LSP的重要性:OCP 是面向对象中很多问题的核心,而LSP 是使OCP成为可能的主要原则之一。
依赖倒置 (DIP)的核心思想是好莱坞原则 ”不要打电话给我,需要的时候我们会打电话给你。“ 这句话要表达的更深层的含义是关于上下层之间的调用关系,底层不应该与上层之间有任何通讯。
依赖倒置的关键在于:
其中第一条解释了为什么该原则要使用”倒置“这个词。还是LSP中举的网络模块的例子,当上层模块拥有接口定义时,我们可以很容易的使用LSP原则来改变底层模块的行为。这基于一个实际问题,当底层模块有改动的时候,高层模块是否也需要跟着改动?如果高层模块独立于底层模块,那么高层模块就可以很容易被复用。该原则是框架设计的核心原则。
所有结构良好的面向对象架构都具有清晰的层次定义,每个层次通过一个定义好的、受控的接口向外提供一组内聚的服务。
使用DIP原则的关键是找出应用背后的抽象,是那些不随具体细节的改变而改变的语义。一般情况下,我们会想到使用多态来实现语义抽象。但例如C++的模板,以及Java的反射也可以实现DIP。
我们可以使用这样一条评判标准,如果程序的依赖关系是倒置的,他就是面向对象的设计。否则,他就是面向过程的设计。如果依赖不是倒置的,那么依赖的细节变更就会影响到整个程序运行的逻辑,所以它是构建富有弹性的代码的一条重要准则。
如果类的接口不是内聚的就表示该接口违反了ISP原则。例如经常在代码中的“胖”接口,其可以分解成多组方法,“胖”接口的问题在于当某个客户要求对其改动时,它会影响该类的其他部分。如果有多个客户使用该类,则其他客户的代码也需要随之改动;同时,“胖”接口带来的问题是当修改其中某些模块的时候可能会影响其它功能。对于集成过多功能的类我们有两个方法将接口进行拆分:
在C++中,当某个类(A)中需要使用其他类(B)时需要包含类B的头文件,此时类B会被强制引用到使用者的文件中。这是一个很大的麻烦,如果类B是一个库的头文件,那么在使用类A的工程中也需要包含类B的头文件路径。而这正是ISP原则告诫我们需要避免的。
设计模式是从软件工程的经验出发,从而总结出来的常用的结构,我们称之为模式。
常用模式简介
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节,对象的创建由相关的工厂来完成。就像我们去商场购买商品时,不需要知道商品是怎么生产出来一样,因为它们由专门的厂商生产。
创建型
创建型模式分为以下几种。
1、单例(Singleton)模式
某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
2、原型(Prototype)模式
将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
3、工厂方法(Factory Method)模式
定义一个用于创建产品的接口,由子类决定生产什么产品。
4、抽象工厂(Abstract Factory)模式
提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
5、建造者(Builder)模式
将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
建造者(Builder)模式的定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
该模式的主要优点如下:
其缺点如下:
6、代理(Proxy)模式
为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
7、适配器(Adapter)模式
适配器模式(Adapter)的定义如下:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
该模式的主要优点如下。
其缺点是:
8、桥接(Bridge)模式
桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。这里将桥接模式的优缺点总结如下。
桥接(Bridge)模式的优点是:
缺点是:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。
9、装饰(Decorator)模式
动态的给对象增加一些职责,即增加其额外的功能。
10、外观(Facade)模式
外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
在日常编码工作中,我们都在有意无意的大量使用外观模式。只要是高层模块需要调度多个子系统(2个以上的类对象),我们都会自觉地创建一个新的类封装这些子系统,提供精简的接口,让高层模块可以更加容易地间接调用这些子系统的功能。尤其是现阶段各种第三方SDK、开源类库,很大概率都会使用外观模式。
外观(Facade)模式是“迪米特法则”的典型应用,它有以下主要优点。
外观(Facade)模式的主要缺点如下。
11、享元(Flyweight)模式
享元(Flyweight)模式的定义:运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
享元模式的主要优点是:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
其主要缺点是:
游戏中的运用,游戏中道具数量众多,为了节省内存,通常将不会变化的数据封装在一个类中,并使用另一个类来引用。对外暴露一个统一的道具接口。
12、组合(Composite)模式
将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
行为型
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
13、模板方法(TemplateMethod)模式
模板方法(Template Method)模式的定义如下:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。
比较常见的是排序算法,可以通过传递一个比较函数来决定排序是升序还是降序,这是模板模式的一种简化。当算法中有多个需要通过回调的返回结果控制程序流程或数据时会自然的想到使用模板方法。
该模式的主要优点如下。
该模式的主要缺点如下。
模板方法可以退化成一组回调函数,或者合并组合模式,这样可以增加灵活性。例如:
游戏中的技能释放,需要逐次调用选取释放目标,释放技能,计算伤害,计算技能效果这样的过程。此时就可以使用模板方法来实现一个技能释放的模板方法,通过调用技能对象提供的算法来决定
14、策略(Strategy)模式
策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
策略模式的主要优点如下。
其主要缺点如下。
策略模式最主要的是通过配置来改变程序行为,很多开发在描述策略模式时往往忽略配置的重要性。通常情况下策略模式会和工厂方法配合使用。账号系统中,短信发送就是典型的策略模式。
15、命令(Command)模式
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
命令模式一方面会解耦调用时间,一方面会解耦调用职责。一般情况下,当所设计的模块有明显的层次划分,且命令的执行和命令的调用在不同的层时,可以通过命令模式将命令通过队列进行传递。消息其实是命令模式的一种退化表现。
16、职责链(Chain of Responsibility)模式
责任链(Chain of Responsibility)模式的定义:为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
注意:责任链模式也叫职责链模式。
在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。
责任链模式是一种对象行为型模式,其主要优点如下。
其主要缺点如下。
典型的应用是窗口消息处理,每一个窗口都会处理其关心的消息。但职责链模式大多数情况下都会面临消息传递路径的不确定性,比如,消息是否处理后还要继续向下传递;消息处理后是否需要将加工过的数据再返给其上层处理。
另一个应用是游戏中的行为树,这也是比较典型的职责链模型。
职责链通常会与组合模式、观察者模式一起使用。
17、状态(State)模式
状态(State)模式的定义:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
状态模式是一种对象行为型模式,其主要优点如下。
状态模式的主要缺点如下。
18、观察者(Observer)模式
观察者(Observer)模式的定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。
观察者模式是一种对象行为型模式,其主要优点如下。
它的主要缺点如下。
19、中介者(Mediator)模式
中介者(Mediator)模式的定义:定义一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。中介者模式又叫调停模式,它是迪米特法则的典型应用。
中介者模式是一种对象行为型模式,其主要优点如下。
其主要缺点是:中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。
中介模式的关键是消息(调用)的翻译,它隐藏了两个沟通对象之间的实现细节,所以可以很容易的将任何一个沟通对象进行替换。从而使两个沟通对象之间解耦。
20、迭代器(Iterator)模式
提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
21、访问者(Visitor)模式
在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
22、备忘录(Memento)模式
在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
UNDO,游戏存档,取档,其关键是对数据的保存和管理。
23、解释器(Interpreter)模式
解释器(Interpreter)模式的定义:给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。
这里提到的文法和句子的概念同编译原理中的描述相同,“文法”指语言的语法规则,而“句子”是语言集中的元素。例如,汉语中的句子有很多,“我是中国人”是其中的一个句子,可以用一棵语法树来直观地描述语言中的句子。
解释器模式是一种类行为型模式,其主要优点如下。
解释器模式的主要缺点如下。
解释器的关键在于读取结构化数据,和根据数据执行逻辑这两点。代码中读取配置文件并不能算作解释器模式。
策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
策略模式的主要优点如下。
其主要缺点如下。
原文:https://www.cnblogs.com/albertclass/p/14182173.html