0%

来吧设计模式

关于设计模式

引言

写了两年的Java和Android,对于面向对象的思想已然有了自己的一份体会,并在一次次的实践中,加深对这种思想的体会。每一次对代码封装复用,松耦合设计的过程中,都能感受到面向对象思想带来的愉悦,这大概就是其魅力所在。关于设计模式,也不可避免的接触了一些想观察者模式,单例模式,工厂模式等,但对于他们都是只停留在简单的实践阶段,对于其思想并没有太深刻的体会。
在我看来,作为一个Java程序员,面向对象以及设计模式的思想深度,将决定其写出来的代码,是一个精致巧妙的艺术品,还是如书中所说是一团毫无弹性的意大利面。
决定需要好好恶补一下关于设计模式的知识,我选择了《Head First 设计模式》,就目前所翻阅到章节看来,我选对了!

来吧设计模式

策略模式

这个模式一直有所耳闻,但一直没有了解。书中没有一开始就点明使用策略模式,最后一刻道破的时候,真的使我有种茅塞顿开的通透感。
原来平时代码中面向接口,继承组合,都是在践行这种设计模式。关于定义,书中给出的描述是:

定义了算法族,分别封装起来,使他们之间可以互相替换,使得算法的变化独立于使用该算法的用户

我的理解是,将有可能变化的代码抽离出来与稳定不变的代码分开,成为所谓的算法族(其实就是行为组,对象的所有行为),使得新的业务逻辑的改变不会影响的旧的代码通过组合的方式,为特定的对象赋予特定的方法组
书中使用定义多种类的鸭子作为例子,即将鸭子的行为(不同鸭子行为不同,即为可能变化的部分)抽象出来,成为一个接口,使用该接口实现不同的行为类,这是封装变化。然后在鸭子的实际类中,持有一个行为接口的引用,通过setter灵活的对该接口进行实例化,从而达到在运行时,动态的修改增加鸭子行为的效果,这是使用组合
关于类的组合,看《Java编程思想》时候就有所了解,当时并不理解为什么说“少用继承,多用组合”,“‘有一个’比‘是一个’更好”,但现在终于有了自己的一点理解。使用继承,意味着可能会出现大量重复的代码,或者局部的修改影响到大部分的代码的情况;而使用组合,可以很容易的知道类的所有算法族,通过setter的方式,灵活修改算法族,并且对于新的算法族的增加,不会影响到已有的代码逻辑。这就是组合优于继承地方。关于组合的缺点,个人觉得可能与增加了类和接口的数量,或者使得类的结构变得复杂有关。

OO设计原则
将代码中可能需要变化之处独立出来,不要和那些不需要变化的代码混合在一起。
针对接口编程,而不针对实现编程
多用组合,少用继承

观察者模式

这是一个相对来说已经十分熟悉的设计模式,在android中,有许多类似的实现,如button的点击事件设置等,甚至常用的接口回调,我都觉得有点观察者模式的味道。
关于观察者模式,我的理解是,对象A对对象B的状态感兴趣(关注),于是A对B进行订阅,则B的状态发生变化时,则会对所有订阅的对象进行通知,订阅对象A再根据B的通知作出相应的动作。因此如果B迟迟没有发生变化,则A不会出现相应的反应。
再来看看书中的理解。观察者模式定义是:定义了对象间的一对多的依赖,当一个对象改变状态时,其所有依赖者将收到通知并自动更新。我认为这与我的理解基本一致。
关于观察者模式的实现,书中介绍了两种方式,一种是面向接口式的实现,一种是java.util中内置的Observer实现方式。
提出内置观察者模式的原因,主要为了解决,主题向观察者推送更新时,会把所有的新数据都推送给观察者,或者将一些细微的变化都推送出去,但其实有的观察者只需要其中的某几个数据足矣。因此可以使用观察者拉取的方式,观察者主动向主题拉取数据。
java.util内置的Observer,只通知观察者更新,但观察者需要自己拉取的方式实现。这是我有些困惑,其本质其实相似,主题通过observer.update()方法通知observer更新的消息,并附上对象实例作为参数,让观察者用getter的方式获取部分特定的数据。
重新看过这一章,发现果然有遗漏的地方。其实java.util的观察者模式,支持主题向观察者推送数据,也支持观察者自己向主题拉取数据。Observer.update(Observable, Object)方法,需要将Observable传入以告知观察者是哪一个主题的更新。如果使用主题推送的方式,则将数据以Object的方式推送给观察者,如果是观察者拉取的方式,则观察者收到通知后,自己通过主题的实例获取更新的数据。关于细微变化的推送,内置的观察者是在notify之前加了一层判断,判断当前的更新是否需要推送。
有一点需要注意,不可依赖观察者接收通知的次序。毕竟主题是通过对订阅列表的遍历进行通知的,所以接收通知的次序不可被过分依赖。
关于java内置的观察者,书中还提到一件事。就是其Observable是需要通过继承实现的并且其中像setChange()方法都是受保护的(protected),因此也不能通过组合的方式实现。这也就违背了“多用组合少用继承”的原则。

因为Observable是一个类,你必须设计一个类继承它。…毕竟Java不支持多重继承,这限制了Observable的复用潜力。
除非你继承自Observable,否则你无法创建Observable实例并组合到你的对象中来。
观察者模式作为一种新的模式,以松耦合的方式在一系列对象之间沟通状态。其典型的代表即为之前Android中常用的MVC模式。

观察者模式中,主题的状态,观察者的数目和类型都会改变,使用该模式,可以改变依赖主题状态的对象,而不用改变主题。
观察者模式使用组合,在运行时将观察者动态的“组合”进主题中。对象间的这种关系不是通过继承产生而是通过运行时的组合实现。
观察者利用主题的接口进行订阅,主题通过观察者的接口进行更新的通知。使两者之间运作正常却又松耦合。

又注意到一个问题,观察者模式的写法,跟回调函数好像有点相似。看了几篇博客,印证了自己的猜想。总结就是,回调模式是观察者模式的特殊实现,不同于一般的一对多的观察者模式,回调模式是一对一的观察者模式。

OO设计原则
为了交互对象之间的松耦合设计而努力,即对象之间依然可以正常交互,但是不太清楚彼此的细节。

装饰者模式

我曾以为男子汉应该用继承处理一切。后来我领教到运行时扩展,远远比编译时期的继承威力大…

说到装饰者模式,我依然只是有所耳闻,但始终没有去了解这是一个怎样的模式,只是隐隐觉得它好像和工厂模式或者build有所关联。
这一章节中经常出现的一句话:类的设计应当对扩展开放,对修改关闭,即“开放-关闭原则”。大概意思是在需求发生变化或者新增时,应当通过扩展原有类的方式进行更新,而不是通过修改原有类的方式。但说实在,对于这句话我没有在更深层次的体会和理解,大概还是OO检验欠缺的缘故。
使用装饰者模式,大概就是对类的一种扩展实现。装饰者模式,旨在使用扩展+委托的方式代替继承。书中的定义是:动态的将责任附加到对象上,提供了一种比继承更有弹性的的扩展的方案。
基本的实现是:首先要求装饰者与被装饰者属于同一类型(基类),通过不断使用装饰类包装(组合)被装饰类的方式,实现类功能属性的拓展更新。即实现扩展的效果,又不修改原有的类实现。
关于装饰者与被装饰者需要继承同一基类,书中这样解释:

这里利用继承达到“类型匹配”的效果,而不是利用继承获取“行为”。

为了装饰类可以对被装饰类进行包装去取代,需要它们有相同的类型,而行为的获得,是通过组合实现,所以这里使用继承,并不影响OO的设计原则。

1
2
3
4
5
6
7
// 被装饰者(茶)
Tea tea = new Tea();
// 装饰者开始装饰
Decoration decoration = new Milk(tea);
decoration = new Sugar(decoration);
decoration = new Soy(decoration);
decoration = new Coffee(decoration);

最简单的装饰者模式实现如上,有点奇怪,好像没有见过这种代码,又好像似曾相识。书中预示道:工厂模式和生成器模式会对装饰者模式有更好的实现方式,这便是我想起了见过生成器模式代码中相似的类的包装。
如果用户需要窥视装饰者链中的每一个装饰者,可以使用一个数据结构如列表等实现方式实现。
关于装饰者模式的经典实现,书中介绍了Java I/O,我立即回想起了当初使用java的文件IO流时,构造流的一系列摸不着头脑的麻烦操作,原来那个过程就是对IO流的一系列装饰以使其获得更多功能如LineNumberInputStream计算行数功能,BufferedInputStream增加缓冲输入改进性能。

工厂模式

终于啃完了这一章。工厂方法,也许是听的最早的设计模式,早在学习Java的时候,就了解过通过静态工厂方法初始化对象的操作。因此对其印象一直只是对对象实例化的封装,然而这一章的诸多概念,确看的一头雾水,一翻目录发现这一章的篇幅竟是其他的两倍!我一共读了三遍,才得以稍微理解。
关于工厂模式书中介绍了三个概念,简单工厂,工厂方法模式,抽象工厂模式。

简单工厂

简单工厂并不是一种设计模式,只能说是一种变成习惯,即是将对象初始化的工程封装到一个工厂类中,由该工厂对该类进行初始化。其中工厂的方法一般设为静态,即是所谓静态工厂方法。
其实说白,就是将一个类的实例化(new过程),放到另一个类(工厂类)的静态方法中,使得类的初始化与客户代码分离。
关于静态工厂,可以实现不创建工厂实例的情况下创建对象。但因为也因为是静态的不能通过继承扩展方法。

工厂模式

工厂模式,是将实际创建对象的工厂方法,从工厂类移到需要创建对象的类中,并设为抽象,由该类的子类决定其该创建具体的子类。
所谓让子类决定,使用书中的比萨店的例子说,在PizzaStore中通过工厂方法实例化Pizza对象,并在orderPizza()中使用该对象。其中由于工厂方法是抽象的,所以orderPizza()在使用Pizza对象时,并不能确定该对象的类型,只有PizzaStore子类实现工厂方法时,Pizza的具体对象类型才能确定。其中,PizzaStore类即为Creator,Pizza为Product,PizzaStore的子类为ConcreteCreator,Pizza的子类为ConcreteProduct。
orderPizza()方法中使用的Pizza类型是抽象的,所以它不清楚Pizza的具体类型,这就是所谓的 解耦
工厂方法:定义一个创建对象的接口,但由子类决定要实例化哪一个类。工厂方法将类的实例化推迟到子类

1
2
// 工厂方法的一般形式,让子类处理类的创建,并必须返回一个产品。
abstract Product factoryMethod(String type)

通过工厂方法对象的使用者与对象的具体创建分割开来。

一个工厂方法与其使用者的联合,可以视为一个框架;
此外,工厂方法将创建对象的方法封装到每一个创建者中,这样的做法也可视为一个框架。

  • 工厂模式与简单工厂的区别
    每一个ConcreteCreator看起来都有点像一个简单工厂,但其实有很大区别。在工厂模式中,工厂方法扩展自所有Creator共有的基类中的抽象方法,由每一个ConreteCreator负责实现,而简单工厂中的工厂,只是Creator中使用的对象。简单工厂中,将所有的事情放在一个地方全部实现,因此其代码缺乏弹性。
    而工厂模式中,通过一个框架的形式,让子类去决定如何实现。而简单工厂仅是封装了对象的创建过程,却无法变更正在创建的产品。(其实这句话没能完全理解,可能对运行时还是没有很透彻吧)

  • 工厂方法不一定要抽象,可以存在一个默认实现的工厂方法,由它实现默认生产的产品。

依赖倒置原则

要依赖抽象,不要依赖具体类
不能让高层组件依赖底层组件,两者都应该依赖抽象
例如在工厂方法模式中,ConcreteProduct类和Creator都依赖于Product抽象类。而一般的OO思想是Creator类依赖于ConcreteProduct,因为ConcreteProduct都在Creator中被实例化,但在工厂模式中就都依赖于抽象的Product,就是依赖倒置。因此工厂模式也是遵循对象倒置的重要手段。

  • 依赖倒置原则的建议
    1. 变量不可以持有具体类的引用(使用工厂方法可以实现这一点
    2. 不要让类派生自具体类(这样会依赖一个具体类
    3. 不要覆盖基类的方法(基类中已经实现的方法,应该被子类共享

抽象工厂模式

创建一个接口,用于创建相关或依赖对象的家族,而不需要指定具体类。允许用户通过一组接口方法来创建一组对象,而不需要关心实际创建的对象的具体类,使得客户与创建的具体对象解耦
抽象工厂的方法经常使用工厂方法实现,由此也能看出,抽象工厂一般用于实现一组对象,工厂方法一般用于实现一种对象

  • 抽象工厂与工厂方法的区别
    工厂方法通过继承创建对象,而抽象工厂使用的是对象的组合。这是说工厂方法创建具体的对象,将该对象实例化进行包装,需要继承扩展一个基类,实现其中的工厂方法。而抽象工厂需要提供一个交代了产品中的对象的生产方法的接口实现类,通过该类实现整个产品家族的创建,以这种方式实现用户与实际产品的解耦。而工厂方法不需要提供一个接口,只需要实现一个工厂方法就好了。
    其实只要认真研读,书中的思路还是很清晰的,只能怪自己第一遍看的不仔细吧…=_=。

单例模式

关于单例模式大概是目前我用过的比较清晰地设计模式,因为单例模式其实很好理解,就是对外隐藏自身的构造方法,只在类的内部初始化一个唯一的实例变量并提供一个外部获取该实例的方法,实现类似全局变量的效果且在代码中只有一个唯一的对象实例
当然我也了解到,我一般使用的都还是最为基础的单例实现,关于单例模式还有很多的实现平时很少使用,如懒汉单例,线程安全的单例等。
关于单例模式的使用场景,书中举例有:线程池,缓存,对话框,偏好设置或注册表的对象,日志,设备驱动或网络连接的对象等。
书中的定义为:单件模式确保一个类只有一个实例,并提供一个全全局的访问点。

懒汉式实现

最经典的实现方式,也叫懒汉式实现,实现了一种延迟实例化的效果。

1
2
3
4
5
6
7
8
9
10
11
12
// 懒汉式单例
public class Singleton {
private static Singleton instance;
private Singleton() {}

public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}

该单例通过getInstance()方法获取到Singleton实例,每一次调用时判断实例对象是否为空,为空则初始化。因此,如果在代码中一直没有用到该单例,则该单例对象永远不会被初始化,即所谓的延迟实例化。

线程安全的懒汉式实现

当然这种实现方式在多线程下是不安全的,因为当两个线程同时执行getInstance()中时,同时判断instance为空从而分别创建了对象。
解决方法可以是使getInstance()方法同步,即加上synchronized修饰getInstance()。但这种解决方式的缺陷是,代码会在每次调用getInstance()时,因为同步机制消耗不必要的性能。但其实我们只需要对创建对象的过程进行同步。

急切实例化的饿汉式实现

饿汉式的实现方式,其实就是在类加载的过程中对instance进行实例化。这样实现的好处是,代码中任何时刻使用getInstance()获取单例时,单例都已经被初始化了。

在静态初始化器中创建单件,保证了线程的安全。
JVM保证在任何线程访问instance之前一定对其进行初始化。

双重校验锁式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 双重校验锁式
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}

public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null)
instance = new Singleton();
}
}
return instance;
}
}

该实现方式,只有在第一次调用时才会进行同步。且进入同步区以后还会对instance进行检查,还是为null时才进行对象的创建。该方式可以大大减少getInstance()时的时间消耗。
注:在Java1.5以前的jvm对volatile的实现会导致双重检查加锁的失效。因此如果对性能没有很大的要求,尽量不要使用该实现方式,因为较为繁琐还需要考虑Java的版本。

是否可以使用类的单件代替对象的单件,即设置一个类和字段都为静态的类?
如果该类可以自给自足且初始化过程不是很复杂,可以使用。但是静态初始化的控制权在Java手上,当涉及较多的类时,可能出现由于类的初始化次序导致的问题。

因为每一个类加载器都会定义一个命名空间,所以不同的类加载器可能会加载同一个类,使得程序中出现两个单件。解决方式是,自行指定类加载器,并指定同一个类加载器。

饿汉式实现

待续

未完待续…

总结设计模式

  • 愈往下读,愈发感觉继承,多态,封装这几个字更加厚重
  • 书中介绍的设计原则,也许比介绍的设计模式更加精辟重要,因为感觉所有的设计模式,隐约都存在着共通点,而这些共通点,就是设计原则!

未完 待续 (╯‵□′)╯︵┻━┻