学习架构的时候复习一下设计模式
面向对象设计原则
单一职责原则
单一职责原则:一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作。
单一职责原则是实现高内聚、低耦合的指导方针,需要设计人员发现类的不同职责并将其分离。
例子
比如一个类即创建了数据库连接,又查询客户信息,还生成和展示图表。它的职责就有点多了。
重构后将各个功能拆分成不同的类,然后加以组合:
其实就是,把一个大的功能模块划分为一个个小的功能模块,然后以组合的方式合并这些功能
开闭原则
开闭原则是面向对象的可复用设计的第一块基石,它是最重要的面向对象设计原则。
开闭原则:一个软件实体,应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
抽象化是开闭原则的关键。可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成拓展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
例子
下面这个例子主要是通过里氏代换原则(抽象类可以转换为实体类)和依赖倒转原则(即用抽象类来代替实体类)来实现开闭原则的。开闭原则是目的,里氏代换和依赖倒转是手段。
下面调用两个 Chart 的展示方法,如果是硬编码就会在 ChartDisplay 中写明要调用的类型,通过 if.else 判断:
1 | ...... |
现在可以将这个方法抽象出来,通过调用抽象方法来解决:
其实就是,将多个类中类似的功能抽取出来,作为一个抽象类,以后所有有这个功能的就继承这个抽象类,要调用的时候就调用这个抽象类的方法。
接口隔离原则
接口隔离原则:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
每一个接口应该承担一种相对独立的角色,不干不该干的事。接口可以有两种不同的定义:
- 当把“接口”理解成一个类型所提供的所有方法特征的集合的时候,这就是一种逻辑上的概念,接口的划分将直接带来类型的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定的一个接口,此时,这个原则可以叫做“角色隔离原则”。
- 如果把“接口”理解成狭义的特定语言的接口,那么ISP表达的意思是指接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的单独的接口,而不要提供大的总接口。
在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。
合成复用原则
合成复用原则:尽量使用对象组合,而不是继承来达到复用的目的。
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类;从基类继承而来的实现是静态的(组合就可以通过抽象类的方式,在运行时随时变换具体的实例),不可能在运行时发生改变,没有足够的灵活性。
例子
比如你想自定义一个 button,那么你可以通过继承实现,重写 initWithFrame:
方法。也可以通过为 UIButton
添加一个分类 UIButton+Custom
,在其中添加自己的自定义方法。当然,按照本原则,尽量使用组合的方式。
组合就是在原来的基础上添加新的方法,可以直接添加(如 iOS 中的分类),也可以通过添加一个属性,由这个属性来执行要实现的方法。
继承主要是为了代码复用,是在原来方法的基础上拓展原方法。继承的目的并不是为了给子类提供一个方法标准,因为定义方法标准最好的方式是协议。所以不要单纯为了提供定义方法来继承。
也就是说,假如基类中有些方法是空方法,是要子类实现的,那么就抽出来作为协议。你可以将该方法直接声明在基类的 interface 里,然后不在 implementation 中实现这个方法,或者直接在这个实现方法中抛出异常(两者是等效的,执行到的时候如果子类没有重写该方法都会异常),这样所有子类都可以调用这个方法了。然后让子类实现一个将该方法声明为 required 的协议。为什么要再弄一个 required 的协议呢?因为这样能提醒开发者这一类型的子类必须要实现该方法,有助于代码规范。
何时用继承何时用组合没有绝对的定义。只要记住不要滥用继承。想要用继承的时候先问自己,能不能用组合?
分类和 AOP 都是组合的实现思路。不过使用 AOP 时要注意如果某些子类不接受 AOP 需要如何特殊化处理(比如你对 UIViewController 的 viewDidLoad 方法做 AOP,但是一些系统的 UIViewController 就不适用你的 AOP 方法,那么你要如何把系统的 UIViewController 派生的子类排除?)。这个时候你就要评估使用 AOP 的组合和使用继承在代码实现上的难易程度了
迪米特法则
迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用。
应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
例子
下面点击 button 后,会调用各种控件产生变化,这样 button 就要和各种空间耦合。
可以通过加入中间件,解耦 button 和其他控件:
其实就是,通过中间层,减少类与类的耦合
原则总结
设计原则总结起来两点:”少”,”解耦”:
少
- 内部功能少 => 单一职责
- 外部暴露少 => 接口隔离
- 相互关系少 => 迪米特
解耦
- 类与内部属性解耦 => 开闭
- 类与拓展方法解耦 => 组合复用
- 类与类解耦 => 迪米特
设计模式的意义
一个方法被多个类调用,一般的做法是将这个方法抽出来作为一个方法类供其他类调用,这种做法没有使用设计模式,是一种合成复用;一个类要根据不同情况调用不同方法或者拓展方法的时候,我们可以选择将方法抽出为类,直接调用方法基类提供的方法,切换方法类的实例来切换方法,也可以选择通过一个中间类的基类调用方法类的方法,通过切换中间类的实例来切换方法。这就是一种设计模式的选择。
设计模式作用在于解耦。但是你可能会问,我们虽然是要么调用方法基类的方法,要么是调用中间类基类的方法,但是最后还是要实例化究竟是哪个方法类和哪个中间类,难道不还是要耦合具体的方法类或者中间类吗?是的,有些情况下是,但是有些时候我们还可以将方法类或者中间类的实例传给调用者,或者使用放射的方式动态创建调用者,这两种情况下,调用者就不需要引入具体的类了。这样就实现了解耦。
所以,当你发现你的某个类需要调用不同方法或者拓展方法的时候,就可以考虑使用设计模式了
六种创建型模式
创建型模式主要用于描述如何创建对象
为什么要用工厂?
这一节不是一个设计模式,但是有必要探讨一下,为什么要使用工厂?工厂目的是为了将对象的创建和使用分离。那么这样有什么好处?
对于创建类与产品类之间来说:工厂类的引入将减少耦合,降低因为产品类改变所造成的维护工作量。如果产品类需要添加或者移除的子属性,或者要修改产品类的接口,如果和业务数据数据无关,那么只要修改工厂类的代码就行了,不需要改动创建类中的代码。
对于创建类来说:防止用来实例化一个类的数据和代码在多个类中到处都是。产品类的某些参数可能不能直接使用,而需要转化(比如日期字符串的转化),再比如产品类的构建的参数可能需要从文件读取。这些写在创建类中就会让初始化的代码到处都是。
对于产品类来说:防止产品类职责过重,耦合其他类。对于2,获取你认为可以将参数的转化放在产品的初始化方法中,但是这样容易产生耦合,不便移植。比如我(产品类)本来就只要一个字符串,你非得让我从某个路径读文件;本来我只要一个 UIView,你非得让我自己引入并创建一个 CustomView 的实例。
归根到底,工厂还是在两个类之间起到一个中间人的作用,只不过它是专门用来创建实例的。所有的中间人都可以分为三部分来分析:调用者,被调用者,两者之间。
工厂模式适用于创建产品类的时候,需要做很多初始化操作。
iOS 中的类簇就是工厂模式的体现。比如 NSString NSNumber NSArray 的创建都隐藏了其实际的子类信息。
简单工厂模式
难度:2 频率:3
简单工厂模式:定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。
如果我们要根据参数 type 的不同,创建不同类型的产品类,那么我们就要在调用者类中添加很多的 if…else… 判断。如果其他调用者也要创建这种产品类,也会添加很多 if…else…,那么就不如直接将这些判断提取出来,作为一个简单工厂类。
这里是因为有很多产品类才创建使用的简单工厂模式,复用 if…else…。如果只有一个产品类呢?如果只是单纯的创建,确实没什么代码可以复用的,直接创建就行了。但是如果对产品类有一些设置,这是共性的,可以复用,所以还是可以创建工厂,也就是下面要说的工程方法模式。
例子
下面工厂 ChartFactory
通过 getChart:
方法创建不同的 Chart
对象。其内部是通过 if…else 实现的。HistogramChart
,LineChart
,PieChart
分别实现了接口 Chart
的 display()
方法:
这里由于所有 Chart 都要实现一个 display 方法供调用者创建好对象后调用,所以要设置一个接口类型。
其实就是,在抽象类中提供一个静态方法,通过输入参数的不同,创建不同的具体子类实例。
工厂方法模式
难度:2 频率:5
工厂方法模式:定义一个抽象工厂类,让其子类决定将哪一个产品类实例化。工厂方法模式让一个产品类的实例化延迟到其工厂子类中。
使用简单工厂面临的问题是所有的初始化方法都在一个工厂类中,通过 if…else…判断,这样职责过重,并且不易拓展。所以使用工厂方法模式。
工厂模式中不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。工厂方法通过反射的方式,动态的创建具体工厂实例,代替简单工厂中的 if…else,满足了开闭原则。如果不适用反射的方式,那么就得先创建工厂实例,然后在调用工厂创建产品的方法了。
那为什么不直接在简单工厂里使用反射动态地创建具体产品实例呢?反射生成对象只能适用一些最简单的情况,如果对象的创建过程比较复杂,例如要调用有参构造函数、创建之前要配置一些不同的环境等等,需要将这些代码封装到工厂中。
例子
提供了接口 LoggerFactory
和接口 Logger
。FileLoggerFactory
和 DatabaseLoggerFactory
实现了接口 LoggerFactory
的 createLogger()
方法;FileLogger
和 DatabaseLogger
实现了接口 Logger
的 writeLog()
方法。在实现的时候可以通过反射,动态地创建具体工厂类 FileLoggerFactory
和 DatabaseLoggerFactory
。然后调用各自实现的接口方法 createLogger:
创建具体的产品类
iOS 中 [NSNumber numberWithInt:1]
这种非 alloc
创建的就是工厂方法,或则准确的说这种不通过 Factory 类的工厂叫做类工厂方法。工厂方法直接将需要的变量作为参数传入。工厂方法的名称不要用图示的 createXXX
,应该类似于 NSNumber
这种 OC 特色的方法命名方式。
由于要解耦,所以将 LoggerFactory
和 Logger
都设置为接口类型,然后子类分别实现他们的方法。
命名最好以 XxxFactory
和 createXxxWithXxx:
命名。
其实就是,为每一个具体的产品类都创建一个具体的工厂类,来包裹一些复杂的创建过程。抽象类通过反射方式创建具体的工厂,再由工厂实例创建产品实例。
抽象工厂模式
难度:4 频率:5
抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。
先要了解两个概念:
- 产品等级结构:产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
- 产品族:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中,海尔电视机、海尔电冰箱构成了一个产品族。
抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建。
对于增加新的产品族,抽象工厂模式很好地支持了“开闭原则”,只需要增加具体产品并对应增加一个新的具体工厂,对已有代码无须做任何修改;对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。
例子
下面是一个定制不同皮肤的例子。由 SpringSkinFactory
和 SummerSkinFactory
实现了接口 SkinFactory
的三个创建对象的方法。同样的,通过反射,动态的创建工厂实例。调用其实现的创建方法获取产品实例:
其实就是,抽象工厂模式和工厂方法模式没啥大的差别,只不过把几个不同类别(即不同产品等级结构)的东西的创建方法放在一起,减少了工厂类的数量。
单例模式
难度:1 频率:4
单例模式:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
缺点:单例类没有抽象层,不易于扩展。同时单例类职责过重,即充当了工厂角色,又充当了产品角色
结构
单例类中有一个对象属性,并且声明了一个静态方法。这个静态方法被调用时,会先判断对象是否为空,如果为空就创建一个,然后返回:
原型模式
难度:3 频率:3
原型模式:使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。
缺点:要为每一个类提供一个克隆方法,当对类进行改造时,需要修改源代码,违反了开闭原则。另外,嵌套多层时,实现代码会较为复杂。
结构
每个要 clone 的类都继承于 Prototype 抽象类,然后实现其 clone 方法,注意这里的方法是实例方法:
调用的代码类似:
1 | Prototype obj1 = new ConcretePrototype(); |
其实就类似于 iOS 中的实现 NSCopy 协议,完成 copyWithZone 方法,然后调用 copy。
建造者模式
难度:4 频率:2
建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
建造者模式其实是为了给一个特定对象在不同情况下赋不同值的。比如你要初始化一个对象,这个对象会根据你给出的 type 不同,将属性初始化为不同的值。你可以用 if…else 判断不同属性赋不同值,但是这样显然违背了开闭原则。所以建造者模式就需要你为每一种 type 创建一个 builder 类,在 builder 中提供对对象的属性进行赋值的方法。然后,创建的时候传入不同的 builder(你也可以通过反射的方式,只传入字符串),调用这些赋值方法,以此达到分拆 if…else 的目的。
结构
抽象类 Builder
(因为 Builder
中含有创建的对象属性,所以不能设置为接口)提供了创建对象的方法以及设置这个对象中的属性的各种方法。会有多个 ConcreteBuilder
实现了抽象 Builder
的方法。每个 ConcreteBuilder
中设置属性方法所设置的属性都是不同的。Director
中会保存一个 Builder
的实例,可以通过反射创建不同的 Builder
设置给 Director
。同时,Director
提供了一个 construct
方法,用来调用 Builder
中设置属性的方法,我们可以自由控制是否需要设置某一属性(即控制是否调用 builder
中设置属性的方法)。最后通过 getResult()
返回设置好的对象:
建造者模式与抽象工厂模式有点相似,但是建造者模式返回一个完整的复杂产品,而抽象工厂模式返回一系列相关的产品;在抽象工厂模式中,客户端通过选择具体工厂来生成所需对象,而在建造者模式中,客户端通过指定具体建造者类型并指导指挥者如何去生成对象,侧重于一步步构造一个复杂对象,然后将结果返回。如果将抽象工厂模式看成一个汽车配件生产厂,生成不同类型的汽车配件,那么建造者模式就是一个汽车组装厂,通过对配件进行组装返回一辆完整的汽车。
例子
下面例子中,ActorController
充当指挥者(Director
),ActorBuilder
充当抽象建造者,HeroBuilder
、AngelBuilder
和 DevilBuilder
充当具体建造者,Actor
充当复杂产品:
ActorController
中预先设置一个 builder
,然后通过 construct
方法调用 builder
的各种设置方法,达到设置 Actor
的目的。具体的流程上面已经叙述过了。
类比工厂方法模式,builder
其实和 factory
基本一致,两个都是初始化属性,builder
中有更统一的初始化属性的方法。并且一个 builder/factory
对应一个产品类。但是为什么这里专门要有一个指挥者(Director
)呢?因为有了指挥者就可以控制 builder
中的方法到底哪些要执行哪些不要执行了,可以装门创建一个 Director
对象,以组合的方式作为d调用者的一个属性,随时可以替换。工厂方法模式的调用者话就不关心要设置哪些属性了,所以工厂方法模式的调用者只调用 factory
的 create 方法,具体如何设置属性的,调用者无从知晓。
这里 Actor
没有继承任何类,ActorBuilder
是一个抽象类,其中保存了 Actor
的实例。其实设计模式中没有硬性规定这个设计模式中一定要包含某个接口,或者某个基类,完全是按照实际需求来的,只需要提供通用方法的就设置为接口,需要提供通用属性的,就设置为基类,什么也不需要的就是普通的类。比如本例,我们就只需要生成这一个 Actor
,所以就不需要搞任何继承和组合;ActorBuilder
必须保存一个 Actor
实例属性(因为各种build 方法都要调用它的方法),所以就必须要设置为抽象类,不能设置为接口。
命名最好以 XxxBuilder
和 buildXxxWithXxx:
方式命名。
建造者模式的抽象程度比工厂方法模式的高,所以同样是初始化属性的操作,建造者模式抽象出了各种 build 方法,而工厂方法模式则全部藏在工厂的 create 方法中。
七种结构型模式
结构型模式主要用于描述类与类之间的关系,几个类之间如何解耦的
适配器模式
适配器模式:将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。在实际开发中,对象适配器的使用频率更高.
结构
调用目标抽象类的某个方法的时候,通过适配器,转而调用了适配者类的某个方法,完成适配。
例子
对象适配器
对象适配器的适配器(adapter)和适配者(adaptee)是关联关系。下面的 OperationAdapter
是实现了接口 ScoreOperation
的 sort
以及 search
方法。可以通过反射的方式自由更改接口 ScoreOperation
的实现类以满足开闭原则。在适配器中,真正的实现对象 QuickSort
以及 BinarySearch
是作为其属性 sortObj
,searchObj
存在的,所以称为关联关系。在调用 sort
,search
方法后,将调用真正的实现方法:
类适配器
类适配器的适配器和适配者是继承关系, Adapter
是继承于 Adaptee
的,实现了 specificRequest()
方法.同时, Adapter
也实现了接口 Target
的 request()
方法,在 request()
方法中调用实现方法 specificRequest()
。注意下图和上面结构图的区别:
Target
是一个接口,提供了一个通用的方法供被适配对象调用,由具体的 Adapter
继承实现。Adaptee
由于是具体的适配类,各个类不同,所以不提供通用的方法。
命名最好以 XxxAdapter
和 requestXxxWithXxx:
命名。
其实大部分设计模式的结构都和上面拿到很像,一个类通过一个中间人调用另一个类的方法,我们可以中间人调用另一个类方法前后分别调用一些自己的方法。比如外观模式,代理模式,命令模式等
区分这些设计模式的要点在于区分目的。
适配器模式的目的是适配,举个简单的例子,比如我只能提供 CFloat 的参数,但是被调用者必须要 NSNumber,那么我就不能直接调用被调用者。所以这个中间类就是用来适配的。
代理模式的目的是代理,就是我就不想用原来的被调用对象,我想用一个新的中间类代替它,然后做一些额外的操作。所以这个中间类是用来代理的,虽然可能也做了适配的工作,但是我目的就是想用它代理原来的类。
外观模式的目的是改变外观。就是原来我既要调用这个类的接口,又要调用那个类的接口,太麻烦了,所以我创建了一个中间类,来统一所有的接口。所有的调用都走这个中间类,虽然可能它也做了适配和代理的工作,但是我的目的就是要统一所有接口。
命令模式的目的是包装方法。这和上面的解构型设计模式不太一样。命令模式是行为型设计模式,讲究的是对于方法的执行,结构型讲究的是类与类之间的关系。我不是让他适配某一个类,也不是让它代理某一个类,更不是想让它替换某一个类的接口。就是想把某个类的某个方法包装一下。
桥接模式
难度:3 频率:3
桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化。
如果软件系统中某个类存在两个或者多个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。
缺点:要能正确识别系统中两个独立变化的维度。如果不是独立变化的,那还是不可避免的要通过 if…else 判断。
结构
如图,抽象类 Abstraction
为一个维度,它要求它的子类都实现 operation()
方法。接口 Implementor
是另一个维度,它要求实现它接口的类都需要实现 operationImpl
方法。在抽象类 Abstraction
中保留有接口 Implementor
的具体实现属性 impl
。两者通过组合的方式实现了同一,能够实现两个维度需要的方法 operation()
和 operationImpl()
。
仍然可以通过反射的方式获得抽象类的实现,和接口的实现。增加新的抽象类或接口实现就可以拓展系统,符合开闭原则。
例子
比如要实现一个图像浏览系统,要能够显示不同格式的图片文件,又要兼容不同的平台。如果不用桥接模式,会产生如下的继承树。因为有格式和平台两个维度,所以继承树有两层:
但是使用桥接模式,可以将平台维度作为接口独立出来。以图片维度作为主维度,设置抽象类 Image
,其中除了自己维度需要的方法 parseFile()
外,还保存了平台维度 ImageImp
的实例 imp
。可以通过 imp.doPaint()
调用平台维度的方法。
一个维度是主要维度,涉及属性,所以涉及为抽象类,另一个维度不涉及属性,只涉及一些方法,所以涉及为接口。这是组合的一个典型运用,没有涉及到中间人。
没有固定的命名方式
什么是维度?是要做相似处理(有着相同的方法名,但方法实现方式不同)的抽象
其实就是,将主要维度作为抽象类用于继承。次要维度分离为接口。最终还是通过组合的方式,代替继承。
组合模式
难度:3 频率:4
组合模式:组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式。
组合模式为处理树形结构提供了一种较为完美的解决方案,它描述了如何将容器和叶子进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器和叶子
组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理.
结构
抽象类 Component
定义了一系列方法,operation()
方法用来做实际业务处理,add()
和 remove()
用来添加和删除对象,这个对象可以是叶子也可以是容器,getChild()
用来返回叶子或者容器。在容器的 opeartion()
方法中,会调用其所有子对象的 operation()
方法。
Lead
和 Composite
继承于同一个超类 Component
,Component
中的 add
和 remove
以及 getChild
在 Leaf
中国不实现,只实现 operation
。
例子
抽象类 AbstractFile
定义了多个方法,其中 killVirus()
就是上面的 operation()
方法。ImageFile
,VideoFile
,TextFile
就是叶子对象。Folder
就是容器对象,容器对象里可能还有很多图像视图文本文件。
没有固定的命名方式,不过基类要定义添加、删除、查看子节点以及真正的执行方法的命名。
组合模式叶子和容器的关系;建造者模式是一个维度和另一个维度的关系。组合模式中叶子和容器其实是一类的,他们都继承于同一个抽象类;建造者模式中两个维度是不同的,继承于不同的抽象类或者接口。
组合模式不是我们通常讲的组合。通常将的组合是指把一个对象放在另一个对象中作为其属性。而组合模式则是指把一堆类似的叶子对象放在一起作为一个容器。
装饰模式
难度:3 频率:3
装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。
如果我们不适用装饰模式,那么对一个功能添加新的功能就要继承成为一个新类:
结构
如图所示,component
是一个抽象类,也可以是一个接口,定义了一个 operation()
方法。concreteComponent
是这个抽象类或接口的实现,也是被装饰对象。现在 Decorator
是一个抽象类,其中含有一个 concreteComponent
的具体实例 component
,在其 operation()
方法中,每次都会调用这个 component
的方法。会有许多具体的实现类 ConcreteDecorateX
实现 Decorator
,它们会在自身 operation()
方法中调用其中保存的 component
的方法,并且加上自己的实现。
调用的话,需要先创建一个 ConcreteComponent
,然后把它设置给装饰者的 Component
,然后调用装饰者中的处理方法。如果有多个装饰者,可以将某个装饰者作为另一个装饰者的 Component
设置进去。
缺点:会产生多个小对象。更容易出错,且排错困难。
例子
上面的那个例子的正确打开方式是,在装饰类 ComponentDecorator
的子类中实现方法 display()
的拓展。调用方式如下:
1 | Component component,componentSB,componentBB; //全部使用抽象构件定义 |
装饰模式的装饰对象和被装饰对象都继承于一个基类,基类中提供的方法就是将被装饰的方法。创建装饰者对象的时候要将被装饰者(被装饰着可以是最根本的 Component
也可以是其他的 Director
)作为参数传入,装饰者内部会将其作为一个属性保存。外部直接调用装饰者的方法,装饰者在进行方法拓展后会调用被装饰者的相应方法。
命名最好以 XxxDirector
的方式,方法没有特别的命名,与被装饰对象相关。
外观模式
难度:1 频率:5
外观模式(Facade):为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
外观模式中,一个子系统的外部与其内部的通信通过一个统一的外观类进行,外观类将客户类与子系统的内部复杂性分隔开,使得客户类只需要与外观角色打交道,而不需要与子系统内部的很多对象打交道。
外观模式的主要目的在于降低系统的复杂程度,在面向对象软件系统中,类与类之间的关系越多,不能表示系统设计得越好,反而表示系统中类之间的耦合度太大,这样的系统在维护和修改时都缺乏灵活性,因为一个类的改动会导致多个类发生变化,而外观模式的引入在很大程度上降低了类与类之间的耦合关系。
结构
Client
不和 SubSystem
直接接触,而是通过一个中间层 Facade
交互。
在标准的外观模式结构图中,如果需要增加、删除或更换与外观类交互的子系统类,必须修改外观类或客户端的源代码,这将违背开闭原则,因此可以通过引入抽象外观类来对系统进行改进,在一定程度上可以解决该问题。在引入抽象外观类之后,客户端可以针对抽象外观类进行编程,对于新的业务需求,不需要修改原有外观类,而对应增加一个新的具体外观类,由新的具体外观类来关联新的子系统对象,同时通过修改配置文件来达到不修改任何源代码并更换外观类的目的。
例子
上面的例子中提供了两个子系统类,它们的 cipher
是不同的。Client
只要调用 FileEncrypt()
即可,不需要再和 FileReader
,FileWriter
,CipherMachine
耦合了。
命名最好为 XxxFacade
,方法没有特别的命名,与被调用方法相关
其实就是原本一个方法中要调用好几个类的好几个方法,现在就把这个方法拿出去,作为外观类的一个方法了。
代理模式
难度:3 频率:4
代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。
在代理模式中引入了一个新的代理对象,代理对象在客户端对象和目标对象之间起到中介的作用,它去掉客户不能看到的内容和服务或者增添客户需要的额外的新服务。
结构
抽象主题角色 Subject
中有一个方法 request()
,代理主题角色 Proxy
和真实主题角色 RealSubject
都实现了其方法。Proxy
会自己创建 RealSubject
的实例,其 request()
方法中,不仅调用 RealSubject
的相应方法,还在其前后各加上自己的方法 PreRequest()
以及 PostRequest()
。可以通过反射动态替换代理主题类名。
命名最好为 XxxProxy
,方法没有特别的命名,与被代理对象相关。
装饰器模式和代理模式和外观模式的区别
装饰器模式关注于在一个对象上动态的添加方法,当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。装饰者能够在运行时递归地被构造。
然而代理模式关注于控制对对象的访问。对使用者来说,代理类就是你要用的那个对象。因此,当使用代理模式的时候,我们常常在一个代理类中自己创建一个对象的实例。代理和真实对象之间的的关系通常在编译时就已经确定了
外观模式用来封装子系统的方法,隐藏子系统间的通信和依赖关系。其实外观是使用最广的,是个方法都能抽出来做成外观模式,只不过一般接口没有复用需求的话没必要罢了。
十一个行为型模式
行为型模式主要用于描述方法是如何执行的
职责链模式
难度:3 频率:2
职责链模式:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
我们经常会写出很长的 if…else 来分别处理不同的情况。例如
1 | if (request.getAmount() < 50000) { |
这样各个方法都几种在一个类中,违反了单一职责原则。并且有 if…else 的地方基本都是违反开闭原则的。我们需要使用职责链模式修改。
结构
定义了一个抽象处理者handler
,它有一个 successor
属性,用来标识谁是响应链的下一级。 handler
还定义了一个抽象处理方法 handleRequest()
,每一个具体处理者 concreteHandler
继承该方法都会先判断自己能否处理,如果能就走自己的实现逻辑,如果不能就调用 successor.handleRequest()
。
职责链模式并不创建职责链,职责链的创建工作必须由系统的其他部分来完成,一般是在使用该职责链的客户端中创建职责链。职责链模式降低了请求的发送端和接收端之间的耦合,使多个对象都有机会处理这个请求。
例子
上图中 Approver
是抽象处理者,successor
的类型是自己本身,用来标记处理链中的下一级处理者。processRequest()
方法用来处理具体的事件,接收一个 PurchaseRequest
对象,这是一个具体要处理的事件。然后就是各个具体处理者。他们会在初始化的时候设置好处理层级。
所有责任链的对象都继承于统一的基类,因为要继承在基类中提供的 successor
。
命名最好在基类中将属性定义为 successor
或者 next
责任链模式用在同类型的串行处理,比如网络请求,就可以提供一个 successor,保存下一级请求。再比如本例,各个处理者其实都是同级的,他们相应的处理方法都会执行到。
命令模式
难度:3 频率:4
命令模式:将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式别名为动作(Action)模式或事务(Transaction)模式。
命令模式将请求发送者和执行者解耦。如果没有命令模式,那么请求发送者和执行者是耦合在一起的。那么就像开关一样,一个开关只能控制一个电器,不能更换电器,且增加电器的时候必须要增加开关。
命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。
命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。
结构
调用者 Invoker
中有一个抽象命令类 Command
的实例。Invoker
不直接调用接受者 Receiver
的方法,而是在这个 Command
的 execute()
方法里调用。可以通过反射的方式动态创建 ConcreteCommand
实例,来决定 Invoker
最终调用的是那个 Receiver
,完成了两者间的解耦。
例子
这个例子的场景是控制某一个按钮 FunctionButton
的功能。FunctionButton
并不直接控制究竟是执行 WindowHandler
还是 HelpHandler
。它内部存有一个 command
,这个 command
根据反射决定究竟是 MinimizeCommand
还是 HelpCommand
实例。最后调用其实现的 execute()
方法。
Command
类中可以添加自己的属性。你可以在 execute
方法前后添加自己的方法,如果调用者要监控命令的执行情况,可以在 command
中设置一个 delegate,提供 command:didRequestXXXWithXXX:
以及 command:didFinishXXX:
方法。
命名最好为 XxxCommand
以及 execute:
的形式,代理命名为 XxxCommandDelegate
,代理方法命名为 command:didRequestXXXWithXXX:
以及 command:didFinishXXX:
其实就是加个中间层,让调用和实现分离
命令模式的撤销
命令模式的一个应用是在封装命令的时候,可以添加一些撤销和保存状态。iOS 中提供了 NSUndoManager
来实现这个撤销和恢复的过程。
迭代器模式
难度:3 频率:5
迭代器模式:提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。
在软件开发中,我们经常需要使用聚合对象来存储一系列数据。聚合对象拥有两个职责:一是存储数据;二是遍历数据。从依赖性来看,前者是聚合对象的基本职责;而后者既是可变化的,又是可分离的。因此,可以将遍历数据的行为从聚合对象中分离出来,封装在一个被称之为“迭代器”的对象中,由迭代器来提供遍历聚合对象内部数据的行为,这将简化聚合对象的设计,更符合“单一职责原则”的要求。
结构
聚合类 ConcreteAggregate
中有一个列表属性,同时还提供了一个 createIterator()
方法,该方法会将其列表属性传入,并返回一个具体迭代器对象 ConcreteIterator
。ConcreteIterator
中保存一个列表,同时还有一个私有的游标 cursor
用来标识迭代到的当前值,并提供 first()
,next()
等方法进行迭代。
例子
这个例子和上面说的基本类似,ProductList
中有一个数组,还提供 createIterator()
方法创建迭代器 ProductIterator
,并将数组设置给迭代器的 productList
属性中。迭代器提供了两个游标 cursor
分别用来正序遍历和逆序遍历。
就是在一个类中保存一个数组,然后调用一些方法遍历数组
中介者模式
难度:3 频率:2
中介者模式:用一个中介对象(中介者)来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式。
中介者模式是“迪米特法则”的一个典型应用.
缺点:中介者类可能非常复杂,难以维护
结构
中介者模式主要是消除耦合。所有的具体同事类 ConcreteColleague
都不能直接相互调用,它们内部有一个从 Colleague
中继承过来的 Mediator
属性,在创建的时候设置具体中介者 ConcreteMediator
。ConcreteMediator
中存在各种调用具体同事类ConcreteColleague
的方法。
例子
这是一个各个组件相互调用的例子。同上面所说的一样,Component
中有抽象中介者 mediator
,会在生成具体同事类(即各个组件)的时候,setMediator()
设置具体中介者 ConcreteMediator
。它提供一个 componentChanged()
方法供具体同事类调用,这个方法用来处理跳转啊交互之类的逻辑。Component
中提供的 change()
方法用来调用其内部 mediator
的 componentChanged()
,相当于是一个对于 componentChanged()
方法的封装。update()
方法是在其他类调用了 componentChanged()
后,当前类做出的响应操作。change()
和 update()
两个方法都不是必须的。
备忘录模式
难度:2 频率:2
备忘录模式:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
其实就是提供了一个备忘录类,用来存储历史状态。
结构
负责人 Caretaker
是管理者,用来保存备忘录 Memento
,但是不能对其内容进行操作。Caretaker
可以存储一个或多个备忘录对象。备忘录 Memento
用来保存原发器 Originator
的内部状态,根据原发器来决定保存哪些内部状态。原发器 Originator
是一个普通类,可以创建一个备忘录,并存储它的当前内部状态,也可以用备忘录来回复其内部状态。
例子
如图是一个保存象棋路径的例子。负责人 MementoCaretaker
中保存了一个备忘录 memento
。这里的 memento
只是表示备忘一种状态,如果你想保存多个状态,那么需要使用一个 memento
的数组。它还提供了设置和获取 memento
的方法 getMemento()
和 setMemento()
(由于只有原发器 Originator
能够创建备忘录 Memento
,所以这里 setMemento()
传入的是此例中 Chessman
的 save()
方法)。备忘录对象 ChesmanMemento
保存了具体需要备忘的属性,这里指坐标x、y。原发器 Chessman
提供了需要备忘的状态值,它的 save()
方法用来创建新的备忘录对象 ChessmanMemento
。同时,它还提供了一个 restore()
方法用来恢复原来的状态。由于其不管理 memento
,所以传入的是 MementoCaretaker
中保存的备忘录。
1 | public static void main(String args[]) { |
没什么特别的命名方式,这是一个很基础的设计模式。
其实这个逻辑没有写的这么复杂。如果你想保存历史状态,你就需要一个对象去保存它,也就是管理者
Caretaker
。那么要保存什么状态呢?那就抽象出来,编程一个备忘录类Memento
吧。那么这个状态哪里来的呢?那只有原发器Originator
能够创建,因为只有它才知道当前的状态啊。
观察者模式
难度:3 频率:5
观察者模式:定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
结构
目标 Subject
中保存了许多观察者 Observer
,通过 attach()
和 detach()
方法增删。同时,还提供了一个 notify()
的方法,当 Subject
觉得状态变化了,需要通知观察者的时候,主动调用触发观察者的方法,。(不用管里面的 subjectState 相关)
例子
这个例子的背景是一个联机对战游戏,一个玩家被攻击后,会通知其它玩家。其中目标 Subject
是一个控制中心 ConcreteAllyControlCenter
,它继承了抽象目标 AllyControlCenter
,实现了其通知方法 notifyObserver()
>它的观察者 Observer
是各个玩家 Player
。当某个玩家被攻击后,会调用 beAttached
方法,它会调用控制中心的 notifyObserver()
方法通知其它观察者。
状态模式
难度:3 频率:3
状态模式:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。
状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。对象根据不同的状态执行不同的操作,同事对象中的状态也会因为外部因素改变。
结构
环境类 Context
是拥有不同状态 state
的对象。其中有一个 request()
方法,会执行不同状态下的不同操作。抽象状态类 State
中也保存了环境类的实例,目的是方便针对不同状态对环境类的属性操作。不同状态的不同操作在具体状态类 ConcreteState
中的 handle()
方法中实现。改变状态的方法最好在环境类中实现。
例子
该例子中,环境类 Account
有三种状态 OverdraftState
,RestrictedState
,NormalState
。状态中也保存着环境类的实例 acc
。环境类调用其处理方法诸如 deposit()
的时候,会调用状态类的相应方法进行处理。
这里,抽象状态类 AccountState
中有一个 stateCheck()
方法,用来改变环境类的状态,它会在处理完环境类的相关属性后被调用。我认为,在状态类中调用改变状态的方法是不对的,这样明显违反了迪米特法则,使调试变得艰难。我认为应该还是在环境类中进行判断。可以结合职责链模式进行状态的设置。
状态模式和职责链模式的区别
其实,状态模式和职责链模式挺像的。
职责链模式对目标类中的某个属性进行判断,来决定职责链上的哪个类来进行处理;而状态模式预先将状态(即要处理的类)设置好,每次处理完之后再判断下一个状态是什么(即下次应该那个类处理)。
策略模式
难度:1 频率:4
策略模式:定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式
实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。
策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开。
结构
环境类 Context
中有一个抽象策略类 Strategy
,这个策略类将会在执行的时候通过反射的方式动态的生成具体抽象类 ConcreteStrategy
,以表示不同的策略。在执行环境类中方法 algorithm()
的时候,会执行策略类的相应方法。
例子
这是一个电影票打折的例子。针对不同的人群有不同的打折策略 StudentDiscount
,VIPDiscount
,ChildrenDiscount
。这个策略将保存在环境类 MoviewTicket
的 discount
属性中,可以。每次执行环境类的方法 setPrice()
的时候,都会调用相应策略的方法。
策略模式和状态模式的区别
策略模式和状态模式很像。策略模式需要在环境类中设置一个策略类,状态模式也需要在环境类中设置一个状态类。两者的区别在于,状态模式会根据环境类中的属性自动改变状态的值,而策略类需要直接设置不同的策略。
策略模式和命令模式区别
命令模式用来隐藏接收者执行的细节。也就是说要有一个接收者,命令类充当转发的过程。
策略模式不含有接收者,仅仅是对算法的多种封装。
模板方法模式
难度:2 频率:3
模板方法模式:定义一个操作中算法的框架,而将一些步骤延迟到子类中。模板方法模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
结构
抽象类中定义了多个方法,并在方法 TemplateMethod()
中定义了调用次序。具体子类中实现这些方法。
其实就是在基类中添加一个方法,调用一些需要子类实现的抽象方法。很普通的设计模式
访问者模式
难度:4 频率:1
访问者模式(Visitor Pattern):提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
试想一下,如果某个类要动态替换某一个方法怎么做?我们可以使用命令模式,不同的 command
类的 excute()
执行不同的行为。那么更进一步,如果这个类有多个兄弟类也各自有实现了这个方法呢?如果还是用命令模式,那么每个兄弟类都要对应一个 Command
类,类型将非常多。这个时候,可以借助于访问者模式。下面看看访问者模式如何实现的。
结构
先看 Element
,就是上面说的要动态方法的基类,也就是下面所说的被访问者,它的子类各自实现了一些方法 opertaionA()
,operationB()
。现在还有一个访问者的基类 Visitor
,在基类中直接就定义了针对 Element
不同子类会被调用的方法 visitConcreteElementA()
和 visitConcreteElementB()
。Element
的子类将原本在 operationA()
中调用的业务方法,放到了 Visitor
的子类的 visitCooncreteElementA()
中去。另外, Element
方法提供了一个接收不同 Visitor
的方法 accept()
。
具体的调用过程就是,原本调用业务逻辑的 operationA()
现在改为调用 accept()
方法,然后再在 accept()
方法中,调用具体的实现方法 visitConcreteElementA()
。参考代码如下:
1 | // ElementA |
访问者模式可以方便的替换 Visitor
符合开闭原则,但是如果增加了 Element
的子类,就需要为每一个 Visitor
添加 visitConcreteElementC()
方法,不符合开闭原则。
命名上,访问者类命名为 XxxVisitor
,然后为每一个被访问者对象的子类提供 visitXxx:
方法。被访问者的基类提供抽象方法 accept:
方法。
访问者模式确实用的比较少,算是一种针对于多子类对象的命令模式吧
分类
接口适配
- 适配器
- 桥接
- 外观
对象去耦
- 中介者
- 观察者
抽象集合
- 组合
- 迭代器
行为拓展
- 访问者
- 装饰
- 责任链
算法封装:
- 模板方法
- 策略
- 命令
对象访问:
- 代理
对象状态:
- 备忘录
对象创建:
- 原型
- 工厂方法
- 抽象工厂
- 建造者
- 单例
备注
本文中的所有示意图:紫色的都表示接口类型,蓝色的都表示实体类(如果要在某个类内部保存属性,那么就不能将其设置为接口)
本文中的所有示意图:“+” 表示方法,”-“ 表示对象
还有解释器模式和访问者模式和享元模式没有看,用的太少不看了。