深入浅出设计模式
深入浅出设计模式
整理自电子书,作者是 AI92;
作者是在20年前写的博客;
另附 UML讲解博客:https://blog.csdn.net/weixin_45994575/article/details/123757323
一、工厂模式
1.1 引子
话说十年前,有一个暴发户,他家有三辆汽车——Benz奔驰、Bmw宝马、Audi奥迪,还雇了司机为他开车。
不过,暴发户坐车时总是怪怪的:上Benz车后跟司机说“开奔驰车!”,坐上Bmw后他说“开宝马车!”,坐上Audi说“开奥迪车!”。你一定说:这人有病!直接说开车不就行了?!
而当把这个暴发户的行为放到我们程序设计中来时,会发现这是一个普遍存在的现象。
幸运的是,这种有病的现象在OO(面向对象)语言中可以避免了。下面就以Java语言为基础来引入我们本文的主题:工厂模式。
1.2 分类
工厂模式主要是为创建对象提供过渡接口,以便将创建对象的具体过程屏蔽隔离起来,达到提高灵活性的目的。
工厂模式在《Java与模式》中分为三类:
- 简单工厂模式(Simple Factory)
- 工厂方法模式(Factory Method)
- 抽象工厂模式(Abstract Factory)
这三种模式从上到下逐步抽象,并且更具一般性。
GOF在《设计模式》一书中将工厂模式分为两类:工厂方法模式(Factory Method)与抽象工厂模式(Abstract Factory)。将简单工厂模式(Simple Factory)看为工厂方法模式的一种特例,两者归为一类。
两者皆可,在本文使用《Java与模式》的分类方法。下面来看看这些工厂模式是怎么来“治病”的。
1.3 简单工厂模式
1.3.1 描述
简单工厂又称为静态工厂,它存在的目的很简单:定义一个用于创建对象的接口。
1.3.2 组成
-
工厂类角色
这是本模式的核心,含有一定的商业逻辑和判断逻辑。在 Java 中它往往由一个具体类实现。
-
抽象产品角色
它一般是具体产品继承的父类或者实现的接口。在 java 中由接口或者抽象类来实现。
-
具体产品角色
工厂类所创建的对象就是此角色的实例。在 java 中由一个具体类实现。
用类图来清晰地表示它们之间的关系
那么简单工厂模式怎么来使用呢?我们就以简单工厂模式来改造暴发户坐车的方式——现在暴发户只需要坐在车里对司机说句:”开车”就可以了。
1.3.3 代码
-
抽象产品角色
public interface Car { void drive(); }
-
具体产品角色
public class Audi implements Car{ @Override public void drive() { System.out.println("Driving Audi"); } } // benz, bmw都类似
-
工厂类角色
public class Driver { public static Car driveCar(String s) throws Exception { if (s.equalsIgnoreCase("Benz")) { return new Benz(); } else if (s.equalsIgnoreCase("Audi")) { return new Audi(); } else if (s.equalsIgnoreCase("Bmw")) { return new Bmw(); } throw new Exception(); } }
-
main
public class Magnate { public static void main(String[] args) { try { // Driver就是工厂类,根据指定的参数创建对象 Car car = Driver.driveCar("benz"); car.drive(); } catch (Exception e) { e.printStackTrace(); } } }
其中各个类的关系如下:
1.3.4 总结
首先,使用了简单工厂模式后,我们的程序不在“有病”,更加符合现实中的情况;而且客户端免除了直接创建产品对象的责任,而仅仅负责“消费”产品(正如暴发户所为)。
下面我们从开闭原则(对扩展开放;对修改封闭)上来分析下简单工厂模式:
- 当暴发户增加了一辆车的时候,只要符合抽象产品制定的合同,那么只要通知工厂类知道就可以被客户使用了(想要增加一个 Car的实现类,只需要修改 Driver的代码即可)。所以对产品部分来说,它是符合开闭原则的(对于 Car相关的类来说,符合开闭原则);
- 但是工厂部分好像不太理想,因为每增加一辆车,都要在工厂类中增加相应的业务逻辑或者判断逻辑,这显然是违背开闭原则的。可想而知对于新产品的加入,工厂类是很被动的。对于这样的工厂类(在我们的例子中是为司机师傅),我们称它为全能类或者上帝类(对于 Driver来说,不符合开闭原则)。
也就是说,每次有 Car实现类的变动,都需要修改 Driver类
1.4 工厂方法模型
1.4.1 描述
工厂方法模式去掉了简单工厂模式中工厂方法的静态属性,使得它可以被子类继承。这样在简单工厂模式里集中在工厂方法上的压力可以由工厂方法模式里不同的工厂子类来分担。
也就是说,原来只有一个非常牛逼的大工厂,现在是多家连锁工厂。
1.4.2 组成
-
抽象工厂角色
这是工厂方法模式的核心,它与应用程序无关。
是具体工厂角色必须实现的接口或者必须继承的父类。在 Java 中它由抽象类或者接口来实现。
-
具体工厂角色
它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。
-
抽象产品角色
它是具体产品继承的父类或者是实现的接口。在 Java 中一般有抽象类或者接口来实现。
-
具体产品角色
具体工厂角色所创建的对象就是此角色的实例。在 Java 中由具体的类来实现。
用类图来清晰的表示下的它们之间的关系:
工厂方法模式使用继承自抽象工厂角色的多个子类来代替简单工厂模式中的“上帝类”正如上面所说,这样便分担了对象承受的压力;
而且这样使得结构变得灵活起来——当有新的产品(即暴发户的汽车)产生时,只要按照抽象产品角色、抽象工厂角色提供的合同来生成,那么就可以被客户使用,而不必去修改任何已有的代码。可以看出工厂角色的结构也是符合开闭原则的!
1.4.3 代码
-
抽象工厂角色
public interface Driver { Car driveCar(); }
-
具体工厂角色
public class BenzDriver implements Driver { @Override public Car driveCar() { return new Benz(); } } // BmwDriver, AudiDriver也类似
-
main
public class Magnate { public static void main(String[] args) { try { Driver driver = new BenzDriver(); Car car = driver.driveCar(); car.drive(); } catch (Exception e) { e.printStackTrace(); } } }
1.4.4 总结
可以看出,由于工厂方法的加入,使得对象的数量成倍增长。当产品种类非常多时,会出现大量的与之对应的工厂对象,这不是我们所希望的。
为了避免这种情况,可以考虑使用 简单工厂模式 与 工厂方法模式 相结合的方式来减少工厂类:即对于产品树上类似的种类(一般是树的叶子中互为兄弟的)使用简单工厂模式来实现。
工厂方法模式仿佛已经很完美的对对象的创建进行了包装,使得客户程序中仅仅处理抽象产品角色提供的接口。那我们是否一定要在代码中遍布工厂呢?大可不必。也许在下面情况下你可以考虑使用工厂方法模式:
- 当客户程序不需要知道要使用对象的创建过程。
- 客户程序使用的对象存在变动的可能,或者根本就不知道使用哪一个具体的对象。
简单工厂模式与工厂方法模式真正的避免了代码的改动了?没有。
- 在简单工厂模式中,新产品的加入要修改工厂角色中的判断语句;
- 而在工厂方法模式中,要么将判断逻辑留在抽象工厂角色中,要么在客户程序中将具体工厂角色写死(就象上面的例子一样)。而且产品对象创建条件的改变必然会引起工厂角色的修改。
面对这种情况,Java 的反射机制与配置文件的巧妙结合突破了限制——这在Spring 中完美的体现了出来。
1.5 抽象工厂模式
1.5.1 描述
先来认识下什么是产品族: 位于不同产品等级结构中,功能相关联的产品组成的家族。还是让我们用一个例子来形象地说明一下吧。
图中的BmwCar 和 BenzCar 就是两个产品树(产品层次结构);而如图所示的BenzSportsCar 和BmwSportsCar 就是一个产品族。他们都可以放到跑车家族中,因此功能有所关联。同理BmwBussinessCar 和 BenzBussinessCar 也是一个产品族。
可以说,抽象工厂模式和工厂方法模式的区别就在于需要创建对象的复杂程度上。而且抽象工厂模式是三个里面最为抽象、最具一般性的。
抽象工厂模式的用意为:给客户端提供一个接口,可以创建多个产品族中的产品对象。
而且使用抽象工厂模式还要满足一下条件:
- 系统中有多个产品族,而系统一次只可能消费其中一族产品(有多种车,但一次只能开一辆车)。
- 同属于同一个产品族的产品一起使用。
1.5.2 组成
和工厂方法的如出一辙
-
抽象工厂角色
这是抽象工厂模式的核心,它与应用程序无关。
是具体工厂角色必须实现的接口或者必须继承的父类。在 Java 中它由抽象类或者接口来实现。
-
具体工厂角色
它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象。在 Java 中它由具体的类来实现。
-
抽象产品角色
它是具体产品继承的父类或者是实现的接口。在 Java 中一般有抽象类或者接口来实现。
-
具体产品角色
具体工厂角色所创建的对象就是此角色的实例。在 Java 中由具体的类来实现。
类图如下:
千万注意满足使用抽象工厂模式的条件!!!
1.5.3 代码
举一个如下的例子:
- 有手机和路由器两种产品,有华为和小米两种品牌,两种品牌都可以生产手机和路由器;
- 有手机和路由器两种产品,定义 两个接口;
- 小米和华为都可以生产这两种产品,所以有 4个实现类;
- 现在需要创建华为和小米的工厂类,先将工厂类进行抽象,里面有创建两个产品的方法,返回的是产品的接口类;
- 创建华为和小米的工厂实现类,继承工厂类接口,实现创建各自产品的方法;
- 客户端调用时,直接用工厂接口类创建需要的工厂,拿到对应的产品;
-
产品接口
public interface IPhoneProduct { /** * 开机 */ void start(); /** * 关机 */ void shutdown(); /** * 打电话 */ void callup(); /** * 发短信 */ void sendSMS(); }
public interface IRouterProduct { /** * 开机 */ void start(); /** * 关机 */ void shutdown(); /** * 打开 wifi */ void openwifi(); /** * 设置 */ void setting(); }
-
产品实现类
public class HuaweiPhone implements IPhoneProduct{ @Override public void start() { System.out.println("开启华为手机"); } @Override public void shutdown() { System.out.println("关闭华为手机"); } @Override public void callup() { System.out.println("华为手机打电话"); } @Override public void sendSMS() { System.out.println("华为手机发邮件"); } }
public class HuaweiRouter implements IRouterProduct{ @Override public void start() { System.out.println("开启华为路由器"); } @Override public void shutdown() { System.out.println("关闭华为路由器"); } @Override public void openwifi() { System.out.println("打开华为wifi"); } @Override public void setting() { System.out.println("设置华为路由器"); } }
public class XiaomiPhone implements IPhoneProduct{ @Override public void start() { System.out.println("开启小米手机"); } @Override public void shutdown() { System.out.println("关闭小米手机"); } @Override public void callup() { System.out.println("小米手机打电话"); } @Override public void sendSMS() { System.out.println("小米手机发邮件"); } }
public class XiaomiRouter implements IRouterProduct{ @Override public void start() { System.out.println("开启小米路由器"); } @Override public void shutdown() { System.out.println("关闭小米路由器"); } @Override public void openwifi() { System.out.println("打开小米wifi"); } @Override public void setting() { System.out.println("设置小米路由器"); } }
-
工厂接口
public interface IProductFactory { /** * 生产手机 * @return */ IPhoneProduct phoneProduct(); /** * 生产路由器 * @return */ IRouterProduct routerProduct(); }
-
工厂实现类
public class HuaweiFactory implements IProductFactory{ @Override public IPhoneProduct phoneProduct() { return new HuaweiPhone(); } @Override public IRouterProduct routerProduct() { return new HuaweiRouter(); } }
public class XiaomiFactory implements IProductFactory{ @Override public IPhoneProduct phoneProduct() { return new XiaomiPhone(); } @Override public IRouterProduct routerProduct() { return new XiaomiRouter(); } }
-
main
public class Client { public static void main(String[] args) { System.out.println("============小米产品============"); //创建小米工厂 IProductFactory xiaomiFactory = new XiaomiFactory(); //生产小米手机 IPhoneProduct xiaomiPhone = xiaomiFactory.phoneProduct(); xiaomiPhone.start(); xiaomiPhone.sendSMS(); //生产小米路由器 IRouterProduct xiaomiRouter = xiaomiFactory.routerProduct(); xiaomiRouter.openwifi(); xiaomiRouter.setting(); System.out.println("============华为产品============"); //创建华为工厂 IProductFactory huaweiFactory = new HuaweiFactory(); //生产华为手机 IPhoneProduct huaweiPhone = huaweiFactory.phoneProduct(); huaweiPhone.start(); huaweiPhone.sendSMS(); //生产华为路由器 IRouterProduct huaweiRouter = huaweiFactory.routerProduct(); huaweiRouter.openwifi(); huaweiRouter.setting(); } }
1.5.4 总结
-
此时,拓展一个产品族是非常困难的。例如产品族中新增一个笔记本电脑,也就是说华为和小米现在可以生产电脑了,如下图所示(黄色字体为新增一个产品族需要做的事),对顶层的工厂接口类也要修改,这是非常麻烦的;
-
但如果仅仅是拓展一个手机,也就是说新增一个品牌的手机,则不需要修改原来的代码,符合【开闭原则】
-
优点:一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象(将一个系列的产品统一一起创建);
-
缺点:
- 产品族扩展非常困难,要增加一个系列的某一产品,既要修改工厂抽象类里加代码,又修改具体的实现类里面加代码;
- 增加了系统的抽象性和理解难度;
-
适用场景:
- 一系列相关产品对象(属于同一产品族)一起创建时需要大量的重复代码;
- 提供一个产品类的库,所有的产品以同样的接口出现,从而使得客户端不依赖于具体的实现;
-
抽象工厂模式符合依赖抽象原则
- 创建对象实例时,不要直接 new一个对象, 而是把创建对象的动作放在一个工厂的方法中;
- 不要让类继承具体类,而是继承抽象类或者是实现接口;
- 不要覆盖基类中已经实现的方法;
二、单例模式
2.1 引子
单例模式是设计模式中使用很频繁的一种模式,在各种开源框架、应用系统中多有应用。
2.2 定义与结构
单例模式又叫做单态模式或者单件模式。在GOF 书中给出的定义为:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式中的“单例”通常用来代表那些本质上具有唯一性的系统组件(或者叫做资源)。比如文件系统、资源管理器等等。
单例模式的目的就是要控制特定的类只产生一个对象,当然也允许在一定情况下灵活的改变对象的个数。那么怎么来实现单例模式呢?
- 一个类的对象的产生是由类构造函数来完成的,如果想限制对象的产生,就要将构造函数变为私有的(至少是受保护的),使得外面的类不能通过引用来产生对象;
- 同时为了保证类的可用性,就必须提供一个自己的对象以及访问这个对象的静态方法。
其实单例模式在实现上是非常简单的——只有一个角色,而客户则通过调用类方法来得到类的对象。
其类图如下:
单例模式可分为有状态的和无状态的。有状态的单例对象一般也是可变的单例对象,多个单态对象在一起就可以作为一个状态仓库一样向外提供服务。没有状态的单例对象也就是不变单例对象,仅用做提供工具函数。
2.3 实现
2.3.1 饿汉式
public class StarveSingleton {
/**
* 自己内部定义一个实例
*/
private static final StarveSingleton INSTANCE = new StarveSingleton();
/**
* 私有化构造
*/
private StarveSingleton() {}
public static StarveSingleton getInstance() {
return INSTANCE;
}
}
2.3.2 懒汉式
public class LazySingleton {
private static LazySingleton INSTANCE = null;
/**
* 私有化构造
*/
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
2.3.2 两种方式对比
-
首先他们的构造函数都是私有的,彻底断开了使用构造函数来得到类的实例的通道,但是这样也使得类失去了多态性(大概这就是为什么有人将这种模式称作单态模式)。
-
在第二种方式中,对静态工厂方法进行了同步处理,原因很明显——为了防止多线程环境中产生多个实例;而在第一种方式中则不存在这种情况。
-
在第二种方式中将类对自己的实例化延迟到第一次被引用的时候。而在第一种方式中则是在类被加载的时候实例化,这样多次加载会照成多次实例化。但是第二种方式由于使用了同步处理,在反应速度上要比第一种慢一些。
-
在 《java与模式》书中提到,就java语言来说,第一种方式更符合java语言本身的特点。
-
以上两种实现方式均失去了多态性,不允许被继承。还有另外一种灵活点的实现,将构造函数设置为受保护的,这样允许被继承产生子类。这种方式在具体实现上又有所不同,可以将父类中获得对象的静态方法放到子类中再实现;也可以在父类的静态方法中进行条件判断来决定获得哪一个对象;
-
在GOF中认为最好的一种方式是维护一张存有对象和对应名称的注册表(可以使用HashMap来实现)。下面的实现参考《java与模式》采用带有注册表的方式。
public class CachedSingleton { /** * 用来存放对应关系 */ private static HashMap<String, CachedSingleton> REGISTRY = new HashMap<>(); private static CachedSingleton s = new CachedSingleton(); protected CachedSingleton() { } public static CachedSingleton getInstance(String name) { if (name == null) { name = "Singleton"; } else if (REGISTRY.get(name) == null) { try { REGISTRY.put(name, (CachedSingleton) Class.forName(name).newInstance()); } catch (Exception e) { e.printStackTrace(); } } return REGISTRY.get(name); } }
2.4 单例模式邪恶论
单例模式在 Java 中的使用存在很多陷阱和假象,这使得没有意识到单例模式使用局限性的你在系统中布下了隐患……
-
多个虚拟机
当系统中的单例类被拷贝运行在多个虚拟机下的时候,在每一个虚拟机下都可以创建一个实例对象。在使用了EJB、JINI、RMI技术的分布式系统中,由于中间件屏蔽掉了分布式系统在物理上的差异,所以对你来说,想知道具体哪个虚拟机下运行着哪个单例对象是很困难的。
因此,在使用以上分布技术的系统中,应该避免使用存在状态的单例模式,因为一个有状态的单例类,在不同虚拟机上,各个单例对象保存的状态很可能是不一样的,问题也就随之产生。而且在EJB中不要使用单例模式来控制访问资源,因为这是由EJB容器来负责的。在其它的分布式系统中,当每一个虚拟机中的资源是不同的时候,可以考虑使用单例模式来进行管理。
-
多个类加载器
当存在多个类加载器加载类的时候,即使它们加载的是相同包名,相同类名甚至每个字节都完全相同的类,也会被区别对待的。因为不同的类加载器会使用不同的命名空间(namespace)来区分同一个类。因此,单例类在多加载器的环境下会产生多个单例对象。
也许你认为出现多个类加载器的情况并不是很多。其实多个类加载器存在的情况并不少见。在很多J2EE服务器上允许存在多个servlet引擎,而每个引擎是采用不同的类加载器的;浏览器中applet小程序通过网络加载类的时候,由于安全因素,采用的是特殊的类加载器,等等。
这种情况下,由状态的单例模式也会给系统带来隐患。因此除非系统由协调机制,在一般情况下不要使用存在状态的单例模式。
-
错误的同步处理
在使用上面介绍的懒汉式单例模式时,同步处理的恰当与否也是至关重要的。不然可能会达不到得到单个对象的效果,还可能引发死锁等错误。因此在使用懒汉式单例模式时一定要对同步有所了解。不过使用饿汉式单例模式就可以避免这个问题。
-
子类破坏了对象控制
由于类构造函数变得不再私有,就有可能失去对对象的控制。这种情况只能通过良好的文档来规范。
-
串行化
为了使一个单例类变成可串行化的,仅仅在声明中添加“implements Serializable”是不够的。因为一个串行化的对象在每次反串行化的时候,都会创建一个新的对象,而不仅仅是一个对原有对象的引用。为了防止这种情况,可以在单例类中加入readResolve方法。关于这个方法的具体情况请参考《Effective Java》一书第57条建议。
其实对象的串行化并不仅局限于上述方式,还存在基于XML格式的对象串行化方式。这种方式也存在上述的问题,所以在使用的时候要格外小心。
2.5 多种懒汉式单例汇总
-
类上加 synchronized
public class LazySimpleSingleton { private static LazySimpleSingleton INSTANCE = null; private LazySimpleSingleton() {} public synchronized static LazySimpleSingleton getInstance() { //加上空判断保证初只会初始化一次 if (INSTANCE == null) { INSTANCE = new LazySimpleSingleton(); } return INSTANCE; } }
-
双检锁
public class LazySimpleSingleton { private static volatile LazySimpleSingleton INSTANCE = null; private LazySimpleSingleton() {} public static LazySimpleSingleton getInstance() { //加上空判断保证初只会初始化一次 if (INSTANCE == null) { synchronized (LazySimpleSingleton.class) { if (INSTANCE == null) { INSTANCE = new LazySimpleSingleton(); } } } return INSTANCE; } }
-
静态内部类
public class LazyInnerClassSingleton { private LazyInnerClassSingleton() {} public static final LazyInnerClassSingleton getInstance() { // 返回内部类中的对象 return LazyHolder.LAZY; } // 静态内部类中定义单例对象 private static class LazyHolder { private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton(); } }
但是,此时通过反射仍然会破坏单例
public class LazyInnerClassTest { public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Class<?> clazz = LazyInnerClassSingleton.class; Constructor c = clazz.getDeclaredConstructor(null); c.setAccessible(true); Object o1 = c.newInstance(); Object o2 = LazyInnerClassSingleton.getInstance(); System.out.println(o1 == o2); // false } }
并且,序列化和反序列化也会破坏单例
-
枚举
public enum EnumSingleton { INSTANCE; public static EnumSingleton getInstance() { return INSTANCE; } }
绝对安全,上述两种问题都不会发生;但是不能满足继承等场景
-
ThreadLocal
public class ThreadLocalSingleton { private ThreadLocalSingleton() { } private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>() { @Override protected ThreadLocalSingleton initialValue() { return new ThreadLocalSingleton(); } }; public static ThreadLocalSingleton getInstance() { return threadLocalInstance.get(); } }
-
CAS
public class CASSingleton { private CASSingleton() {} private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<>(); public static CASSingleton getInstance() { while (true) { CASSingleton cur = INSTANCE.get(); if (cur != null) { return cur; } cur = new CASSingleton(); if (INSTANCE.compareAndSet(null, cur)) { return cur; } } } }
三、建造模式
3.1 引子
前几天陪朋友去装机店攒了一台电脑,看着装机工在那里熟练的装配着机器,不禁想起来了培训时讲到的建造模式。作为装机工,他们不用管你用的CPU是Intel还是AMD,也不管你的显卡是2000千大元还是白送的,都能三下五除二的装配在一起——一台PC就诞生了!当然对于客户来说,你也不知道太多关于PC组装的细节。
这和建造模式是多么的相像啊!
3.2 定义与结构
GOF 给建造模式的定义为:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
可以将建造模式的精髓概括为:将构造复杂对象的过程和对象的部件解耦。这是对降低耦合、提高可复用性精神的一种贯彻。其实这种精神贯彻在GOF几乎所有的设计模式中。
就像攒电脑一样,不管什么品牌的配件,只要是兼容的就可以装上;同样,一样的配件,可以有好几种组装的方式。这是对降低耦合、提高可复用性精神的一种贯彻!
当要生成的产品有着复杂的内部结构时——比如由多个对象组成;而系统中对此产品的需求将来可能要改变产品对象的内部结构的构成,比如说产品的一些属性限制由一个小对象组成,而更改后的型号可能需要 N个小对象组成;而且不能将产品的内部构造完全暴露给客户程序,一是为了可用性,二是为了安全性。
满足上面的设计环境就可以考虑使用建造者模式了。
3.3 组成
-
抽象建造者角色
这个角色用来规范产品对象的各个组成成分的建造。一般而言,此角色独立于应用程序的商业逻辑。
-
具体建造者角色
担任这个角色的是于应用程序紧密相关的类,它们在指导者的调用下创建产品实例。
这个角色在实现抽象建造者角色提供的方法的前提下,达到完成产品组装,提供成品的功能。
-
指导者角色
调用具体建造者角色以创建产品对象。
指导者并没有产品类的具体知识,真正拥有产品类的具体知识的是具体建造者对象。
-
产品角色
建造中的复杂对象。
它要包含那些定义组件的类,包括将这些组件装配成产品的接口。
以下为类图:
- 首先客户程序创建一个指导者对象,一个建造者角色,并将建造者角色传入指导者对象进行配置。
- 然后,指导者按照步骤调用建造者的方法创建产品。
- 最后客户程序从建造者或者指导者那里得到产品。
从建造模式的工作流程来看,建造模式将产品的组装“外部化”到了建造者角色中来。这是和任何正规的工厂模式不一样的——产品的创建是在产品类中完成的。
3.4 代码实现
将《Think in Patterns with Java》中的例子放到这里权且充个门面。
媒体可以存在不同的表达形式,比如书籍、杂志和网络。这个例子表示不同形式的媒体构造的步骤是相似的,所以可以被提取到指导者角色中去。
-
抽象建造者角色
public class MediaBuilder { public void buildBase() { } public void addMediaItem(MediaItem item) { } public Media getFinishedMedia() { return null; } }
-
具体建造者角色
public class BookBuilder extends MediaBuilder { private Book b; @Override public void buildBase() { System.out.println("Building book framework"); b = new Book(); } @Override public void addMediaItem(MediaItem item) { System.out.println("Adding chapter " + item); b.add(item); } @Override public Media getFinishedMedia() { return b; } }
public class MagazineBuilder extends MediaBuilder { private Magazine b; @Override public void buildBase() { System.out.println("Building Magazine framework"); b = new Magazine(); } @Override public void addMediaItem(MediaItem item) { System.out.println("Adding Article " + item); b.add(item); } @Override public Media getFinishedMedia() { return b; } }
public class WebSiteBuilder extends MediaBuilder { private Website b; @Override public void buildBase() { System.out.println("Building Website framework"); b = new Website(); } @Override public void addMediaItem(MediaItem item) { System.out.println("Adding Website " + item); b.add(item); } @Override public Media getFinishedMedia() { return b; } }
-
指导者角色
public class MediaDirector { private MediaBuilder mb; public MediaDirector(MediaBuilder mb) { // 具有策略模式相似的特征 this.mb = mb; } public Media produceMedia(List<MediaItem> input) { mb.buildBase(); for (MediaItem o : input) { mb.addMediaItem(o); } return mb.getFinishedMedia(); } }
-
产品
-
直接的产品类
-
接口
public class Media extends ArrayList {}
-
实现
public class Book extends Media{ }
public class Magazine extends Media{ }
public class Website extends Media{ }
-
-
组成产品的类
-
接口
public class MediaItem { private String s; public MediaItem(String s) { this.s = s; } @Override public String toString() { return s; } }
-
实现
public class Article extends MediaItem{ public Article(String s) { super(s); } }
public class Chapter extends MediaItem{ public Chapter(String s) { super(s); } }
public class WebItem extends MediaItem{ public WebItem(String s) { super(s); } }
-
-
-
main
public class MainApp { public static List<MediaItem> input = Arrays.asList(new MediaItem("item01"), new MediaItem("item02"), new MediaItem("item03"), new MediaItem("item04")); public static void main(String[] args) { testBook(); testMagazine(); testWebsite(); } private static void testWebsite() { MediaDirector director = new MediaDirector(new WebSiteBuilder()); Media website = director.produceMedia(input); System.out.println("website: " + website); } private static void testMagazine() { MediaDirector director = new MediaDirector(new MagazineBuilder()); Media magazine = director.produceMedia(input); System.out.println("magazine: " + magazine); } private static void testBook() { MediaDirector director = new MediaDirector(new BookBuilder()); Media book = director.produceMedia(input); System.out.println("book: " + book); } }
注意:
- 在实现的时候,抽象建造角色提供的接口必须足够普遍,以适应不同的具体建造角色。
- 对于一个具体的建造角色来说可能某个步骤是不需要的,可以将此接口实现为空。
- 多个产品之间可能没有太多的共同点,可以提供一个标示接口作为抽象产品角色;也可以不提供抽象产品角色,这时要将提供产品的接口从抽象建造角色里面去掉,不然就会编译出问题。
3.5 应用优点
- 建造模式可以使得产品内部的表象独立变化。
- 在原来的工厂方法模式中,产品内部的表象是由产品自身来决定的;
- 而在建造模式中则是“外部化”为由建造者来负责;
- 这样定义一个新的具体建造者角色就可以改变产品的内部表象,符合“开闭原则”。
- 建造模式使得客户不需要知道太多产品内部的细节。它将复杂对象的组建和表示方式封装在一个具体的建造角色中,而且由指导者来协调建造者角色来得到具体的产品实例。
- 每一个具体建造者角色是毫无关系的。
- 建造模式可以对复杂产品的创建进行更加精细的控制。
- 产品的组成是由指导者角色调用具体建造者角色来逐步完成的,所以比起其它创建型模式能更好的反映产品的构造过程。
3.6 扩展
- 建造模式中很可能要用到组成成品的各种组件类,对于这些类的创建可以考虑使用工厂方法或者原型模式来实现,在必要的时候也可以加上单例模式来控制类实例的产生。但是要坚持一个大前提就是要使引入的模式给你的系统带来好处,而不是臃肿的结构。
- 建造模式在得到复杂产品的时候可能要引用多个不同的组件,在这一点上来看,建造模式和抽象工厂模式是相似的。
可以从以下两点来区分两者:- 创建模式着重于逐步将组件装配成一个成品并向外提供成品,而抽象工厂模式着重于得到产品族中相关的多个产品对象;
- 抽象工厂模式的应用是受限于产品族的,建造模式则不会。
- 由于建造模式和抽象工厂模式在实现功能上相似,所以两者使用的环境都比较复杂并且需要更多的灵活性。
- 建造模式中可能要使用到不同“大小”的组件类,因此这时也经常和合成模式在一起使用。
- 组合模式中的树枝构件角色(COmposite)往往是由多个树叶构件角色(Leaf)组成,因此树枝构件角色的产生可以由建造模式来担当。
四、原型模式
4.1 引子
古人云:书非借不能读也。
我深谙古人教诲,更何况现在IT书籍更新快、价格贵、质量水平更是参差不齐,实在不忍心看到用自己的血汗钱买的书不到半年就要被淘汰,更不想供养使用金山快译、词霸等现代化工具的翻译们。于是我去书店办了张借书卡,这样便没有了后顾之忧了——书不好我可以换嘛!
但是,借书也有不爽的地方,就是看到有用或者比较重要的地方,不能在书旁标记下来。一般我会将这页内容复印下来,这样作为我自己的东西就可以对其圈圈画画,保存下来了。
在软件设计中,往往也会遇到类似或者相似的问题,GOF 将这种解决方案叫作原型模式。也许原形模式会给你一些新的启迪。
4.2 定义与结构
原型模式属于对象创建模式,GOF给它的定义为:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
在Java 中提供了clone() 方法来实现对象的克隆,所以Prototype模式实现变得简单许多。注:clone()方法的使用,请参考《Thinking in Java》或者《Effective Java》,对于许多原型模式中讲到的浅克隆、深克隆,本文不作为谈论话题。
但是在什么情况下使用原型模式最为合适呢?它能给我们带来什么好处呢?这些问题不是看上一两行教学代码能够解决的。在这里我会尽力着重来讨论这两个问题。
我认为原型模式应该是为了弥补工厂方法模式的弱点而产生,在前面已经讲过——工厂方法模式对于适应产品变化方面比较弱,添加删除一种产品都会额外引起工厂类的相应变化。那么在原型模式中,是如何来适应这种变化的呢?往下看……
4.3 组成
-
客户角色
让一个原型克隆自己来得到一个新对象
-
抽象原型角色
实现了自己的clone方法,扮演这种角色的类通常是抽象类,且它具有许多具体的子类。
-
具体原型角色
被复制的对象,为抽象原型角色的具体子类。
类图如下:
按照定义客户角色不仅要负责使用对象,而且还要负责对象原型的生成和克隆。这样造成客户角色分工就不是很明确,所以我们把对象原型生成和克隆功能单拿出来放到一个原型管理器中。原型管理器维护了已有原型的清单。
客户在使用时会向原型管理器发出请求,而且可以修改原型管理器维护的清单。这样客户不需要编码就可以实现系统的扩展。
类图如下:
4.4 代码实现
对于抽象原型角色和具体原型角色,它们就是一个继承或者实现关系,没有什么好讲的,记住实现好clone方法就好了。
那么客户是怎么来使用这些角色的对像的呢?来看两行简要的代码
// 先new一个具体原型角色
Prototype p = new ConcretePrototype();
……
// 使用原型p克隆出一个新对象p1
Prototype p1 = (Prototype)p.clone();
- 原型模式使用 clone能够动态的抽取当前对象运行时的状态并且克隆到新的对象中,新对象就可以在此基础上进行操作而不损坏原有对象;
- 而new只能得到一个刚初始化的对象,而在实际应用中,这往往是不够的。
在实际运用中,客户程序和原型角色之间往往存在一个原型管理器。因此,创建原型角色、拷贝原型角色就与客户程序分离开来了。这时才能真正的体会到原型模式带给我们的效果。
// 使用原型管理器后,客户获得对象的方式
Prototype p1 = PrototypeManager.getManager().getPrototype("ConcretePrototype");
当客户自定义自己的类别的时候,同时向原型管理器注册一个原型对象,而使用的类只需要根据客户的需要来从原型管理器中得到一个对象就可以了。这样就使得功能扩展变得容易些。
原型模式还体现了OO中的多态性——当别人需要我的副本时,我只管克隆我自己,但是我的具体类型我并不关心,这一切要到运行时才能知道。
至于上面提到的原型管理器的实现,简单来说就是对原型清单的维护。可以考虑一下几点:
- 要保存一个原型对象的清单,我们可以使用一个 HashMap来实现,使原型对象和它的名字相对应;
- 原型管理器只需要一个就够了,所以可以使用单例模式来实现控制;
- 实现得到、注册、删除原型对象的功能只是对 HashMap的对应操作而已。
public class PrototypeManager {
/**
* 单例
*/
private static PrototypeManager pm;
/**
* 缓存
*/
private Map<String, Prototype> prototypes = null;
/**
* 私有构造
*/
private PrototypeManager() {
prototypes = new HashMap<>();
}
public static synchronized PrototypeManager getInstance() {
if (pm == null) {
pm = new PrototypeManager();
}
return pm;
}
public void register(String name, Prototype prototype) {
prototypes.put(name, prototype);
}
public void unregister(String name) {
prototypes.remove(name);
}
public Prototype getPrototype(String name) throws CloneNotSupportedException {
if (prototypes.containsKey(name)) {
return (Prototype) prototypes.get(name).clone();
}
Prototype object = null;
try {
object = (Prototype) Class.forName(name).newInstance();
register(name, object);
} catch (Exception e) {
e.printStackTrace();
}
return object;
}
}
当客户自定义新的产品对象时,同时向原型管理器注册一个原型对象,而使用的类只需要根据客户的需要来从原型管理器中得到一个对象就可以了。
原型模式与其它创建型模式有着相同的特定:它们都将具体产品的创建过程进行包装,使得客户对创建不可知。就像上面的例子中一样,客户程序仅仅知道一个抽象产品的接口。
通过增加或删除原型管理器中注册的对象,可以比其它创建型模式更方便地在运行时增加或删除产品。
如果一个对象的创建总是由几种固定组件不同方式组合而成,如果对象之间仅仅实例属性不同,那么将不同情况的对象缓存起来,直接克隆使用回避采用传递参数重新 new一个对象要更快。
原型模式与工厂模式很像,它去掉了抽象工厂模式或者工厂方法模式那样繁多的子类。因此可以说原型模式就是在工厂模式的基础上加入了克隆方法。
任何设计模式都是存在缺陷的,原型模式的主要缺陷就是每个原型都必须含有 clone方法,在已有类的基础上来添加 clone操作是比较困难的;而且当内部包括一些不支持 clone或者循环引用的对象时,就更加困难了
4.5 总结
由于clone方法在java实现中有着一定的弊端和风险,所以clone方法是不建议使用的。因此很少能在java应用中看到原型模式的使用。但是原型模式还是能够给我们一些启迪。
五、适配器模式
5.1 引子
昨天在给新买的MP3充电的时候,发现这款MP3播放器只提供了USB接口充电的方式,而它所配备的充电器无法直接给USB接口充电,聪明的厂商为充电器装上了一个USB接口转换器解决了问题。
这个USB接口转接器正是我们今天要谈到的适配器。而在软件开发中采用类似于上面方式的编码技巧被称为适配器模式。
5.2 定义和结构
《设计模式》一书中是这样给适配器模式定义的:将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。由引子中给出的例子可知,这个定义描述的功能和现实中的适配器的功能是一致的。
可能你还是不太明白为什么要使用适配器模式。我们来举个例子也许能更直接的解除你的疑惑。
比如,在一个画图的小程序中,你已经实现了绘制点、直线、方块等图形的功能。而且为了让客户程序在使用的时候不用去关心它们的不同,还使用了一个抽象类来规范这些图形的接口。现在你要来实现圆的绘制,这时你发现在系统其他的地方已经有了绘制圆的实现。在你庆幸之余,发现系统中已有的方法和你在抽象类中规定的方法名称不一样!这可怎么办?修改绘制圆的方法名,就要去修改所有使用它的地方;修改你的抽象类的方法名,也要去修改所有图形的实现方法以及已有的引用。还有其它的方法没有?那就是适配器模式了。
可以看出使用适配器模式是为了在面向接口编程中更好的复用。如果你的系统中没有使用到面向接口编程,没有使用到多态,我想大概也不会使用到适配器模式。
5.3 组成
-
目标角色 Target
定义Client使用的接口
-
被适配角色 Adaptee
这个角色有一个已存在并使用了的接口,而这个接口是需要我们适配的。
-
适配器角色 Adapter
这个适配器模式的核心。它将被适配角色已有的接口转换为目标角色希望的接口。
类图如下:
5.4 代码实现
接着上面举的画图程序的例子,先来看看在添加绘制圆的需求前的类结构:
添加了圆的绘制后的类结构:
可以看出Shape、Circle和TextCircle 三者的关系是和标准适配器模式中Target、Adapter、Adaptee三者的关系相对应的。我们只关心这个画图程序中是怎么来使用适配器模式的。
// adapter
public class Circle extends Shape{
// adaptee
private TextCircle tc;
public Circle() {
this.tc = new TextCircle();
}
// 适配的方法
@Override
public void display() {
tc.displayId();
}
}
其实在适配器角色中不仅仅可以完成接口转换的过程,而且还可以对其功能进行改进和扩充,当然这就不属于适配器模式描述的范围内了。
它和代理模式的区别在于,代理模式应用的情况是不改变接口命名的,而且是对已有接口功能的一种控制;而适配器模式则强调接口转换。
5.5 总结
在java中有一种叫做“缺省适配模式”的应用,它和我们所讲的适配器模式是完全的两种东西。
缺省适配模式是为一个接口提供缺省的实现,这样子类型就可以从缺省适配模式中进行扩展,避免了从原有接口中扩展时要实现一些自己不关心的接口。在java.awt.event中的XXXAdapter就是它的很好的例子。
六、桥梁模式
6.1 引子
桥梁模式是一个功能非常强大而且适用于多种情况的模式。
6.2 定义与结构
GOF在《设计模式》中给桥梁模式的定义为:将抽象部分与它的实现部分分离,使它们都可以独立地变化。这里的抽象部分和实现部分不是我们通常认为的父类与子类、接口与实现类的关系,而是组合关系。也就是说,实现部分是被抽象部分调用,以用来完成(实现)抽象部分的功能。
在《Thinking in Patterns with Java》一书中,作者将抽象部分叫做“front-end”(权且翻译为“前端”),而实现部分叫做“back-end”(后端)。这种叫法要比抽象实现什么的好理解多了。
在系统设计中,总是充满了各种变数,这是防不胜防的。面对这样的变动,你只能去不停地修改设计和代码,并且重复测试。
那采取什么样的方法可以较好地解决变化带给系统的影响呢?你可以分析变化的种类,将不变的框架使用抽象类定义出来,然后将变化的内容使用具体的子类来分别实现。这样面向客户的只是一个抽象类,这种方式可以较好地避免为抽象类中现有的接口添加新的实现所带来的影响,缩小了变化带来的影响,但可能会导致子类数量的爆炸,并且在使用的时候不太灵活。
当这颗继承树上一些子树存在了类似的行为,这意味着这些子树中存在了几乎重复的功能代码。这时我们不妨将这些行为提取出来,也采用接口的形式提供出来,然后以组合的方式将服务提供给原来的子类。这样就达到了前端和被使用的后端独立的变化,而且还达到了后端的重用。
这就是桥梁模式的诞生。
6.3 组成
-
抽象角色 Abstraction
它定义了抽象类的接口而且维护着一个指向实现(Implementor)角色的引用。
-
**精确抽象角色 **RefinedAbstraction
实现并扩充由抽象角色定义的接口。
-
**实现角色 **Implementor
给出了实现类的接口,这里的接口与抽象角色中的接口可以不一致。
-
**具体实现角色 **ConcreteImplementor
给出了实现角色定义接口的具体实现。
类图如下:
目前 awt中就是使用的桥梁模式
6.4 代码实现
-
前端抽象角色
public class Abstraction { // 维护着一个指向实现的引用,这个引用就是 桥梁 private Implementation implementation; public Abstraction(Implementation imp) { this.implementation = imp; } public void service01() { implementation.facility1(); implementation.facility2(); } public void service02() { implementation.facility2(); implementation.facility3(); } public void service03() { implementation.facility1(); implementation.facility2(); implementation.facility4(); } public Implementation getImplementation() { return implementation; } }
-
前端精确角色
public class ClientService1 extends Abstraction{ public ClientService1(Implementation imp) { super(imp); } // 使用抽象角色提供的方法组合起来完成某项功能 // 这就是为什么称之为精确抽象角色(修正抽象角色) public void serviceA() { service01(); service02(); } public void serviceB() { service03(); } }
public class ClientService2 extends Abstraction{ public ClientService2(Implementation imp) { super(imp); } public void serviceE() { getImplementation().facility3(); } }
-
后端实现角色
public interface Implementation { // 定义一些接口 void facility1(); void facility2(); void facility3(); void facility4(); }
-
后端具体实现角色
public class ConcreteImplementation implements Implementation{ @Override public void facility1() { } @Override public void facility2() { } @Override public void facility3() { } @Override public void facility4() { } }
在桥梁模式中,不仅实现部分和抽象部分(前后端)所提供的接口可以完全不一样;而且实现部分内部、抽象部分内部的接口也完全可以不一样。但实现部分要提供类似的功能才行。
6.5 总结
由上面我们分析得来的桥梁模式,可以看出来桥梁模式应该适用于以下环境:
- 当你的系统中有多个地方要使用到类似的行为,或者是多个类似行为的组合时,可以考虑使用桥梁模式来提高重用,并减少因为行为的差异而产生的子类。
- 系统中某个类的行为可能会有几种不同的变化趋势,为了有效的将变化封装,可以考虑将类的行为抽取出来。
- 当然上面的情况也可以是这样,行为可能要被不同相似类使用,也可以考虑使用桥梁模式来实现。
桥梁模式使用了低耦合性的组合代替继承,使得它具备了不少好处:
- 将可能变化的部分单独封装起来,使得变化产生的影响最小,不用编译不必要的第代码。
- 抽象部分和实现部分可以单独的变动,并且每一部分的扩充都不会破坏桥梁模式搭起来架子。
- 对于客户程序来说,你的实现细节是透明的。
Bruce Eckel 在《Thinking in patterns with Java》中提到,可以把桥梁模式当作帮助你编码前端和后端独立变化的框架。
七、组合模式
7.1 引子
在大学的数据结构这门课上,树是最重要的章节之一。还记得树是怎么定义的吗?树(Tree)是n(n≥0)个结点的有限集T,T为空时称为空树,否则它满足如下两个条件:
- 有且仅有一个特定的称为根(Root)的结点;
- 其余的结点可分为m(m≥0)个互不相交的子集Tl,T2,…,Tm,其中每个子集本身又是一棵树,并称其为根的子树(SubTree)。
上面给出的递归定义刻画了树的固有特性:一棵非空树是由若干棵子树构成的,而子树又可由若干棵更小的子树构成。而这里的子树可以是叶子也可以是分支。
今天要学习的组合模式就是和树型结构以及递归有关系。
7.2 定义与结构
组合(Composite)模式的其它翻译名称也很多,比如合成模式、树模式等等。在《设计模式》一书中给出的定义是:将对象以树形结构组织起来,以达成“部分-整体”的层次结构,使得客户端对单个对象和组合对象的使用具有一致性。
从定义中可以得到使用组合模式的环境为:在设计中想表示对象的“部分-整体”层次结构;希望用户忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象。
7.3 组成
-
抽象构件角色(Component)
它为组合中的对象声明接口,也可以为共有接口实现缺省行为。
-
树叶构件角色(Leaf)
在组合中表示叶节点对象——没有子节点,实现抽象构件角色声明的接口。
-
树枝构件角色(Composite)
在组合中表示分支节点对象——有子节点,实现抽象构件角色声明的接口;存储子部件。
类图如下:
不管是 Leaf还是 Composite,对于客户程序来说都是一样的——客户只知道 Component这个抽象类。而且在 Composite中还持有对 Component的引用,这使得 Composite中可以包含任何 Component抽象类的子类。
6.4 安全性与透明性
组合模式中必须提供对子对象的管理方法,不然无法完成对子对象的添加、删除等操作,也就失去了灵活性和扩展性。但是管理方法是在Component中就声明还是在Composite中声明呢?
-
一种方式是在Component里面声明所有的用来管理子类对象的方法,以达到Component接口的最大化(如下图所示)。目的就是为了使客户看来在接口层次上树叶和分支没有区别——透明性。但树叶是不存在子类的,因此Component声明的一些方法对于树叶来说是不适用的。这样也就带来了一些安全性问题。
-
另一种方式就是只在Composite里面声明所有的用来管理子类对象的方法(如下图所示)。这样就避免了上一种方式的安全性问题,但是由于叶子和分支有不同的接口,所以又失去了透明性。
《设计模式》一书认为:在这一模式中,相对于安全性,我们比较强调透明性。对于第一种方式中叶子节点内不需要的方法可以使用空处理或者异常报告的方式来解决。
6.5 代码实现
这里以JUnit中的组合模式的应用为例,JUnit是一个单元测试框架,按照此框架下的规范来编写测试代码,就可以使单元测试自动化。
为了达到“自动化”的目的,JUnit中定义了两个概念:
-
TestCase
TestCase是对一个类或者jsp等等编写的测试类;
-
TestSuite
TestSuite是一个不同TestCase的集合,当然这个集合里面也可以包含TestSuite元素;
这样运行一个TestSuite会将其包含的TestCase全部运行。
然而在真实运行测试程序的时候,是不需要关心这个类是TestCase还是TestSuite,我们只关心测试运行结果如何。这就是为什么JUnit使用组合模式的原因。
JUnit为了采用组合模式将TestCase和TestSuite统一起来,创建了一个Test接口来扮演抽象构件角色,这样原来的TestCase扮演组合模式中树叶构件角色,而TestSuite扮演组合模式中的树枝构件角色。
-
抽象构件角色
public interface Test { /** * 统计测试案例数量 * @return */ int countTestCases(); /** * 执行测试案例 * @param result */ void run(TestResult result); }
-
树叶构件角色
public class TestCase implements Test{ @Override public int countTestCases() { return 1; } @Override public void run(TestResult result) { result.run(this); } }
-
树干构件角色
public class TestSuite implements Test{ private List<Test> list = new ArrayList(); public void addTest(Test test) { // test可能是 testCase,也可能是 testSuite list.add(test); } @Override public int countTestCases() { int cnt = 0; for (Test test : list) { cnt += test.countTestCases(); } return cnt; } @Override public void run(TestResult result) { for (Test test : list) { if (result.shouldStop()) { break; } // 关键在这个方法上 runTest(test, result); } } private void runTest(Test test, TestResult result) { // 这个方法就是递归调用,至于 Test到底是什么类型,只有在运行时才能确定 test.run(result); } }
6.6 总结
组合模式有以下优点:
- 使客户端调用简单,客户端可以一致的使用组合结构或其中单个对象,用户就不必关心自己处理的是单个对象还是整个组合结构,这就简化了客户端代码。
- 更容易在组合体内加入对象部件. 客户端不必因为加入了新的对象部件而更改代码。这一点符合开闭原则的要求,对系统的二次开发和功能扩展很有利!
组合模式有以下缺点:
- 组合模式不容易限制组合中的构件。
七、装饰模式
7.1 引子
装饰模式?肯定让你想起又黑又火的家庭装修来。其实两者在道理上还是有很多相像的地方。家庭装修无非就是要掩盖住原来实而不华的墙面,抹上一层华而不实的涂料,让生活多一点色彩。而墙还是那堵墙,他的本质一点都没有变,只是多了一层外衣而已。
那设计模式中的装饰模式,是什么样子呢?
7.2 定义与结构
装饰模式(Decorator)也叫包装器模式(Wrapper)。GOF在《设计模式》一书中给出的定义为:动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。
让我们来理解一下这句话。我们来设计“门”这个类。假设你根据需求为“门”类作了如下定义:
现在,在系统的一个地方需要一个能够报警的Door,你来怎么做呢?你或许写一个Door的子类AlarmDoor,在里面添加一个子类独有的方法alarm()。嗯,那在使用警报门的地方你必须让客户知道使用的是警报门,不然无法使用这个独有的方法。而且,这个还违反了Liskov替换原则。
也许你要说,那就把这个方法添加到Door里面,这样不就统一了?但是这样所有的门都必须有警报,至少是个“哑巴”警报。而当你的系统仅仅在一两个地方使用了警报门,这明显是不合理的——虽然可以使用缺省适配器来弥补一下。
这时候,你可以考虑采用装饰模式来给门动态的添加些额外的功能。
7.3 组成
-
抽象构件角色 Component
定义一个抽象接口,以规范准备接收附加责任的对象。
-
具体构件角色 Concrete Component
这是被装饰者,定义一个将要被装饰增加功能的类。
-
装饰角色 Decorator
持有一个构件对象的实例,并定义了抽象构件定义的接口。
-
具体装饰角色 Concrete Decorator
负责给构件添加增加的功能。
类图如下:
ConcreteComponent可能继承自其它体系,而为了实现装饰模式,它还需要实现 Component接口;
整个装饰模式是按照组合模式来实现的——两者有着相似的结构图,都基于递归组合来组织可变数目的数量。
但是二者不同之处在于,组合模式侧重通过递归组合构造里,使不同的对象、多重的对象可以“一视同仁”;而装饰模式仅仅是借递归来达到定义中的目的
7.4 代码实现
背景:路边的煎饼摊,我们可以选择只要一个煎饼,什么也不加,也可以加鸡蛋,香肠等一些配料,不同的组合最后的价格也不一样;但是,无论怎么加料,最终拿到的它还是一个饼,只是这个饼经过了不同的加工,增加了不同的配料而已。
动态的给煎饼添加配料,最终得到不一样的煎饼。下面用代码简单的实现下
-
抽象构件角色
public abstract class Pancake { protected abstract String getDescription(); protected abstract double cost(); }
-
具体构件角色
public class DefaultPancake extends Pancake{ @Override protected String getDescription() { return "普通煎饼"; } @Override protected double cost() { return 1.0; } }
-
装饰角色
public class PancakeDecorator extends Pancake{ private Pancake pancake; public PancakeDecorator(Pancake pancake) { this.pancake = pancake; } @Override protected String getDescription() { return pancake.getDescription(); } @Override protected double cost() { return pancake.cost(); } }
-
具体装饰器角色
public class EggPancakeDecorator extends PancakeDecorator { public EggPancakeDecorator(Pancake pancake) { super(pancake); } @Override protected String getDescription() { return super.getDescription() + "加了一个鸡蛋"; } @Override protected double cost() { return super.cost() + 1; } }
public class SausagePancakeDecorator extends PancakeDecorator { public SausagePancakeDecorator(Pancake pancake) { super(pancake); } @Override protected String getDescription() { return super.getDescription() + "加了一根香肠"; } @Override protected double cost() { return super.cost() + 2; } }
7.5 总结
GOF书中给出了以下使用场景:
- 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 处理那些可以撤消的职责。
- 当不能采用生成子类的方法进行扩充时
- 一种情况是,可能有大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
- 另一种情况可能是因为类定义被隐藏,或类定义不能用于生成子类;
对于面向接口编程,应该尽量使客户程序不知道具体的类型,而应该对一个接口操作。这样就要求装饰角色和具体装饰角色要满足Liskov替换原则。像下面这样:
Component c = new ConcreteComponent();
Component c1 = new ConcreteDecorator(c);
在java.io中,并不是纯装饰模式的范例,它是装饰模式、适配器模式的混合使用。
采用Decorator模式进行系统设计往往会产生许多看上去类似的小对象,这些对象仅仅在他们相互连接的方式上有所不同,而不是它们的类或是它们的属性值有所不同。尽管对于那些了解这些系统的人来说,很容易对它们进行定制,但是很难学习这些系统,排错也很困难。这是GOF提到的装饰模式的缺点。
八、门面模式
8.1 引子
门面模式是非常简单的设计模式
8.2 定义与结构
门面模式(facade)又称外观模式。GOF在《设计模式》一书中给出如下定义:为子系统中的一组接口提供一个一致的界面, Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
定义中提到的子系统是指在设计中为了降低复杂性根据一定的规则(比如业务、功能),对系统进行的划分。子系统中封装有一些类。客户程序在使用子系统的时候,可能会像下图一样零乱。
在上面的实现方法中,客户类紧紧地依赖在子系统的实现上。子系统发生的变化,很可能要影响到客户类的调用。而且子系统在不断优化、可重用化的重构路上,会产生更多更小的类。这对使用子系统的客户类来说要完成一个工作流程,似乎要记住的接口太多了。
门面模式就是为了解决这种问题而产生的。看看使用了门面模式后的图:
这样就减少了客户程序和子系统之间的耦合,增加了可维护性。
8.3 组成
-
门面角色
这是门面模式的核心。它被客户角色调用,因此它熟悉子系统的功能。
它内部根据客户角色已有的需求预定了几种功能组合。
-
子系统角色
实现了子系统的功能。
对它而言,façade角色就和客户角色一样是未知的,它没有任何façade角色的信息和链接。
-
客户角色
调用façade角色来完成要得到的功能
8.4 代码实现
Facade一个典型应用就是进行数据库连接。一般我们在每一次对数据库进行访问,都要进行以下操作:先得到connect实例,然后打开connect获得连接,得到一个statement,执行sql语句进行查询,得到查询结果集。
我们可以将这些步骤提取出来,封装在一个类里面。这样,每次执行数据库访问只需要将必要的参数传递到这个类中就可以了。
8.5 总结
《设计模式》给出了门面模式的使用环境:
- 当你要为一个复杂子系统提供一个简单接口时。在上面已经描述了原因。
- 客户程序与抽象类的实现部分之间存在着很大的依赖性。引入facade将这个子系统与客户以及其他的子系统分离,可以提高子系统的独立性和可移植性(上面也提到了)。
- 当你需要构建一个层次结构的子系统时,使用facade模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,你可以让它们仅通过facade进行通讯,从而简化了它们之间的依赖关系。
优点:
- 它对客户屏蔽子系统组件,因而减少了客户处理的对象的数目并使得子系统使用起来更加方便。
- 它实现了子系统与客户之间的松耦合关系,而子系统内部的功能组件往往是紧耦合的。松耦合关系使得子系统的组件变化不会影响到它的客户。
- Facade模式有助于建立层次结构系统,也有助于对对象之间的依赖关系分层。
- Facade模式可以消除复杂的循环依赖关系。这一点在客户程序与子系统是分别实现的时候尤为重要。
- 在大型软件系统中降低编译依赖性至关重要。在子系统类改变时,希望尽量减少重编译工作以节省时间。用Facade可以降低编译依赖性,限制重要系统中较小的变化所需的重编译工作。
- Facade模式同样也有利于简化系统在不同平台之间的移植过程,因为编译一个子系统一般不需要编译所有其他的子系统。
- 如果应用需要,它并不限制它们使用子系统类。因此你可以让客户程序在系统易用性和通用性之间加以选择。
先来想想门面模式和我们已经讲过的哪个模式相像?答案就是抽象工厂模式。
- 两者虽然在分类上有所区别,但是都是为了方便客户程序的使用而建立。
- 两者的不同应该在于门面模式不仅方便了客户的使用,而且隐藏了不该让客户知道的类(这些类仅仅为子系统的其他类服务)。
但是在java语言中提供的包的概念已经能够很好的解决上面门面模式提到的问题。你可以把一个子系统放在一个包里面,里面要提供给外面访问的类定义为public,而不该公布的类就可以设计为非public。因此,在一定程度上,门面模式在java中基本上可以不使用了。
标准的门面模式虽然可以不再使用,但是这种提供一个中间类或者中间方法来方便客户程序使用的思想应该值得我们来实践的
九、享元模式
9.1 引子
我们先来复习下java中String类型的特性:String类型的对象一旦被创造就不可改变;当两个String对象所包含的内容相同的时候,JVM只创建一个String对象对应这两个不同的对象引用。
String类型的设计避免了在创建N多的String对象时产生的不必要的资源损耗,可以说是享元模式应用的范例,那么让我们带着对享元的一点模糊的认识开始,来看看怎么在自己的程序中正确的使用享元模式!
9.2 定义与分类
享元模式的定义为:采用一个共享类来避免大量拥有相同内容的“小类”的开销。这种开销中最常见、直观的就是内存的损耗。享元模式以共享的方式高效的支持大量的细粒度对象。
在名字和定义中都体现出了共享这一个核心概念,那么怎么来实现共享呢?要知道每个事物都是不同的,但是又有一定的共性,如果只有完全相同的事物才能共享,那么享元模式可以说就是不可行的;
因此我们应该尽量将事物的共性共享,而又保留它的个性。为了做到这点,享元模式中区分了 内蕴状态 和 外蕴状态。内蕴状态就是共性,外蕴状态就是个性了。
- 内蕴状态存储在享元内部,不会随环境的改变而有所不同,是可以共享的
- 外蕴状态是不可以共享的,它随环境的改变而改变的,因此外蕴状态是由客户端来保持(因为环境的变化是由客户端引起的)
在每个具体的环境下,客户端将外蕴状态传递给享元,从而创建不同的对象出来。
注:共享的对象必须是不可变的,不然一变则全变(如果有这种需求除外)
9.3 组成
先看单纯享元模式的结构
-
抽象享元角色
为具体享元角色规定了必须实现的方法,而外蕴状态就是以参数的形式通过此方法传入。在Java中可以由抽象类、接口来担当。
-
具体享元角色
实现抽象角色规定的方法。如果存在内蕴状态,就负责为内蕴状态提供存储空间。
-
享元工厂角色
负责创建和管理享元角色。要想达到共享的目的,这个角色的实现是关键!
-
客户端角色
维护对所有享元对象的引用,而且还需要存储对应的外蕴状态。
再看复合享元模式的结构
-
抽象享元角色
-
具体享元角色
-
复合享元角色
它所代表的对象是不可以共享的,但是可以分解成为多个单纯享元对象的组合。
-
享元工厂角色
-
客户端角色
复合享元模式采用了组合模式——为了将具体享元模式角色和复合角色同等对待和处理。
这也就决定了复合享元角色中所包含的每个单纯享元模式都具有相同的外蕴状态,而这些单纯享元的内蕴状态可以是不同的。
9.4 代码实现
-
抽象享元角色
public interface Shape { void draw(); }
-
具体享元角色
@Setter public class Circle implements Shape{ private String color; private int x; private int y; private int radius; public Circle(String color) { this.color = color; } @Override public void draw() { } }
-
享元工厂角色
public class ShapeFactory { private static final HashMap<String, Shape> cache = new HashMap<>(); public Circle getCircle(String color) { Circle circle = (Circle) cache.get(color); if (circle == null) { circle = new Circle(color); cache.put(color, circle); } return circle; } }
9.5 总结
享元模式优点就在于它能够大幅度的降低内存中对象的数量;
而为了做到这一步也带来了它的缺点:它使得系统逻辑复杂化,而且在一定程度上外蕴状态影响了系统的速度。
所以一定要切记使用享元模式的条件:
- 系统中有大量的对象,他们使系统的效率降低。
- 这些对象的状态可以分离出所需要的内外两部分。
外蕴状态和内蕴状态的划分以及两者关系的对应也是非常值得重视的。只有将内外划分妥当才能使内蕴状态发挥它应有的作用;如果划分失误,在最糟糕的情况下系统中的对象是一个也不会减少的!
两者的对应关系的维护和查找也是要花费一定的空间(当然这个比起不使用共享对象要小得多)和时间的,可以说享元模式就是使用时间来换取空间的。在Gof的书中是使用了B树来进行对应关系查找优化。
十、代理模式
10.1 引子
我们去科技市场为自己的机器添加点奢侈的配件,很多DIYer都喜欢去找代理商,因为在代理商那里拿到的东西不仅质量有保证,而且价格和售后服务上都会好很多。客户通过代理商得到了自己想要的东西,而且还享受到了代理商额外的服务;
而生产厂商通过代理商将自己的产品推广出去,而且可以将一些销售服务的任务交给代理商来完成(当然代理商要和厂商来共同分担风险,分配利润),这样自己就可以花更多的心思在产品的设计和生产上了。
在美国,任何企业的产品要想拿到市场上去卖就必须经过代理商这一个环节,否则就是非法的。看来代理商在商业运作中起着很关键的作用。
10.2 定义
代理模式在设计模式中的定义就是:为其他对象提供一种代理以控制对这个对象的访问。
说白了就是,在一些情况下客户不想或者不能直接引用一个对象,而代理对象可以在客户和目标对象之间起到中介作用,去掉客户不能看到的内容和服务或者增添客户需要的额外服务。
那么什么时候要使用代理模式呢?在对已有的方法进行使用的时候出现需要对原有方法进行改进或者修改,这时候有两种改进选择:修改原有方法来适应现在的使用方式,或者使用一个“第三者”方法来调用原有的方法并且对方法产生的结果进行一定的控制。
第一种方法是明显违背了“对扩展开放、对修改关闭”(开闭原则),而且在原来方法中作修改可能使得原来类的功能变得模糊和多元化(就像现在企业多元化一样),而使用第二种方式可以将功能划分的更加清晰,有助于后面的维护。所以在一定程度上第二种方式是一个比较好的选择!
根据《Java与模式》书中对代理模式的分类,代理模式分为8种,这里将几种常见的、重要的列举如下:
- 远程(Remote)代理:为一个位于不同的地址空间的对象提供一个局域代表对象。比如:你可以将一个在世界某个角落一台机器通过代理假象成你局域网中的一部分。
- 虚拟(Virtual)代理:根据需要将一个资源消耗很大或者比较复杂的对象延迟的真正需要时才创建。比如:如果一个很大的图片,需要花费很长时间才能显示出来,那么当这个图片包含在文档中时,使用编辑器或浏览器打开这个文档,这个大图片可能就影响了文档的阅读,这时需要做个图片Proxy来代替真正的图片。
- 保护(Protect or Access)代理:控制对一个对象的访问权限。比如:在论坛中,不同的身份登陆,拥有的权限是不同的,使用代理模式可以控制权限(当然,使用别的方式也可以实现)。
- 智能引用(Smart Reference)代理:提供比对目标对象额外的服务。比如:纪录访问的流量(这是个再简单不过的例子),提供一些友情提示等等。
10.3 组成
-
抽象主题角色
声明了真实主题和代理主题的共同接口。
-
代理主题角色
内部包含对真实主题的引用,并且提供和真实主题角色相同的接口。
-
真实主题角色
定义真实的对象。
类图如下:
10.4 代码实现
-
抽象主题
public interface MyForum { void addFile(); }
-
代理主题
public class ProxyMyForum implements MyForum{ private RealMyForum forum = new RealMyForum(); private int permission; public ProxyMyForum(int permission) { this.permission = permission; } @Override public void addFile() { if (permission<0) { return; } forum.addFile(); } }
-
真实主题
public class RealMyForum implements MyForum{ @Override public void addFile() { } }
10.5 扩展
-
jdk动态代理
-
创建接口
public interface SomeService { void doSome(); void doOther(); }
-
实现接口
public class SomeServiceImpl implements SomeService { @Override public void doSome() { System.out.println("执行doSome"); } @Override public void doOther() { System.out.println("执行doOther"); } }
-
创建 InvocationHandler实现类
public class MyInvocationHandler implements InvocationHandler { //目标对象 private Object target; public MyInvocationHandler(Object target) { this.target = target; } //通过代理对象执行方法时,会调用执行这个invoke()方法 /** * * @param proxy 代理类实例 * @param method 被代理的方法 * @param args 方法的参数数组 * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("执行MyInvocationHandler中的invoke方法"); //打印被调用的目标类中的方法名 System.out.println(method.getName()); Object res = null; //此方法是通过动态代理实现的增强的功能 ServiceTools.doLog(); //执行目标类方法,通过Method实现 res = method.invoke(target, args); //此方法是通过动态代理实现的增强的功能 ServiceTools.doTrans(); //返回目标方法的执行结果 return res; } }
-
通过 Proxy创建代理
public class MyApp { public static void main(String[] args) { //创建目标对象 SomeService target = new SomeServiceImpl(); //创建InvocationHandler对象 InvocationHandler handler = new MyInvocationHandler(target); //使用Proxy创建代理 SomeService proxy = (SomeService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler ); //通过代理执行方法,会调用handle中的invoke()方法 proxy.doSome(); } }
-
-
cglib代理
10.6 总结
代理模式能够协调调用者和被调用者,能够在一定程度上降低系统的耦合度。
代理模式中的真实主题角色可以结合组合模式来构造,这样一个代理主题角色就可以对一系列的真实主题角色生效,提高代码利用率。
十一、责任链模式
11.1 引子
初看责任链模式,心里不禁想起了一个以前听过的相声:看牙。说的是一个病人看牙的时候,医生不小心把拔下的一个牙掉进了病人嗓子里。病人因此楼上楼下的跑了好多科室,最后无果而终。
责任链模式就是这种“推卸”责任的模式,你的问题在我这里能解决我就解决,不行就把你推给另一个对象。至于到底谁解决了这个问题了呢?我管呢!
11.2 定义与结构
从名字上大概也能猜出这个模式的大概模样——系统中将会存在多个有类似处理能力的对象。当一个请求触发后,请求将在这些对象组成的链条中传递,直到找到最合适的“责任”对象,并进行处理。
《设计模式》中给它的定义如下:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
从定义上可以看出,责任链模式的提出是为了“解耦”,以应变系统需求的变更和不明确性。
下面是《设计模式》中给出的适用范围:
- 有多个对象可以处理一个请求,哪个对象处理该请求由运行时刻自动确定。
- 你想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
- 可处理一个请求的对象集合应被动态指定。
11.3 组成
-
抽象处理者角色 Handler
它定义了一个处理请求的接口。当然对于链子的不同实现,也可以在这个角色中实现后继链。
-
具体处理者角色 Concrete Handler
实现抽象角色中定义的接口,并处理它所负责的请求。如果不能处理则访问它的后继者。
责任链模式的纯与不纯的区别,就像黑猫、白猫的区别一样。
不要刻意的去使自己的代码来符合一个模式的公式。只要能够使代码降低耦合、提高重用,满足系统需求并能很好的适应变化就好了。正所谓:管它黑猫白猫,抓住老鼠就是好猫!
纯的责任链模式,规定一个具体处理者角色只能对请求作出两种动作:自己处理 或 传给下家。不能出现处理了一部分,把剩下的传给了下家的情况。而且请求在责任链中必须被处理,而不能出现无果而终的结局。
反之,则就是不纯的责任链模式。不纯的责任链模式还算是责任链模式吗?比如一个请求被捕获后,每个具体处理者都尝试去处理它,不管结果如何都将请求再次转发。我认为这种方式的实现,算不算是责任链模式的一种倒不重要,重要的是我们也能从中体味到责任链模式的思想:通过将多个处理者之间建立联系,来达到请求与具体的某个处理者的解耦
11.4 代码实现
-
抽象处理者角色
public interface CodeAutoParse { String[] generateCode(String moduleCode, int number, String rule, String[] target); }
-
具体处理者角色
public class DateAutoParse implements CodeAutoParse{ private final Calendar currentDate = Calendar.getInstance(); private CodeAutoParse nextOne; public DateAutoParse(CodeAutoParse nextOne) { this.nextOne = nextOne; } @Override public String[] generateCode(String moduleCode, int number, String rule, String[] target) { // 业务代码 if (nextOne != null) { return nextOne.generateCode(moduleCode, number, rule, target); } return target; } }
其它处理者也基本都是类似的结构,每一个里面都设置有一个用来存放下一个处理者的引用,不管你有没有下一个处理者。
11.5 总结
责任链模式优点,上面已经体现出来了。无非就是降低了耦合、提高了灵活性;
但是责任链模式可能会带来一些额外的性能损耗,因为它要从链子开头开始遍历。
十二、命令模式
12.1 引子
命令模式是从界面设计中提取出来的一种分离耦合,提高重用的方法。被认为是最优雅而且简单的模式,它的应用范围非常广泛。让我们一起来认识下它吧。
先从起源说起。在设计界面时,大家可以注意到这样的一种情况,同样的菜单控件,在不同的应用环境中的功能是完全不同的;而菜单选项的某个功能可能和鼠标右键的某个功能完全一致。按照最差、最原始的设计,这些不同功能的菜单、或者右键弹出菜单是要分开来实现的,你可以想象一下,word文档上面的一排菜单要实现出多少个“形似神非”的菜单类来?这完全是行不通的。这时,就要运用分离变化与不变的因素,将菜单触发的功能分离出来,而制作菜单的时候只是提供一个统一的触发接口。这样修改设计后,功能点可以被不同的菜单或者右键重用;而且菜单控件也可以去除变化因素,很大的提高了重用;而且分离了显示逻辑和业务逻辑的耦合。这便是命令模式的雏形。
12.2 定义与结构
《设计模式》中命令模式的定义为:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
看起来,命令模式好像神通广大。其实命令模式的以上功能还要看你是怎么写的——程序总是程序员写出来的,你写啥它才能干啥 : )
在我看来,其实命令模式像很多设计模式一样——通过在你的请求和处理之间加上了一个中间人的角色,来达到分离耦合的目的。通过对中间人角色的特殊设计来形成不同的模式。当然命令模式就是一种特殊设计的结果。
12.3 组成
-
命令角色 Command
声明执行操作的接口。由java接口或者抽象类来实现。
-
具体命令角色 Concrete Command
将一个接收者对象绑定于一个动作;调用接收者相应的操作,以实现命令角色声明的执行操作的接口。
-
客户角色 Client
创建一个具体命令对象(并可以设定它的接收者)。
-
请求者角色 Invoker
调用命令对象执行这个请求。
-
接收者角色 Receiver
知道如何实施与执行一个请求相关的操作。任何类都可能作为一个接收者。
类图如下:
12.4 代码实现
-
Command
public interface Command { void exec(); }
-
Concrete Command
public class ConcreteCommand implements Command{ private Receiver receiver; public ConcreteCommand(Receiver receiver) { this.receiver = receiver; } @Override public void exec() { receiver.action(); } }
-
Invoker
public class Invoker { private Command command; public Invoker(Command command) { this.command = command; } public void action() { command.exec(); } }
-
Receiver
public class Receiver { public void action() { System.out.println("zzzzzz"); } }
-
MainApp
public class MainApp { public static void main(String[] args) { Receiver receiver = new Receiver(); Command command = new ConcreteCommand(receiver); Invoker invoker = new Invoker(command); invoker.action(); } }
12.5 总结
当需要先将一个函数登记上,然后再以后调用此函数时,就需要使用命令模式,其实这就是回调函数。
有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系
优点:
- 调用者角色与接收者角色之间没有任何依赖关系,调用者实现功能时只需调用Command 抽象类的execute方法就可以,不需要了解到底是哪个接收者执行。
- Command的子类可以非常容易地扩展,而调用者Invoker和高层次的模块Client不产生严重的代码耦合。
- 命令模式结合其他模式会更优秀:命令模式可以结合责任链模式,实现命令族解析任务;结合模板方法模式,则可以减少 Command子类的膨胀问题。
缺点:
- 命令模式也是有缺点的,请看Command的子类:如果有N个命令,问题就出来 了,Command的子类就可不是几个,而是N个,这个类膨胀得非常大
在定义中提到,命令模式支持可撤销的操作。而在上面的举例中并没有体现出来。
其实命令模式之所以能够支持这种操作,完全得益于在请求者与接收者之间添加了中间角色。为了实现undo功能:
- 首先需要一个历史列表来保存已经执行过的具体命令角色对象;
- 修改具体命令角色中的执行方法,使它记录更多的执行细节,并将自己放入历史列表中;
- 并在具体命令角色中添加undo方法,此方法根据记录的执行细节来复原状态(很明显,首先程序员要清楚怎么来实现,因为它和execute的效果是一样的)。
同样,redo功能也能够照此实现。
命令模式还有一个常见的用法就是执行事务操作。这就是为什么命令模式还叫做事务模式的原因吧。它可以在请求被传递到接收者角色之前,检验请求的正确性,甚至可以检查和数据库中数据的一致性,而且可以结合组合模式的结构,来一次执行多个命令。
十三、解释器模式
13.1 引子
解释器模式描述了如何构成一个简单的语言解释器,主要应用在使用面向对象语言开发编译器中;在实际应用中,我们可能很少碰到去构造一个语言的文法的情况。
虽然你几乎用不到这个模式,但是看一看还是能受到一定的启发的。
13.2 定义与构造
解释器模式的定义如下:定义语言的文法,并且建立一个解释器来解释该语言中的句子。它属于类的行为模式。这里的语言意思是使用规定格式和语法的代码。
在GOF的书中指出:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。而且当文法简单、效率不是关键问题的时候效果最好。
13.3 组成
-
抽象表达角色
声明一个抽象的解释操作,这个接口为所有具体表达式角色(抽象语法树中的节点)都要实现的。
什么叫做抽象语法树呢?《java与模式》中给的解释为:抽象语法树的每一个节点都代表一个语句,而在每个节点上都可以执行解释方法。这个解释方法的执行就代表这个语句被解释。由于每一个语句都代表这个语句被解释。由于每一个语句都代表一个常见的问题的实例,因此每一个节点上的解释操作都代表对一个问题实例的解答。
-
终结符表达式角色
具体表达式
- 实现与文法中的终结符相关联的解释操作
- 而且句子中的每个终结符需要该类的一个实例与之对应
-
非终结符表达式角色
具体表达式
- 文法中的每条规则R::=R1R2…Rn都需要一个非终结符表带式角色
- 对于从R1到Rn的每个符号都维护一个抽象表达式角色的实例变量
- 实现解释操作,解释一般要递归地调用表示从R1到Rn的那些对象的解释操作
-
上下文环境角色
包含解释器之外的一些全局信息
-
客户角色
构建(或者被给定)表示该文法定义的语言中的一个特定的句子的抽象语法树;
调用解释操作
类图如下:
13.4 代码实现
不太用
13.5 总结
解释器模式提供了一个简单的方式来执行语法,而且容易修改或者扩展语法。一般系统中很多类使用相似的语法,可以使用一个解释器来代替为每一个规则实现一个解释器。而且在解释器中不同的规则是由不同的类来实现的,这样使得添加一个新的语法规则变得简单。
但是解释器模式对于复杂文法难以维护。可以想象一下,每一个规则要对应一个处理类,而且这些类还要递归调用抽象表达式角色,多如乱麻的类交织在一起是多么恐怖的一件事啊!
十四、迭代器模式
14.1 引子
迭代这个名词对于熟悉Java的人来说绝对不陌生。我们常常使用JDK提供的迭代接口进行java collection的遍历:
Iterator it = list.iterator();
while (it.hasNext()) {
// using it.next() to do some business logic
}
14.2 定义与结构
迭代器(Iterator)模式,又叫做游标(Cursor)模式。GOF给出的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。
从定义可见,迭代器模式是为容器而生。很明显,对容器对象的访问必然涉及到遍历算法。你可以一股脑的将遍历方法塞到容器对象中去;或者根本不去提供什么遍历算法,让使用容器的人自己去实现去吧。这两种情况好像都能够解决问题。
然而在前一种情况,容器承受了过多的功能,它不仅要负责自己“容器”内的元素维护(添加、删除等等),而且还要提供遍历自身的接口;而且由于遍历状态保存的问题,不能对同一个容器对象同时进行多个遍历。第二种方式倒是省事,却又将容器的内部细节暴露无遗。
而迭代器模式的出现,很好的解决了上面两种情况的弊端。
14.3 组成
-
迭代器角色 Iterator
迭代器角色负责定义访问和遍历元素的接口。
-
具体迭代器角色 Concrete Iterator
具体迭代器角色要实现迭代器接口,并要记录遍历中的当前位置。
-
容器角色 Container
容器角色负责提供创建具体迭代器角色的接口。
-
具体容器角色 Concrete Container
具体容器角色实现创建具体迭代器角色的接口——这个具体迭代器角色于该容器的结构相关。
类图如下:
迭代器模式在客户在容器之间加入了迭代器角色,迭代器角色的加入,就可以很好的避免容器内部细节的暴露,而且也使得设计符合“单一职责原则”。
在迭代器模式中,具体迭代器角色和具体容器角色是耦合在一起的——遍历算法是与容器的内部细节紧密相关的。为了使客户程序从与具体迭代器角色耦合的困境中脱离出来,避免具体迭代器角色的更换给客户程序带来的修改,迭代器模式抽象了具体迭代器角色,使得客户程序更具一般性和重用性,这被称为多态迭代。
14.4 代码实现
迭代器模式本身的规定比较松散,以 Java Collection为例。
- 迭代器角色定义了遍历的接口,但是没有规定由谁来控制迭代。在 Java Collection的应用中,是由客户进程来控制遍历的进程,被称为“外部迭代器”;还有一种实现方式便是由迭代器自身来控制迭代,被称为“内部迭代器”。外部迭代器要比内部迭代器灵活、强大,而且内部迭代器在 Java语言环境中,可用性很弱。
- 在迭代器模式中没有规定谁来实现遍历算法,好像理所当然的要在迭代器角色中实现。因为既便于一个容器上使用不同的遍历算法,也便于将一种遍历算法应用于不同的容器。但是这样就破坏掉了容器的封装——容器角色就要公开自己的私有属性,在 Java中便意味着向其它类公开了自己的私有属性。
- 在 Java Collection的应用中,提供的具体迭代器角色是定义在容器角色中的内部类。这样便保护了容器的封装,同时也提供类遍历算法接口,让程序员可以扩展自己的迭代器。
14.5 总结
迭代器的优点:
- 支持以不同的方式遍历一个容器角色。根据实现方式的不同,效果上会有差别。
- 简化了容器的接口。但是在java Collection中为了提高可扩展性,容器还是提供了遍历的接口。
- 对同一个容器对象,可以同时进行多个遍历。因为遍历状态是保存在每一个迭代器对象中的。
由此也能得出迭代器模式的适用范围:
- 访问一个容器对象的内容而无需暴露它的内部表示。
- 支持对容器对象的多种遍历。
- 为遍历不同的容器结构提供一个统一的接口(多态迭代)。
十五、调停者模式
15.1 引子
Mediator Pattern中文译为 “中介者模式”、“调停者模式”。其实都不太好,由于现实生活中的“中介”是要和客户打交道,而省去客户原本繁琐的手续,这一点和门面模式的初衷很像;
而在 Mediator Parttern中 Mediator是不可见的,它的初衷仅仅是规范信息的传递方式。因此称为“传递器模式”似乎更贴切一些。
15.2 定义与结构
GOF给中介者模式下的定义是:用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。简单点来说,将原来两个直接引用或者依赖的对象拆开,在中间加入一个“中介”对象,使得两头的对象分别和“中介”对象引用或者依赖。
当然并不是所有的对象都需要加入“中介”对象。如果对象之间的关系原本一目了然,中介对象的加入便是“画蛇添足”。
15.3 组成
-
抽象调停者角色 Mediator
抽象中介者角色定义统一的接口用于各同事角色之间的通信。
-
具体调停者角色 Concrete Mediator
具体中介者角色通过协调各同事角色实现协作行为。为此它要知道并引用各个同事角色。
-
同事角色 Colleague
每一个同事角色都知道对应的具体中介者角色,而且与其他的同事角色通信的时候,一定要通过中介者角色协作。
类图如下:
由于调停者的行为与要使用的数据与具体业务紧密相关,抽象调停者角色提供一个能方便很多对象使用的接口是不太现实的。所以抽象调停者角色往往是不存在的,或者只是一个标示接口。
“恰到好处,过犹不及”
15.4 实现
是否还记得应用广泛的MVC分为哪三层?模型层(Model)、表现层(View)还有控制层(Controll/Mediator)。控制层便是位于表现层与模型层之间的中介者。笼统地说MVC也算是中介者模式在框架设计中的一个应用。
由于中介者模式在定义上比较松散,在结构上和观察者模式、命令模式十分相像;而应用目的又与结构模式“门面模式”有些相似。
在结构上,中介者模式与观察者模式、命令模式都添加了中间对象——只是中介者去掉了后两者在行为上的方向。因此中介者的应用可以仿照后两者的例子去写。但是观察者模式、命令模式中的观察者、命令都是被客户所知的,具体哪个观察者、命令的应用都是由客户来指定的;而大多中介者角色对于客户程序却是透明的。当然造成这种区别的原因是由于它们要达到的目的不同。
从目的上看,中介者模式与观察者模式、命令模式便没有了任何关系,倒是与前面讲过的门面模式有些相似。但是门面模式是介于客户程序与子系统之间的,而中介者模式是介于子系统与子系统之间的。这也注定了它们有很大的区别:门面模式是将原有的复杂逻辑提取到一个统一的接口,简化客户对逻辑的使用。它是被客户所感知的,而原有的复杂逻辑则被隐藏了起来。而中介者模式的加入并没有改变客户原有的使用习惯,它是隐藏在原有逻辑后面的,使得代码逻辑更加清晰可用。
15.5 总结
前面已经陆陆续续的将中介者模式的特点写了出来。这里再总结一下。
使用中介者模式最大的好处就是将同事角色解耦。这带来了一系列的系统结构改善:提高了原有系统的可读性、简化原有系统的通信协议——将原有的多对多变为一对多、提高了代码的可复用性……
呵呵,但是中介者角色集中了太多的责任,所有有关的同事对象都要由它来控制。这不由得让我想起了 简单工厂模式,但是由于中介者模式的特殊性——与业务逻辑密切相关,不能采用类似 工厂方法模式 的解决方法。因此建议在使用中介者模式的时候注意控制中介者角色的大小。
讨论了这么多关于中介者模式的特点。可以总结出中介者模式的使用时机:一组对象以定义良好但是复杂的方式进行通信,产生了混乱的依赖关系,也导致对象难以复用。
中介者模式很容易在系统中应用,也很容易在系统中误用。当系统出现了“多对多”交互复杂的对象群,不要急于使用中介者模式,而要先反思你的系统在设计上是不是合理。
十六、备忘录模式
16.1 引子
俗话说:世上难买后悔药。所以凡事讲究个“三思而后行”,但总常见有人做“痛心疾首”状:当初我要是……。如果真的有《大话西游》中能时光倒流的“月光宝盒”,那这世上也许会少一些伤感与后悔——当然这只能是痴人说梦了。
但是在我们手指下的程序世界里,却有的后悔药买。今天我们要讲的备忘录模式便是程序世界里的“月光宝盒”。
16.2 定义与结构
备忘录(Memento)模式又称标记(Token)模式。GOF给备忘录模式的定义为:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
在讲 命令模式 的时候,我们曾经提到利用中间的命令角色可以实现undo、redo的功能。从定义可以看出备忘录模式是专门来存放对象历史状态的,这对于很好的实现undo、redo功能有很大的帮助。所以在命令模式中undo、redo功能可以配合备忘录模式来实现。
其实单就实现保存一个对象在某一时刻的状态的功能,还是很简单的——将对象中要保存的属性放到一个专门管理备份的对象中,需要的时候则调用约定好的方法将备份的属性放回到原来的对象中去。但是你要好好看看为了能让你的备份对象访问到原对象中的属性,是否意味着你就要全部公开或者包内公开对象原本私有的属性呢?如果你的做法已经破坏了封装,那么就要考虑重构一下了。
备忘录模式只是GOF对“恢复对象某时的原有状态”这一问题提出的通用方案。因此在如何保持封装性上——由于受到语言特性等因素的影响,备忘录模式并没有详细描述,只是基于C++阐述了思路。那么基于Java的应用应该怎样来保持封装呢?我们将在实现一节里面讨论。
16.3 组成
-
备忘录角色 Memento
备忘录角色存储“备忘发起角色”的内部状态。“备忘发起角色”根据需要决定备忘录角色存储“备忘发起角色”的哪些内部状态。为了防止“备忘发起角色”以外的其他对象访问备忘录。备忘录实际上有两个接口,“备忘录管理者角色”只能看到备忘录提供的窄接口——对于备忘录角色中存放的属性是不可见的。“备忘发起角色”则能够看到一个宽接口——能够得到自己放入备忘录角色中属性。
-
备忘发起者角色 Originator
“备忘发起角色”创建一个备忘录,用以记录当前时刻它的内部状态。在需要时使用备忘录恢复内部状态。
-
备忘录管理者 Caretaker
负责保存好备忘录。不能对备忘录的内容进行操作或检查。
类图如下:
16.4 实现
按照定义中的要求,备忘录角色要保持完整的封装,最好的情况便是:被网络角色只应该暴露操作内部存储属性的接口给“备忘发起角色”;而对于其它角色则是不可见的。
下面是三种在 Java中可保存封装的方法进行探讨:
-
采用两个不同的接口类来限制访问权限
这两个接口类中,一个提供比较完整的操作状态的方法(称为宽接口);另一个则可以只是一个标示(称为窄接口)。
备忘录角色要实现这两个接口,这样对于“备忘录发起角色”采用宽接口进行访问;而对于其它的角色或者对象则采用窄接口进行访问
-
采用内部类来控制访问权限
将备忘录角色作为“备忘发起者角色”的一个私有内部类
这种方式更好!
-
采用 clone方法来简化备忘录模式
由于 Java提供了 clone机制,这使得复制一个对象变得轻松起来。
使用 clone机制的备忘录模式,备忘录角色就基本可以省略了。
但不推荐!
下面采用第二种方式
-
窄接口
public interface MementolF { }
-
备忘录角色
// 内部类 private class Mementol implements com.zwb.design.memento.MementolF { private int state; public Mementol(int state) { this.state = state; } public int getState() { return state; } }
-
备忘录管理者
public class Caretaker { private MementolF mementolF; public MementolF getMemento() { return mementolF; } public void saveMemento(MementolF mementolF) { this.mementolF = mementolF; } }
-
备忘发起者
public class Originator { /** * 需要保存的状态 */ private int state = 90; /** * 备忘录管理者 */ private Caretaker c = new Caretaker(); /** * 读取备忘录角色以恢复以前的状态 */ public void setMemento() { Mementol memento = (Mementol) c.getMemento(); state = memento.getState(); System.out.println("the state is " + state + " now!"); } /** * 创建一个备忘录角色,并将当前状态属性存入,托给“备忘录管理者角色”存放 */ public void createMemento() { c.saveMemento(new Mementol(state)); } private class Mementol implements com.zwb.design.memento.MementolF { private int state; public Mementol(int state) { this.state = state; } public int getState() { return state; } } }
-
客户端程序
public class MainApp { public static void main(String[] args) { Originator originator = new Originator(); originator.createMemento(); originator.setMemento(); } }
16.5 总结
使用了备忘录模式来实现保存对象的历史状态可以有效地保持封装边界。使用备忘录可以避免暴露一些只应由“备忘发起角色”管理却又必须存储在“备忘发起角色”之外的信息。把“备忘发起角色”内部信息对其他对象屏蔽起来, 从而保持了封装边界。
但是如果备份的“备忘发起角色”存在大量的信息或者创建、恢复操作非常频繁,则可能造成很大的开销。
GOF在《设计模式》中总结了使用备忘录模式的前提:
- 必须保存一个对象在某一个时刻的(部分)状态, 这样以后需要时它才能恢复到先前的状态。
- 如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。
十七、观察者模式
17.1 引子
还记得警匪片上,匪徒们是怎么配合实施犯罪的吗?一个团伙在进行盗窃的时候,总 有一两个人在门口把风——如果有什么风吹草动,则会立即通知里面的同伙紧急撤退。也许放风的人并不一定认识里面的每一个同伙;而在里面也许有新来的小弟不认识这个放风的。但是这没什么,这个影响不了他们之间的通讯,因为他们之间有早已商定好的暗号。
呵呵,上面提到的放风者、偷窃者之间的关系就是观察者模式在现实中的活生生的例子。
17.2 定义与结构
观察者(Observer)模式又名发布-订阅(Publish/Subscribe)模式。GOF给观察者模式如下定义:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
在这里先讲一下面向对象设计的一个重要原则——单一职责原则。因此系统的每个对象应该将重点放在问题域中的离散抽象上。因此理想的情况下,一个对象只做一件事情。这样在开发中也就带来了诸多的好处:提供了重用性和维护性,也是进行重构的良好的基础。
因此几乎所有的设计模式都是基于这个基本的设计原则来的。观察者模式的起源我觉得应该是在GUI和业务数据的处理上,因为现在绝大多数讲解观察者模式的例子都是这一题材。但是观察者模式的应用决不仅限于此一方面。
17.3 组成
-
抽象目标角色 Subject
目标角色知道它的观察者,可以有任意多个观察者观察同一个目标。并且提供注册和删除观察者对象的接口。目标角色往往由抽象类或者接口来实现。
-
抽象观察者角色 Observer
为那些在目标发生改变时需要获得通知的对象定义一个更新接口。抽象观察者角色主要由抽象类或者接口来实现。
-
具体目标角色 Concrete Subject
将有关状态存入各个Concrete Observer对象。当它的状态发生改变时, 向它的各个观察者发出通知。
-
具体观察者角色 Concrete Observer
存储有关状态,这些状态应与目标的状态保持一致。实现Observer的更新接口以使自身状态与目标的状态保持一致。在本角色内也可以维护一个指向Concrete Subject对象的引用。
类图如下:
在 Subject这个抽象类中,提供了上面提到的功能,而且存放了一个通知方法:notify()
还可以看到,Subject和 Concrete Subject之间可以说是使用了模板方法。这样当具体模板角色的状态发生改变,按照约定则会去调用通知方法,在这个方法中则会根据目标角色中注册的观察者名单来逐个调用相应的 update方法来调整观察者的状态。
17.4 代码实现
这里依旧采用 Junit作为例子。
JUnit为用户提供了三种不同的测试结果显示界面,以后还可能会有其它方式的现实界面……。怎么才能将测试的业务逻辑和显示结果的界面很好的分离开?不用问,就是观察者模式!
-
抽象观察者
public interface Listener { void addError(Test test, Throwable t); void addFailure(Test test, Failure t); void endTest(Test test); void startTest(Test test); }
-
具体观察者
public class ResultPrinter implements Listener{ @Override public void addError(Test test, Throwable t) { System.out.println("E"); } @Override public void addFailure(Test test, Failure t) { System.out.println("F"); } @Override public void endTest(Test test) { System.out.println("end test"); } @Override public void startTest(Test test) { System.out.println("start test"); } }
-
具体目标对象
public class TestResultSubject { private List<Listener> listeners; private List<Failure> failures; public TestResultSubject() { this.listeners = new ArrayList<>(); this.failures = new ArrayList<>(); } /** * 出现异常后,通知所有订阅者 * @param test * @param t */ public synchronized void addFailure(Test test, Failure t) { failures.add(t); for (Listener listener : listeners) { listener.addFailure(test, t); } } /** * 注册观察者 * @param listener */ public synchronized void addListener(Listener listener) { listeners.add(listener); } public synchronized void removeListener(Listener listener) { listeners.remove(listener); } }
17.5 总结
GOF给出了以下使用观察者模式的情况:
- 当一个抽象模型有两个方面, 其中一个方面依赖于另一方面。将这二者封装在独立的对象中以使它们可以各自独立地改变和复用。
- 当对一个对象的改变需要同时改变其它对象, 而不知道具体有多少对象有待改变。
- 当一个对象必须通知其它对象,而它又不能假定其它对象是谁。换言之, 你不希望这些对象是紧密耦合的。
其实观察者模式同前面讲过的桥梁、策略有着共同的使用环境:将变化独立封装起来,以达到最大的重用和解耦。观察者与后两者不同的地方在于,观察者模式中的目标和观察者的变化不是独立的,而是有着某些联系。
观察者模式在关于目标角色、观察者角色通信的具体实现中,有两个版本:
- 一种情况便是目标角色在发生变化后,仅仅告诉观察者角色“我变化了”;观察者角色如果想要知道具体的变化细节,则就要自己从目标角色的接口中得到。这种模式被很形象的称为:拉模式——就是说变化的信息是观察者角色主动从目标角色中“拉”出来的。
- 还有一种方法,那就是我目标角色“服务一条龙”,通知你发生变化的同时,通过一个参数将变化的细节传递到观察者角色中去。这就是“推模式”——管你要不要,先给你啦。
这两种模式的使用,取决于系统设计时的需要。如果目标角色比较复杂,并且观察者角色进行更新时必须得到一些具体变化的信息,则“推模式”比较合适。如果目标角色比较简单,则“拉模式”就很合适啦。
十八、策略模式
18.1 引子
18日下午3时一刻,沈阳,刚刚下完一场几年罕见的大雪,天气格外的冷,公交车在“车涛汹涌”的公路上举步维艰,我坐在里面不时的看表——回公司的班车就要发车了,我还离等车的地方好远……。
都是这可恶的天气打乱了我的计划!看来我要重新盘算下下了公交车的计划了:
- 如果在半点以前能够到达等班车的地方,我就去旁边卖书报的小店里面买份《南方周末》,顺便逼逼严寒;
- 如果可恶的公交到时候还不能拱到的话,我就只好去附近的家乐福里面打发两个小时直到下一趟班车发车!
打住!!
其实在上面提到的就是对两种不同情况所采取的不同的策略。这种情况在实际系统中也是经常遇到,那么你是怎么来实现不同的策略的呢?也许你看了策略模式后会增加一种不错的选择!
18.2 定义
策略模式(Strategy)属于对象行为型设计模式,主要是定义一系列的算法,把这些算法一个个封装成拥有共同接口的单独的类,并且使它们之间可以互换。策略模式使这些算法在客户端调用它们的时候能够互不影响地变化。这里的算法不要狭义的理解为数据结构中算法,可以理解为不同的业务处理方法。
这种做法会带来什么样的好处呢?
它将算法的使用和算法本身分离,即将变化的具体算法封装了起来,降低了代码的耦合度,系统业务策略的更变仅需少量修改。
算法被提取出来,这样可以使算法得到重用,这种情况还可以考虑使用享元模式来共享算法对象,来减少系统开销(但要注意使用享元模式的建议条件)。
18.3 结构
-
算法使用环境角色 Context
算法被引用到这里和一些其它的与环境有关的操作一起来完成任务
-
抽象策略角色 Strategy
规定了所有具体策略角色所需的接口,通常由 接口 或 抽象类来实现
-
具体策略角色 Concrete Strategy
实现了抽象策略角色定义的接口
类图如下:
18.4 实现
在Java语言中对策略模式的应用是很多的,我们这里举个布局管理器的例子。在java.awt类库中有很多种设定好了的Container对象的布局格式,这些格式你可以在创建软件界面的时候使用到。
如果不使用策略模式,那么就没有了对布局格式扩展的可能,因为你要去修改Container中的方法,去让它知道你这种布局格式,这显然是不可行的。
-
Context
public class ContextContainer { LayoutManager layoutManager; public void setLayoutManager(LayoutManager lm) { this.layoutManager = lm; } public LayoutManager getLayoutManager() { return layoutManager; } }
-
Strategy
public interface LayoutManager { void addLayoutComponent(String name, Component component); Dimension minimumLayoutSize(ContextContainer parent); void layoutContainer(ContextContainer parent); }
-
ConcreteStrategy
public class FlowLayoutWindow implements LayoutManager { @Override public void addLayoutComponent(String name, Component component) { } @Override public Dimension minimumLayoutSize(ContextContainer parent) { return null; } @Override public void layoutContainer(ContextContainer parent) { } }
对于具体布局管理器的感知直到最后的客户程序中才能得到,在此之前是不关心的。但是必须明确的是:客户必须知道有哪些策略方法可以使用,这也限制了它的使用范围。
18.5 总结
使用建议:
- 系统需要能够在几种算法中快速的切换
- 系统中有一些类它们仅行为不同时,可以考虑采用策略模式来进行重构
- 系统中存在多重条件选择语句时,可以考虑采用策略模式来重构。
但是要注意一点,策略模式中不可以同时使用多于一个的算法
十九、状态模式
19.1 引子
状态模式自身结构非常简单——前面刚刚介绍了几个结构比较简单的设计模式,和他们一样,状态模式在具体实现上留下了可变换的余地。我前面已经介绍过它的孪生兄妹 策略模式 了,大家可以两者比较着阅读。本文将会讨论两者的区别。
19.2 定义
GOF《设计模式》中给状态模式下的定义为:允许一个对象在其内部状态改变时改变它的行为。这个对象看起来似乎修改了它的类。看起来,状态模式好像是神通广大——居然能够“修改自身的类”!
能够让程序根据不同的外部情况来做出不同的响应,最直接的方法就是在程序中将这些可能发生的外部情况全部考虑到,使用if else 语句来进行代码响应选择。但是这种方法对于复杂一点的状态判断,就会显得杂乱无章,容易产生错误;而且增加一个新的状态将会带来大量的修改。这个时候“能够修改自身”的状态模式的引入也许是个不错的主意。
状态模式可以有效的替换充满在程序中的if else语句:将不同条件下的行为封装在一个类里面,再给这些类一个统一的父类来约束他们。
19.3 组成
-
使用环境角色 Context
客户程序是通过它来满足自己的需求。它定义了客户程序需要的接口;并且维护一个具体状态角色的实例,这个实例来决定当前的状态。
-
状态角色 State
定义一个接口以封装与使用环境角色的一个特定状态相关的行为。
-
具体状态角色 Concrete State
实现状态角色定义的接口。
类图如下,和策略模式非常相似:
19.4 实现
在引子中已经提到,状态模式在具体实现上存在不同的方案。因此这里重点就这些不同的实现方式进行介绍和讨论。
首先,实现时是否将状态角色、具体状态角色暴露给客户程序?按照GOF的建议是不希望将状态角色暴露给客户程序的,与客户程序打交道的仅仅是使用环境角色,客户是不知道系统是怎么实现的,更不关心什么有几个具体状态。但是当使用环境角色中的初始状态紧紧依赖于客户程序时,似乎暴露是在所难免的——这就与策略模式异常相似了!
具体状态角色中的行为一般是与使用环境角色密切相关的。因此这里便有了一个小细节:我们把使用环境角色作为参数传递进入具体状态角色后,是在具体状态角色中来实现状态响应行为;还是仅仅调用在使用环境角色中已经实现了的方法?由于这些行为往往与使用环境角色相关,所以按照《重构》一书的“指导”——后一种实现方法是比较地道的。
从定义可知,状态模式是要应对状态转换的。那么状态的转换在哪里定义呢?你可以选择在使用环境角色的代码中来表现出来,当然这便意味着状态转变的规则就固定下来了。GOF还给出了另外一种稍微灵活一点的实现方式:在每一个具体状态角色中来指定后续状态以及何时进行转换。
其实在java强大的反射机制的支持下,我们还可以将状态的转换做的更加灵活——我们可以将状态转换的规则写在.xml等等的配置文件里面甚至是数据库中,我们姑且叫做状态转换表。进行转换前,根据状态转换表来读取下一个状态,然后利用反射获得具体的状态对象……。哈哈,看起来很不错的样子,只是效率可能低一些,在企业应用中这应该不是最重要的。
状态模式已经被我们想象着“实现”了一番。那么状态模式的引入会给我们的程序带来哪些优势呢?前面我们已经说过:状态模式的引入免除了代码中复杂而庸长的逻辑判断语句。而且具体状态角色将具体状态和它对应的行为封装了起来,这使得增加一种新的状态变得简单一些。而且如果设计合理得话,具体状态角色可以被重用(和策略模式一样,可以考虑使用享元模式来实现)。
使用状态模式也会带来一些问题。每个状态对应一个具体的状态类,使得整体分散,逻辑不太清晰。当然对于一个状态非常多的系统,状态模式带来的优点还是大于它的缺点的。
由上面的分析就可以很明确的知道什么时候该使用状态模式了。下面是GOF在《设计模式》中给出的状态模式的适用情况:
- 一个对象的行为取决于它的状态, 并且它必须在运行时刻根据状态改变它的行为。
- 一个操作中含有庞大的多分支的条件语句,且这些分支依赖于该对象的状态。
19.5 总结
对比【状态模式】和【策略模式】,难免会产生疑问:这两个明明是一个东西嘛!下面我们就来分析下两者区别。
-
首先我要声明,在实际应用中只要能够使得你的代码灵活漂亮起来,何必计较这些方方面面的差别呢?
-
Brandon Goldfedder在《模式的乐趣》里是怎么说的:
“strategy模式在结构上与state模式非常相似,但是在概念上,他们的目的差异非常大。
区分这两个模式的关键是看行为是由状态驱动还是由一组算法驱动,这条规则似乎有点随意,但是在判断时还是需要考虑它。
通常,State模式的“状态”是在对象内部的,Strategy模式的“策略”可以在对象外部,不过这也不是一条严格、可靠的规则。”
-
这两个模式的划分,就在于使用的目的是不同的——策略模式用来处理算法变化,而状态模式则是处理状态变化。
- 策略模式中,算法是否变化完全是由客户程序开决定的,而且往往一次只能选择一种算法,不存在算法中途发生变化的情况
- 而状态模式如定义中所言,在它的生命周期中存在着状态的转变和行为得更改,而且状态变化是一个线形的整体;对于客户程序来言,这种状态变化往往是透明的。
二十、模板模式
20.1 引子
这是一个很简单的模式,却被非常广泛的使用。之所以简单是因为在这个模式中仅仅使用到了【继承关系】。
继承关系由于自身的缺陷,被专家们扣上了“罪恶”的帽子。“使用委派关系代替继承关系”,“尽量使用接口实现而不是抽象类继承”等等专家警告,让我们这些菜鸟对继承“另眼相看”。
其实,继承还是有很多自身的优点所在。只是被大家滥用的似乎缺点更加明显了。合理的利用继承关系,还是能对你的系统设计起到很好的作用的。而模板方法模式就是其中的一个使用范例。
20.2 定义
GOF给模板方法(Template Method)模式的定义为:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。这里的算法的结构,可以理解为你根据需求设计出来的业务流程。特定的步骤就是指那些可能在内容上存在变数的环节。
可以看出来,模板方法模式也是为了巧妙解决变化对系统带来的影响而设计的。使用模板方法使系统扩展性增强,最小化了变化对系统的影响。
20.3 组成
-
抽象类 Abstract Class
定义了 一~多个抽象的方法,以供具体的子类来实现它们;而且还要实现一个模板方法,来定义一个算法的骨架。
该模板方法不仅调用前面的抽象方法,也可以调用其他的操作,只要能完成自身的使命。
-
具体类 Concrete Class
实现父类中的抽象方法以完成算法中与特定子类相关的步骤。
类图如下:
20.4 组成
JUnit中的 TestCase以及它的子类就是一个【模板方法模式】的例子。
在 TestCase这个抽象类中将整个测试的流程设置好了,比如先执行 Setup方法初始化测试前提,再运行测试方法,然后再 TearDown来取消测试设置。但是你将在 Setup、TearDown里面做一些什么呢?鬼才知道!!!
因此这些步骤的具体实现都需要延迟到子类中去。
20.5 适用情况
根据上面对定义的分析,以及例子的说明,可以看出模板方法适用于以下情况:
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复。其实这可以说是一种好的编码习惯了。
- 控制子类扩展。模板方法只在特定点调用操作,这样就只允许在这些点进行扩展。如果你不愿子类来修改你的模板方法定义的框架,你可以采用两种方式来做:
- 在API中不体现出你的模板方法;
- 将你的模板方法置为final就可以了。
可以看出,使用模板方法模式可以将代码的公共行为提取出来,达到复用的目的。而且,在模板方法模式中,是由父类的模板方法来控制子类中的具体实现。这样你在实现子类的时候,根本不需要对业务流程有太多的了解。
二十一、访问者模式
21.1 引子
对于系统中一个已经完成的类层次结构,我们已经给它提供了满足需求的接口。但是面对新增加的需求,我们应该怎么做呢?如果这是为数不多的几次变动,而且你不用为了一个需求的调整而将整个类层次结构统统地修改一遍,那么直接在原有类层次结构上修改也许是个不错的主意。
但是往往我们遇到的却是:这样的需求变动也许会不停的发生;更重要的是需求的任何变动可能都要让你将整个类层次结构修改个底朝天……。这种类似的操作分布在不同的类里面,不是一个好现象,我们要对这个结构重构一下了。
那么,访问者模式也许是你很好的选择。
21.2 定义
访问者模式,顾名思义使用了这个模式后就可以在不修改已有程序结构的前提下,通过添加额外的“访问者”来完成对已有代码功能的提升。
《设计模式》一书对于访问者模式给出的定义为:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。从定义可以看出结构对象是使用访问者模式必须条件,而且这个结构对象必须存在遍历自身各个对象的方法。这便类似于java中的collection概念了。
21.3 组成
-
访问者角色 Visitor
为该对象结构中具体元素角色声明一个访问操作接口。该操作接口的名字和参数标识了发送访问请求给具体访问者的具体元素角色。这样访问者就可以通过该元素角色的特定接口直接访问它。
-
具体访问者角色 Concrete Visitor
实现每个由访问者角色(Visitor)声明的操作。
-
元素角色 Element
定义一个Accept操作,它以一个访问者为参数。
-
具体元素角色 Concrete Element
实现由元素角色提供的Accept操作。
-
对象结构角色 Object Structure
这是使用访问者模式必备的角色。
它要具备以下特征:
- 能枚举它的元素;
- 可以提供一个高层的接口以允许该访问者访问它的元素;
- 可以是一个复合(组合模式)或是一个集合,如一个列表或一个无序集合。
那么像引言中假想的。我们应该做些什么才能让访问者模式跑起来呢?首先我们要在原有的类层次结构中添加accept方法。然后将这个类层次中的类放到一个对象结构中去。这样再去创建访问者角色……
21.4 实现
见《Think in Java》教学代码
-
访问者角色
public interface Visitor { void visit(Gladiolus g); void visit(Runuculus r); void visit(Chrysanthemum c); }
-
具体访问者角色
public class StringVal implements Visitor{ String s; @Override public String toString() { return s; } @Override public void visit(Gladiolus g) { s = "Gladiolus"; } @Override public void visit(Runuculus r) { s = "Runuculus"; } @Override public void visit(Chrysanthemum c) { s = "Chrysanthemum"; } }
public class Bee implements Visitor{ @Override public void visit(Gladiolus g) { System.out.println("Bee and Gladiolus"); } @Override public void visit(Runuculus r) { System.out.println("Bee and Runuculus"); } @Override public void visit(Chrysanthemum c) { System.out.println("Bee and Chrysanthemum"); } }
-
元素角色
public interface Flower { void accept(Visitor v); }
-
具体元素角色
public class Chrysanthemum implements Flower { @Override public void accept(Visitor v) { v.visit(this); } }
public class Gladiolus implements Flower { @Override public void accept(Visitor v) { v.visit(this); } }
public class Runuculus implements Flower { @Override public void accept(Visitor v) { v.visit(this); } }
-
MainApp
public class BeeAndFlowers { /* 在这里你能看到访问者模式执行的流程: 1. 首先在客户端先获得一个具体的访问者角色 2. 遍历对象结构, 对每一个元素调用accept方法,将具体访问者角色传入 这样就完成了整个过程 */ List<Flower> flowers = new ArrayList<>(); Visitor sval; public BeeAndFlowers() { for (int i = 0; i < 10; i++) { flowers.add(FlowerGenerator.newFlower()); } } public void test() { sval = new StringVal(); for (Flower flower : flowers) { flower.accept(sval); System.out.println(sval); } } public static void main(String[] args) { new BeeAndFlowers().test(); } }
它的本质是双重分派:
- 首先在客户程序中将具体访问者模式作为参数传递给具体元素角色。这便完成了一次分派。
- 进入具体元素角色后,具体元素角色调用作为参数的具体访问者模式中的visitor方法,同时将自己(this)作为参数传递进去。具体访问者模式再根据参数的不同来选择方法来执行。这便完成了第二次分派。
21.5 总结
使用了访问者模式以后,对于原来的类层次增加新的操作,仅仅需要实现一个具体访问者角色就可以了,而不必修改整个类层次。而且这样符合“开闭原则”的要求。而且每个具体的访问者角色都对应于一个相关操作,因此如果一个操作的需求有变,那么仅仅修改一个具体访问者角色,而不用改动整个类层次。
看来访问者模式确实能够解决我们面临的一些问题。
而且由于访问者模式为我们的系统多提供了一层“访问者”,因此我们可以在访问者中添加一些对元素角色的额外操作。
但是“开闭原则”的遵循总是片面的。如果系统中的类层次发生了变化,会对访问者模式产生什么样的影响呢?你必须修改访问者角色和每一个具体访问者角色……
看来访问者角色不适合具体元素角色经常发生变化的情况。而且访问者角色要执行与元素角色相关的操作,就必须让元素角色将自己内部属性暴露出来,而在java中就意味着其它的对象也可以访问。这就破坏了元素角色的封装性。而且在访问者模式中,元素与访问者之间能够传递的信息有限,这往往也会限制访问者模式的使用。
《设计模式》一书中给出了访问者模式适用的情况:
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitor使得你可以将相关的操作集中起来定义在一个类中。
- 当该对象结构被很多应用共享时,用Visitor模式让每个应用仅包含需要用到的操作。
- 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作较好。
二十二、附件 ->《话说分派》
22.1 引言
这篇文章,完全是为了更好地讲解怕【访问者(Visitor)模式】而写的,让我们进入这扑朔迷离的分派世界吧!
22.2 定义
先来解释下“分派”的意思吧
在面向对象语言中(Object Oriented)使用了继承来描述不同的类之间的“社会关系”——类型层次。而这些类实例化的对象们则是对这个类型层次的体系。因此大部分面向对象语言的对象都存在两个身份:静态类型 和 实际类型。
所谓静态类型,就是对象被声明的类型;而实际类型则是创建对象时的类型,如:
// B 是 A的子类,object1的静态类型便是 A,而实际类型却是 B
// 在 Java语言中,编译器会根据对象的静态类型来检查错误;而在运行时,则使用对象的实际类型,即【覆盖】
A object1 = new B();
面向对象还有一个重要的特点:一个类中可以存在两个相同名称的方法,而它们是根据参数类型的不同来区分的,即【重写】。
正是因为以上两个原因,产生了分派——根据类型的不同来选择不同的方法的过程!
22.3 分类
分派可以发送在编译期或者是运行期,因此按照此标准,分派分为【静态分派】和【动态分派】
在程序的编译期,只有对象的静态类型是有效的,因此静态分派就是根据对象(包括参数对象)的静态类型来选择方法的。最典型的便是方法重载(Overload)
在运行期,动态分派会根据对象的实际类型来选择方法,电信的例子便是方法覆盖(Override)
而 面向对象语言 正是由以上两种分派方式来提供多态特性的。按照选择方法时所参照的类型的个数,分派分为【单分派】和【多分派】。面向对象语言也就因此分为了单分派语言(Uni-dispatch) 和 多分派语言(Multi-dispatch)。比如:Smalltalk
就是单分派语言,而 CLOS
和 Ceil
就是多分派语言。
说到多分派,就必须提到另一个概念:多重分派(multiple dispatch),它指的是由多个单分派组成的分派过程(多分派往往是不能分割的),因此单分派语言可以通过多重分派的方式来实现和多分派语言一样的效果。
那么我们熟悉的 Java属于哪一种分派呢?
22.4 Java分派实战
先来看看在 Java中最常见的特性:重载(Overload) 和 重写(Override)
-
下面是一个重载的小例子
public class OverloadTest { public void doSomething(int i) { System.out.println("doSomething int = " + i); } public void doSomething(String s) { System.out.println("doSomething String = " + s); } public void doSomething(int i, String s) { System.out.println("doSomething int = " + i + " String = " + s); } public static void main(String[] args) { OverloadTest test = new OverloadTest(); int i = 1; String s = "abcd"; test.doSomething(i); test.doSomething(s); test.doSomething(i, s); } }
-
下面是重写的小例子
public class OverrideTest { public static void main(String[] args) { Father f = new Father(); Father s = new Son(); f.dost(); s.dost(); } public static class Father { public void dost() { System.out.println("Father ... "); } } public static class Son extends Father { @Override public void dost() { System.out.println("Son ... "); } } }
-
混合 重写 和 重载
-
在编译期间,编译期根据 f、s 的静态类型来为它们选择了方法,当然都选择了父类 Father的方法;
-
而到了运行期,则又根据 s 的实际类型动态的替换了原来选择的父类中的方法
public class OverloadAndOverrideTest { public static void main(String[] args) { Father f = new Father(); Father s = new Son(); f.dost(1); s.dost(4); s.dost("hahahahaaha"); // s.dost("test", 5); } public static class Father { public void dost(int i) { System.out.println("Father ... int i = " + i); } public void dost(String s) { System.out.println("Father ... String = " + s); } } public static class Son extends Father { @Override public void dost(int i) { System.out.println("Son ... int i = " + i); } public void dost(String s, int i) { System.out.println("Father ... String = " + s + " int i = " + i); } } }
-
-
当继承遇到了重载
执行结果并没有像预期那样输出了 ff、sf、fs。为什么?
因为在编译期使用了 s的静态类型为其选择方法,于是这三个调用都选择了第一个方法;
而在运行期,由于 Java仅仅根据方法所属对象的实际类型来分派方法,因此这个“错误”就没有被纠正,而是一直错了下去
public class OverloadAndOverrideMixedTest { public void dost(Father f, Father f1) { System.out.println("ff"); } public void dost(Father f, Son s) { System.out.println("fs"); } public void dost(Son s, Son s2) { System.out.println("ss"); } public void dost(Son s, Father f) { System.out.println("sf"); } public static void main(String[] args) { Father f = new Father(); Father s = new Son(); OverloadAndOverrideMixedTest t = new OverloadAndOverrideMixedTest(); t.dost(f, new Father()); // ff t.dost(f, s); // ff t.dost(s, f); // ff } public static class Father { public void dost(int i) { System.out.println("Father ... int i = " + i); } public void dost(String s) { System.out.println("Father ... String = " + s); } } public static class Son extends Father { @Override public void dost(int i) { System.out.println("Son ... int i = " + i); } public void dost(String s, int i) { System.out.println("Father ... String = " + s + " int i = " + i); } } }
由此可以看出,Java在静态分派时,可以根据 n(n>0)个参数类型来选择不同的方法,这按照上面的定义应该属于【多分派】的范围;
而在运行期时,则只能根据方法所属对象的实际类型来进行方法的选择,这又属于【单分派】的范围
因此可以说 Java语言支持 静态多分派 和 动态单分派
22.5 小插曲
输出的两个变量的值都是 0,因为:
- 【数据是什么】是由编译时决定的;
- 【方法是哪个】是由运行时决定的;
public class DataDispatch {
public static void main(String[] args) {
Father f = new Father();
Father s = new Son();
System.out.println("f.i :" + f.i); // 0
System.out.println("s.i :" + s.i); // 0
f.dost(); // Father
s.dost(); // Son
}
public static class Father {
int i = 0;
public void dost() {
System.out.println("Father");
}
}
public static class Son extends Father {
int i = 9;
@Override
public void dost() {
System.out.println("Son");
}
}
}