设计模式(三)创建型模式
根据菜鸟教程的目录,我们首先来看看创建型模式。 创建型模式研究:
下面分别对创建型模式下的各种具体模式进行讲解。
先看例子: 工厂模式。
某功能的使用者只和接口打交道,不关心如何实现。这种情况下,肯定有一个接口类,使用者使用接口;功能提供者继承并实现接口。这利用了C++的多态特性。
既然使用者只关心接口,那么没有必要把子类直接给使用者,没有必要让使用者在代码中直接new子类。如果这样做,会把不必要的信息暴露给使用者,增加了信息的耦合。试想,如果使用者在很多地方都new了子类,那么如果这些地方需要修改的话,怎么改?只能一个一个地方改,改完还需要编译,维护极其困难。
工厂模式是指,针对某一功能接口,我们要新建一个工厂类,此工厂类将接口子类名称、接口子类的创建过程封装起来,只返回一个接口指针给接口的使用者。接口的实现类对使用者完全透明,高度解耦。这样可以方便地切换接口的具体实现,而不影响上层功能使用者。拿 汽车 打比方,不管工厂生产 汽车 的流程是什么,只要是 汽车 ,它的驾驶方法(人机接口)都类似。
显而易见,工厂模式在使用者和实现者之间增加了一个封装层,这正印证了计算机行业中一句名言:
典型的例子是:Qt中的数据库模块就利用了工厂模式,封装了数据库的底层实现。在保持数据库用户接口不变的情况下,通过更换数据库驱动,可以实现数据库类型无缝切换。
在需求趋于稳定时使用,需求不稳定时,不要过度设计,否则设计很容易被推翻,白费力气。
从设计模式的本质来看,工厂模式:
先看例子: 抽象工厂模式。
由前面工厂模式可知,所有的“工厂”有一个共同点:每个工厂都会提供创建对象的函数。 既然所有工厂都实现了同一类功能,那么我们可以为工厂抽象出一个公共接口(虚基类),此接口定义了创建工厂子类的功能。 这种场景是否似曾相识?是的,工厂和工厂的功能接口构成了使用工厂模式的场景。即工厂本身也适用于工厂模式。 使用工厂模式来设计工厂,必然要写一个生产工厂的工厂。 生产工厂的工厂,返回值是工厂的抽象接口类,所以这种设计模式叫“抽象工厂模式”。其实,笔者觉得把这种设计模式叫做“工厂工厂模式”更容易理解。
如果只有一个工厂就不要使用抽象工厂模式了,只有在工厂很多时,才使用抽象工厂模式。
需求不稳定时,不要过度设计,一切都可能被推翻。 对于小的项目,不需要过度追求使用设计模式,架构的代码最好只占整个项目代码的一小部分,否则就是主次颠倒,给自己找麻烦。 对于大的项目,在需求较稳定的情况下,为了提高可维护性、扩展性,可以考虑使用设计模式。 另外,抽象工厂模式有一定的理解难度,要考虑你设计的代码,其他人是否能够读懂,简单易懂也是需要考虑的方面。
所以,从设计模式的本质来看,
先看例子: 单例模式。
上面的例子都是允许一个类被创建多次的。如果我们想要限制一个类只被创建一次,即只有一个全局可访问的实例(和C语言中的全局变量一样),例如应用程序对象,每个应用程序都应该只有一个应用程序对象。此时应该怎么编写代码呢?
答案还是封装。把不想暴露出来的信息藏起来,把必须暴露的信息暴露出来。单例模式把类的构造函数设置成private私有访问权限,限制外部无法通过new来创建实例。只能通过特定的接口来获取实例指针。需要提及的是,封装时需要考虑多线程安全的问题。
当一个类需要有多个实例存在时,不使用单例模式。
从设计模式的本质上看,
具体的例子和写法,可以参考菜鸟教程中的 建造者模式。
建造者模式的典型使用场景是快餐店的套餐搭配模型。 套餐由若干个单个餐品组合而成。单个餐品又由不同的原材料构成。这种层层组合的树形对象关系的应用场景下,为了创建顶层的对象,需要先一层层的创建底层的对象,逐步向上,直到构造出根对象。 这种场景下,使用继承可以将同类的对象关联起来,使用组合可以将不同类型的对象组合起来。组合就是把不同对象放在一块内存中保存,作为一个整体使用。
完全使用继承来解决此类问题是非常不提倡的。设计模式理论中有一个原则是:“少用继承,多用组合”。因为继承是一种强耦合,组合是一种松散的耦合。耦合不利于适应需求变化,是项目中的一颗定时炸弹。
从设计模式的本质上看,
菜鸟教程中没有提及的一种设计模式是组合模式。具体内容可以参考: 第四节:组合模式和建筑者模式详解。
这里简单说明一下,组合模式和建造者模式比较像,也是遵循树形对象关系结构。和建造者模式相比,不同之处在于,子对象和父对象具有相同的类型。所以可以说,组合模式是简单的建造者模式。
具体的使用场合和实例,见原型模式。
原型模式,在实际使用时可能用得不多。用一句话描述其特点:
这种克隆是一种内存中的复制行为,速度快,能充分利用已有对象的缓存数据,性能高。克隆出来的对象具有和原对象相同的属性和行为,可以用来帮助原对象处理一些事务。用一句动漫中的词汇来描述,“影分身”再合适不过了。
从设计模式的本质看,
下一篇,我们将介绍结构型模式。
适配器模式:SD读卡器
桥接模式:抽象是抽象,具体是具体,隔离开来
过滤器模式:结果是一个list,用不同的标准类去过滤
组合模式:树
装饰器模式:实现同一套接口,但是增加功能
外观模式:隐藏结构的复杂性,比如提供一个api接口,每个api调用的模块细节隐藏
享元模式:
代理模式:实现同一套接口,但是功能不变,只是加一下控制
创建一个接口类,集成被扩展的类;
是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
举个真实的例子SD读卡器,读卡器是作为内存卡和笔记本之间的适配器。您将内存卡插入读卡器,再将读卡器插入笔记本,这样就可以通过笔记本来读取内存卡。
我们通过下面的实例来演示适配器模式的使用。其中,音频播放器设备只能播放 mp3 文件,通过使用一个更高级的音频播放器来播放 vlc 和 mp4 文件。
应用实例:
比如绘制四种车,每种车有3种启动方式,将抽象和实现分离,解决多次继承的问题。
不必要的继承导致类爆炸;
http://www.jasongj.com/design_pattern/bridge/
在一个抽象类里面聚合其他的抽象类(比多继承好)
使用场景:
过滤器模式(Filter Pattern)或标准模式(Criteria Pattern)是一种设计模式,这种模式允许开发人员使用不同的标准来过滤一组对象,通过逻辑运算以解耦的方式把它们连接起来。
即声明一种检测标准类,如下:
通过实现这个接口来选出不同的对象。
其实就是树的架构,每个节点都相同
应用实例:
动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
类图: https://www.jianshu.com/p/d80b6b4b76fc
以下情况可以使用装饰器模式:
隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。
享元模式尝试重用现有的同类对象,如果未找到匹配的对象,则创建新对象。
跟单例模式差不多,可以称之为多例模式:
核心使用一个hashmap存储一个之前用过的对象,如果有了就不用创建新的了
何时使用:
应用实例:
一个类代表另一个类的功能。
主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
例子:
注意事项:
KVO 和 NSNotification 都是 iOS 中观察者模式的一种实现。
KVO 可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey: 等可变代理方法获得集合代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArray 和 NSSet 。
先创建一个类,作为要监听的对象。
监听实现
KVO 主要用来做键值观察操作,想要一个值发生改变后通知另一个对象,则用 KVO 实现最为合适。斯坦福大学的 iOS 教程中有一个很经典的案例,通过 KVO 在 Model 和 Controller 之间进行通信。如图所示:
KVO 触发分为自动触发和手动触发两种方式。
如果是监听对象特定属性值的改变,通过以下方式改变属性值会触发 KVO:
如果是监听集合对象的改变,需要通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO。集合对象包含 NSArray 和 NSSet 。
普通对象属性或是成员变量使用:
NSArray 对象使用:
NSSet 对象使用:
observationInfo 属性是 NSKeyValueObserving.h 文件中系统通过分类给 NSObject 添加的属性,所以所有继承于 NSObject 的对象都含有该属性;
可以通过 observationInfo 属性查看被观察对象的全部观察信息,包括 observer 、 keyPath 、 options 、 context 等。
注册方法 addObserver:forKeyPath:options:context: 中的 context 可以传入任意数据,并且可以在监听方法中接收到这个数据。
context 作用:标签-区分,可以更精确的确定被观察对象属性,用于继承、 多监听;也可以用来传值。
KVO 只有一个监听回调方法 observeValueForKeyPath:ofObject:change:context: ,我们通常情况下可以在注册方法中指定 context 为 NULL ,并在监听方法中通过 object 和 keyPath 来判断触发 KVO 的来源。
但是如果存在继承的情况,比如现在有 Person 类和它的两个子类 Teacher 类和 Student 类,person、teacher 和 student 实例对象都对象的 name 属性进行观察。问题:
当 name 发生改变时,应该由谁来处理呢?
如果都由 person 来处理,那么在 Person 类的监听方法中又该怎么判断是自己的事务还是子类对象的事务呢?
这时候通过使用 context 就可以很好地解决这个问题,在注册方法中为 context 设置一个独一无二的值,然后在监听方法中对 context 值进行检验即可。
苹果的推荐用法:用 context 来精确的确定被观察对象属性,使用唯一命名的静态变量的地址作为 context 的值。可以为整个类设置一个 context ,然后在监听方法中通过 object 和 keyPath 来确定被观察属性,这样存在继承的情况就可以通过 context 来判断;也可以为每个被观察对象属性设置不同的 context ,这样使用 context 就可以精确的确定被观察对象属性。
context 优点:嵌套少、性能高、更安全、扩展性强。
context 注意点:
KVO 可以监听单个属性的变化,也可以监听集合对象的变化。监听集合对象变化时,需要通过 KVC 的 mutableArrayValueForKey: 等方法获得代理对象,并使用代理对象进行操作,当代理对象的内部对象发生改变时,会触发 KVO 的监听方法。集合对象包含 NSArray 和 NSSet 。(注意:如果直接对集合对象进行操作改变,不会触发 KVO。)
可以在被观察对象的类中重写 + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key 方法来控制 KVO 的自动触发。
如果我们只允许外界观察 person 的 name 属性,可以在 Person 类如下操作。这样外界就只能观察 name 属性,即使外界注册了对 person 对象其它属性的监听,那么在属性发生改变时也不会触发 KVO。
也可以实现遵循命名规则为 + (BOOL)automaticallyNotifiesObserversOf<Key> 的方法来单一控制属性的 KVO 自动触发,<Key>为属性名(首字母大写)。
使用场景:
使用 KVO 监听成员变量值的改变;
在某些需要控制监听过程的场景下。比如:为了尽量减少不必要的触发通知操作,或者当多个更改同时具备的时候才调用属性改变的监听方法。
由于 KVO 的本质,重写 setter 方法来达到可以通知所有观察者对象的目的,所以只有通过 setter 方法或 KVC 方法去修改属性变量值的时候,才会触发 KVO,直接修改成员变量不会触发 KVO。
当我们要使用 KVO 监听成员变量值改变的时候,可以通过在为成员变量赋值的前后手动调用 willChangeValueForKey: 和 didChangeValueForKey: 两个方法来手动触发 KVO,如:
NSKeyValueObservingOptionPrior (分别在值改变前后触发方法,即一次修改有两次触发)的两次触发分别在 willChangeValueForKey: 和 didChangeValueForKey: 的时候进行的。
如果注册方法中 options 传入 NSKeyValueObservingOptionPrior ,那么可以通过只调用 willChangeValueForKey: 来触发改变前的那次 KVO,可以用于在属性值即将更改前做一些操作。
有时候我们可能会有这样的需求,KVO 监听的属性值修改前后相等的时候,不触发 KVO 的监听方法,可以结合 KVO 的自动触发控制和手动触发来实现。
例如:对 person 对象的 name 属性注册了 KVO 监听,我们希望在对 name 属性赋值时做一个判断,如果新值和旧值相等,则不触发 KVO,可以在 Person 类中如下这样实现,将 name 属性值改变的 KVO 触发方式由自动触发改为手动触发。
有些情况下我们想手动观察集合属性,下面以观察数组为例。
关键方法:
需要注意的是,根据 KVC 的 NSMutableArray 搜索模式:
有些情况下,一个属性的改变依赖于别的一个或多个属性的改变,也就是说当别的属性改了,这个属性也会跟着改变。
比如我们想要对 Download 类中的 downloadProgress 属性进行 KVO 监听,该属性的改变依赖于 writtenData 和 totalData 属性的改变。观察者监听了 downloadProgress ,当 writtenData 和 totalData 属性值改变时,观察者也应该被通知。以下有两种方法可以解决这个问题。
以上两个方法可以同时存在,且都会调用,但是最终结果会以 keyPathsForValuesAffectingValueForKey: 为准。
以上方法在观察集合属性时就不管用了。例如,假如你有一个 Department 类,它有一个装有 Employee 类的实例对象的数组,Employee 类有 salary 属性。你希望 Department 类有一个 totalSalary 属性来计算所有员工的薪水,也就是在这个关系中 Department 的 totalSalary 依赖于所有 Employee 实例对象的 salary 属性。以下有两种方法可以解决这个问题。
有时候我们难以避免多次注册和移除相同的 KVO,或者移除了一个未注册的观察者,从而产生可能会导致 Crash 的风险。
三种解决方案: 黑科技防止多次添加删除KVO出现的问题
我们在对象添加监听之前分别打印对象类型
我们看到,添加监听后,使用 object_getClass 方法获取model类型时获取到的是 NSKVONotifying_DJModel 。
这里就产生了几个问题:
从打印结果可以看出, NSKVONotifying_DJModel 是 DJModel 的子类,说明我们添加了监听之后动态创建了一个 DJModel 的子类 NSKVONotifying_DJModel ,并将对象 DJModel 的类型更改为了 NSKVONotifying_DJModel 。
我们从源码看出,实例对象调用 class 方法会返回 isa 指针,类对象调用 class 方法会返回自己,通过 object_getClass 方法获取对象的类型也会返回 isa 指针。从源码上看model对象添加监听之后使用 class 和使用 object_getClass 方法获取到的类型应该是一样的,但是这里却不同,我们猜测在添加了监听之后在 NSKVONotifying_DJModel 中重写了 class 方法。
我们打印一下添加监听前后 class 方法的 IMP 地址来确认是否重写了 class 方法。
从打印结果可以看出,添加监听之后 class 方法的地址改变了,这验证了我们之前的猜想, NSKVONotifying_DJModel 类中重写了 class 方法。
我们监听对象时调用了 set 方法,我们对监听前后的 set 方法单独分析。
我们再添加监听前后分别打印 setName 方法的 IMP 地址。
通过打印结果可以看出 setName 方法也在 NSKVONotifying_DJModel 中被重写了,我们再使用lldb来看下 setName 具体是什么
第一个地址打印的是添加监听前 setName 方法的 IMP 地址,第二个打印的是添加监听后 setName 方法的 IMP 地址。
这里看出添加监听前 setName 对应的具体方法就是 setName ,但是添加监听后, setName 对应的鸡头方法却变成了 _NSSetObjectValueAndNotify 函数。
下面我们就来研究一下 _NSSetObjectValueAndNotify 函数。
从上面与KVO相关的方法中我们可以看出,每一种数据类型都对应了一个 setXXXValueAndNotify 函数。
不过这些函数的具体实现没有公布,所以内部构造这里还是不清楚。
但是我们知道,在调用 `setXXXValueAndNotify 函数的过程中会调用另外两个方法。
测试后得出了以下几个结论:
我们还可以利用这两个方法手动触发 observeValueForKeyPath 方法:
所以我们判断在 _NSSetObjectValueAndNotify 函数内部,在调用原来的 set 方法之前插入了 willChangeValueForKey 方法,在调用原来的 set 方法之后插入了 didChangeValueForKey 方法,并根据初始化时的枚举值决定调用 observeValueForKeyPath 的时机。
(1)添加监听时,会动态创建一个监听对象类型的子类,并将监听对象的 isa 指针指向新的子类。
(2)子类中重写了 class 和监听属性的 set 方法。
(3)重写 class 方法是为了不将动态创建的类型暴露出来。
(4)重写 set 方法是将 set 方法的具体实现替换成了与属性类型相关的 __NSSetXXXValueAndNotify 函数。
(5)在 __NSSetXXXValueAndNotify 函数内部在 set 方法前后分别插入了 willChangeValueForKey 和 didChangeValueForKey 这两个方法。
(6)根据添加监听时的枚举值决定调用 observeValueForKeyPath 的具体时机。
C#设计模式(2)-Factory Method Pattern
C#设计模式(3)-Abstract Factory Pattern
C#设计模式(4)-Singleton Pattern
C#设计模式(5)-Builder Pattern
C#设计模式(6)-Prototype Pattern
C#设计模式(7)-Adapter Pattern
C#设计模式(8)-Composite Pattern
C#设计模式(9)-Decorator Pattern
C#设计模式(10)-Proxy Pattern
设计模式(11)-Flyweight Pattern
设计模式(12)-Facade Pattern
设计模式(13)-Bridge Pattern
设计模式(14)-Chain of Responsibility Pattern
设计模式(15)-Command Pattern
设计模式(16)-Observer Pattern
设计模式(17)-Visitor Pattern
设计模式(18)-Template Method Pattern
设计模式(19)-Strategy Pattern
在博客园人搜索C#设计模式,会有详细的实践教程,这个更有效!
学海无涯啊!
按照目的来分,设计模式可以分为创建型模式、结构型模式和行为型模式。
创建型模式用来处理对象的创建过程;结构型模式用来处理类或者对象的组合;行为型模式用来对类或对象怎样交互和怎样分配职责进行描述。
创建型模式用来处理对象的创建过程,主要包含以下5种设计模式:
工厂方法模式(Factory Method Pattern)
抽象工厂模式(Abstract Factory Pattern)
建造者模式(Builder Pattern)
原型模式(Prototype Pattern)
单例模式(Singleton Pattern)
结构型模式用来处理类或者对象的组合,主要包含以下7种设计模式:
适配器模式(Adapter Pattern)
桥接模式(Bridge Pattern)
组合模式(Composite Pattern)
装饰者模式(Decorator Pattern)
外观模式(Facade Pattern)
享元模式(Flyweight Pattern)
代理模式(Proxy Pattern)
行为型模式用来对类或对象怎样交互和怎样分配职责进行描述,主要包含以下11种设计模式:
责任链模式(Chain of Responsibility Pattern)
命令模式(Command Pattern)
解释器模式(Interpreter Pattern)
迭代器模式(Iterator Pattern)
中介者模式(Mediator Pattern)
备忘录模式(Memento Pattern)
观察者模式(Observer Pattern)
状态模式(State Pattern)
策略模式(Strategy Pattern)
模板方法模式(Template Method Pattern)
访问者模式(Visitor Pattern)
推荐你一本好书:《软件秘笈:设计模式那点事》,里面讲解的23中设计模式例子很生动,容易理解,还有JDK中设计模式应用情况,看了收获挺大的!百度里面搜“设计模式”,第一条中设计模式百度百科中就有首推该图书,浏览量在20几万以上的,不会错的。好东西大家一起分享!
祝你早日学会设计模式!
总体来说设计模式分为三大类:创建型模式,结构型模式,行为型模式
1、创建型模式,共五种:
工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
2、结构型模式,共七种:
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
3、行为型模式,共十一种:
策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
Java视频教程: