浅谈 Java 24个设计模式(23个GoF设计模式 + 简单工厂模式) 之 六个建立型模式...

[list]
[最近在接手一个新的项目,在综合总结以前本身设计的一部分项目架构来看,老是以为一些地方老有些欠缺,或者说不是那么的合理。因而,开始了设计模式之旅。在此,也分享给想学习设计模式的童鞋。因为讲解不少,故分了章节。]
[/list]
[quote]Sunny 24个设计模式[/quote]
[b][size=xx-large]六个建立型模式[/size][/b]

[size=x-large]简单工厂模式-Simple Factory Pattern[/size]
[size=large]简单工厂模式1[/size]
[color=gray]工厂模式是最经常使用的一类建立型设计模式,一般咱们所说的工厂模式是指工厂方法模式,它也是使用频率最高的工厂模式。本章将要学习的简单工厂模式是工厂方法模式的“小弟”,它不属于GoF 23种设计模式,但在软件开发中应用也较为频繁,一般将它做为学习其余工厂模式的入门。此外,工厂方法模式还有一位“大哥”——抽象工厂模式。这三种工厂模式各具特点,难度也逐个加大,在软件开发中它们都获得了普遍的应用,成为面向对象软件中经常使用的建立对象的工具。
1 图表库的设计
例:xxxx公司欲基于Java语言开发一套图表库,该图表库能够为应用系统提供各类不一样外观的图表,例如柱状图、饼状图、折线图等。xxxx公司图表库设计人员但愿为应用系统开发人员提供一套灵活易用的图表库,并且能够较为方便地对图表库进行扩展,以便可以在未来增长一些新类型的图表。
xxxx公司图表库设计人员提出了一个初始设计方案,将全部图表的实现代码封装在一个Chart类中,其框架代码以下所示:[/color]
class Chart {  
private String type; //图表类型

public Chart(Object[][] data, String type) {
this.type = type;
if (type.equalsIgnoreCase("histogram")) {
//初始化柱状图
}
else if (type.equalsIgnoreCase("pie")) {
//初始化饼状图
}
else if (type.equalsIgnoreCase("line")) {
//初始化折线图
}
}

public void display() {
if (this.type.equalsIgnoreCase("histogram")) {
//显示柱状图
}
else if (this.type.equalsIgnoreCase("pie")) {
//显示饼状图
}
else if (this.type.equalsIgnoreCase("line")) {
//显示折线图
}
}
}

[color=gray]客户端代码经过调用Chart类的构造函数来建立图表对象,根据参数type的不一样能够获得不一样类型的图表,而后再调用display()方法来显示相应的图表。
不难看出,Chart类是一个“巨大的”类,在该类的设计中存在以下几个问题:
(1) 在Chart类中包含不少“if…else…”代码块,整个类的代码至关冗长,代码越长,阅读难度、维护难度和测试难度也越大;并且大量条件语句的存在还将影响系统的性能,程序在执行过程当中须要作大量的条件判断。
(2) Chart类的职责太重,它负责初始化和显示全部的图表对象,将各类图表对象的初始化代码和显示代码集中在一个类中实现,违反了“单一职责原则”,不利于类的重用和维护;并且将大量的对象初始化代码都写在构造函数中将致使构造函数很是庞大,对象在建立时须要进行条件判断,下降了对象建立的效率。
(3) 当须要增长新类型的图表时,必须修改Chart类的源代码,违反了“开闭原则”。
(4) 客户端只能经过new关键字来直接建立Chart对象,Chart类与客户端类耦合度较高,对象的建立和使用没法分离。
(5) 客户端在建立Chart对象以前可能还须要进行大量初始化设置,例如设置柱状图的颜色、高度等,若是在Chart类的构造函数中没有提供一个默认设置,那就只能由客户端来完成初始设置,这些代码在每次建立Chart对象时都会出现,致使代码的重复。
面对一个如此巨大、职责如此重,且与客户端代码耦合度很是高的类,咱们应该怎么办?本章将要介绍的简单工厂模式将在必定程度上解决上述问题。[/color]

[size=large]简单工厂模式2[/size]
[color=gray]2 简单工厂模式概述
简单工厂模式并不属于GoF 23个经典设计模式,但一般将它做为学习其余工厂模式的基础,它的设计思想很简单,其基本流程以下:
首先将须要建立的各类不一样对象(例如各类不一样的Chart对象)的相关代码封装到不一样的类中,这些类称为具体产品类,而将它们公共的代码进行抽象和提取后封装在一个抽象产品类中,每个具体产品类都是抽象产品类的子类;而后提供一个工厂类用于建立各类产品,在工厂类中提供一个建立产品的工厂方法,该方法能够根据所传入的参数不一样建立不一样的具体产品对象;客户端只需调用工厂类的工厂方法并传入相应的参数便可获得一个产品对象。
简单工厂模式定义以下:
简单工厂模式(Simple Factory Pattern):定义一个工厂类,它能够根据参数的不一样返回不一样类的实例,被建立的实例一般都具备共同的父类。由于在简单工厂模式中用于建立实例的方法是静态(static)方法,所以简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,它属于类建立型模式。
简单工厂模式的要点在于:当你须要什么,只须要传入一个正确的参数,就能够获取你所须要的对象,而无须知道其建立细节。简单工厂模式结构比较简单,其核心是工厂类的设计,其结构如图1所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7907/c9d120c7-0639-30a4-95e1-d082cfc891d2.jpeg[/img]
[color=gray]图1 简单工厂模式结构图
在简单工厂模式结构图中包含以下几个角色:
● Factory(工厂角色):工厂角色即工厂类,它是简单工厂模式的核心,负责实现建立全部产品实例的内部逻辑;工厂类能够被外界直接调用,建立所需的产品对象;在工厂类中提供了静态的工厂方法factoryMethod(),它的返回类型为抽象产品类型Product。
● Product(抽象产品角色):它是工厂类所建立的全部对象的父类,封装了各类产品对象的公有方法,它的引入将提升系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,由于全部建立的具体产品对象都是其子类对象。
● ConcreteProduct(具体产品角色):它是简单工厂模式的建立目标,全部被建立的对象都充当这个角色的某个具体类的实例。每个具体产品角色都继承了抽象产品角色,须要实如今抽象产品中声明的抽象方法。
在简单工厂模式中,客户端经过工厂类来建立一个产品类的实例,而无须直接使用new关键字来建立对象,它是工厂模式家族中最简单的一员。
在使用简单工厂模式时,首先须要对产品类进行重构,不能设计一个一应俱全的产品类,而需根据实际状况设计一个产品层次结构,将全部产品类公共的代码移至抽象产品类,并在抽象产品类中声明一些抽象方法,以供不一样的具体产品类来实现,典型的抽象产品类代码以下所示:[/color]
abstract class Product {  
//全部产品类的公共业务方法
public void methodSame() {
//公共方法的实现
}

//声明抽象业务方法
public abstract void methodDiff();
}

[color=gray]在具体产品类中实现了抽象产品类中声明的抽象业务方法,不一样的具体产品类能够提供不一样的实现,典型的具体产品类代码以下所示:[/color]
class ConcreteProduct extends Product {  
//实现业务方法
public void methodDiff() {
//业务方法的实现
}
}

[color=gray]简单工厂模式的核心是工厂类,在没有工厂类以前,客户端通常会使用new关键字来直接建立产品对象,而在引入工厂类以后,客户端能够经过工厂类来建立产品,在简单工厂模式中,工厂类提供了一个静态工厂方法供客户端使用,根据所传入的参数不一样能够建立不一样的产品对象,典型的工厂类代码以下所示:
[/color]
class Factory {  
//静态工厂方法
public static Product getProduct(String arg) {
Product product = null;
if (arg.equalsIgnoreCase("A")) {
product = new ConcreteProductA();
//初始化设置product
}
else if (arg.equalsIgnoreCase("B")) {
product = new ConcreteProductB();
//初始化设置product
}
return product;
}
}

[color=gray]在客户端代码中,咱们经过调用工厂类的工厂方法便可获得产品对象,典型代码以下所示:[/color]
class Client {  
public static void main(String args[]) {
Product product;
product = Factory.getProduct("A"); //经过工厂类建立产品对象
product.methodSame();
product.methodDiff();
}
}


[size=large]简单工厂模式3[/size]
[color=gray]3 完整解决方案

为了将Chart类的职责分离,同时将Chart对象的建立和使用分离,xxxx公司开发人员决定使用简单工厂模式对图表库进行重构,重构后的结构如图2所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7911/ff7be9b5-5300-34ac-a1ea-da8804aff68e.jpeg[/img]
[color=gray]图2 图表库结构图

在图2中,Chart接口充当抽象产品类,其子类HistogramChart、PieChart和LineChart充当具体产品类,ChartFactory充当工厂类。完整代码以下所示:[/color]
//抽象图表接口:抽象产品类  
interface Chart {
public void display();
}

//柱状图类:具体产品类
class HistogramChart implements Chart {
public HistogramChart() {
System.out.println("建立柱状图!");
}

public void display() {
System.out.println("显示柱状图!");
}
}

//饼状图类:具体产品类
class PieChart implements Chart {
public PieChart() {
System.out.println("建立饼状图!");
}

public void display() {
System.out.println("显示饼状图!");
}
}

//折线图类:具体产品类
class LineChart implements Chart {
public LineChart() {
System.out.println("建立折线图!");
}

public void display() {
System.out.println("显示折线图!");
}
}

//图表工厂类:工厂类
class ChartFactory {
//静态工厂方法
public static Chart getChart(String type) {
Chart chart = null;
if (type.equalsIgnoreCase("histogram")) {
chart = new HistogramChart();
System.out.println("初始化设置柱状图!");
}
else if (type.equalsIgnoreCase("pie")) {
chart = new PieChart();
System.out.println("初始化设置饼状图!");
}
else if (type.equalsIgnoreCase("line")) {
chart = new LineChart();
System.out.println("初始化设置折线图!");
}
return chart;
}
}

[color=gray]编写以下客户端测试代码:[/color]
class Client {  
public static void main(String args[]) {
Chart chart;
chart = ChartFactory.getChart("histogram"); //经过静态工厂方法建立产品
chart.display();
}
}

[color=gray]编译并运行程序,输出结果以下:[/color]

建立饼状图!
初始化设置饼状图!
显示饼状图!


[size=large]简单工厂模式4[/size]
[color=gray]4 方案的改进
xxxx公司开发人员发如今建立具体Chart对象时,每更换一个Chart对象都须要修改客户端代码中静态工厂方法的参数,客户端代码将要从新编译,这对于客户端而言,违反了“开闭原则”,有没有一种方法可以在不修改客户端代码的前提下更换具体产品对象呢?答案是确定的,下面将介绍一种经常使用的实现方式。
咱们能够将静态工厂方法的参数存储在XML或properties格式的配置文件中,以下config.xml所示:[/color]
<?xml version="1.0"?>  
<config>
<chartType>histogram</chartType>
</config>

[color=gray]再经过一个工具类XMLUtil来读取配置文件中的字符串参数,XMLUtil类的代码以下所示:[/color]
import javax.xml.parsers.*;  
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;

public class XMLUtil {
//该方法用于从XML配置文件中提取图表类型,并返回类型名
public static String getChartType() {
try {
//建立文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml"));

//获取包含图表类型的文本节点
NodeList nl = doc.getElementsByTagName("chartType");
Node classNode = nl.item(0).getFirstChild();
String chartType = classNode.getNodeValue().trim();
return chartType;
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
}

[color=gray]在引入了配置文件和工具类XMLUtil以后,客户端代码修改以下:[/color]
class Client {  
public static void main(String args[]) {
Chart chart;
String type = XMLUtil.getChartType(); //读取配置文件中的参数
chart = ChartFactory.getChart(type); //建立产品对象
chart.display();
}
}

[color=gray]不难发现,在上述客户端代码中不包含任何与具体图表对象相关的信息,若是须要更换具体图表对象,只需修改配置文件config.xml,无须修改任何源代码,符合“开闭原则”。
思考:在简单工厂模式中增长新的具体产品时是否符合“开闭原则”?若是不符合,原有系统需做出哪些修改?[/color]
[color=gray]5 简单工厂模式的简化
有时候,为了简化简单工厂模式,咱们能够将抽象产品类和工厂类合并,将静态工厂方法移至抽象产品类中,如图3所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7923/54643eda-0f95-344c-b211-7bb7cdffc165.jpeg[/img]
[color=gray]图3 简化的简单工厂模式
在图3中,客户端能够经过产品父类的静态工厂方法,根据参数的不一样建立不一样类型的产品子类对象,这种作法在JDK等类库和框架中也普遍存在。
6 简单工厂模式总结
简单工厂模式提供了专门的工厂类用于建立对象,将对象的建立和对象的使用分离开,它做为一种最简单的工厂模式在软件开发中获得了较为普遍的应用。
主要优势
简单工厂模式的主要优势以下:
(1) 工厂类包含必要的判断逻辑,能够决定在何时建立哪个产品类的实例,客户端能够免除直接建立产品对象的职责,而仅仅“消费”产品,简单工厂模式实现了对象建立和使用的分离。
(2) 客户端无须知道所建立的具体产品类的类名,只须要知道具体产品类所对应的参数便可,对于一些复杂的类名,经过简单工厂模式能够在必定程度减小使用者的记忆量。
(3) 经过引入配置文件,能够在不修改任何客户端代码的状况下更换和增长新的具体产品类,在必定程度上提升了系统的灵活性。
主要缺点
简单工厂模式的主要缺点以下:
(1) 因为工厂类集中了全部产品的建立逻辑,职责太重,一旦不能正常工做,整个系统都要受到影响。
(2) 使用简单工厂模式势必会增长系统中类的个数(引入了新的工厂类),增长了系统的复杂度和理解难度。
(3) 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能形成工厂逻辑过于复杂,不利于系统的扩展和维护。
(4) 简单工厂模式因为使用了静态工厂方法,形成工厂角色没法造成基于继承的等级结构。
适用场景
在如下状况下能够考虑使用简单工厂模式:
(1) 工厂类负责建立的对象比较少,因为建立的对象较少,不会形成工厂方法中的业务逻辑太过复杂。
(2) 客户端只知道传入工厂类的参数,对于如何建立对象并不关心。[/color]


[size=x-large]工厂方法模式-Factory Method Pattern[/size]
[size=large]工厂方法模式1[/size]
[color=gray]简单工厂模式虽然简单,但存在一个很严重的问题。当系统中须要引入新产品时,因为静态工厂方法经过所传入参数的不一样来建立不一样的产品,这一定要修改工厂类的源代码,将违背“开闭原则”,如何实现增长新产品而不影响已有代码?工厂方法模式应运而生,本文将介绍第二种工厂模式——工厂方法模式。
1 日志记录器的设计
xxxx公司欲开发一个系统运行日志记录器(Logger),该记录器能够经过多种途径保存系统的运行日志,如经过文件记录或数据库记录,用户能够经过修改配置文件灵活地更换日志记录方式。在设计各种日志记录器时,xxxx公司的开发人员发现须要对日志记录器进行一些初始化工做,初始化参数的设置过程较为复杂,并且某些参数的设置有严格的前后次序,不然可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是xxxx公司开发人员面临的一个难题。
xxxx公司的开发人员经过对该需求进行分析,发现该日志记录器有两个设计要点:
(1) 须要封装日志记录器的初始化过程,这些初始化工做较为复杂,例如须要初始化其余相关的类,还有可能须要读取配置文件(例如链接数据库或建立文件),致使代码较长,若是将它们都写在构造函数中,会致使构造函数庞大,不利于代码的修改和维护;
(2) 用户可能须要更换日志记录方式,在客户端代码中须要提供一种灵活的方式来选择日志记录器,尽可能在不修改源代码的基础上更换或者增长日志记录方式。
xxxx公司开发人员最初使用简单工厂模式对日志记录器进行了设计,初始结构如图1所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7925/7fca4ffa-4e40-3df1-af35-85ca4bea282b.jpeg[/img]
[color=gray]图1 基于简单工厂模式设计的日志记录器结构图
在图1中,LoggerFactory充当建立日志记录器的工厂,提供了工厂方法createLogger()用于建立日志记录器,Logger是抽象日志记录器接口,其子类为具体日志记录器。其中,工厂类LoggerFactory代码片断以下所示:[/color]
//日志记录器工厂  
class LoggerFactory {
//静态工厂方法
public static Logger createLogger(String args) {
if(args.equalsIgnoreCase("db")) {
//链接数据库,代码省略
//建立数据库日志记录器对象
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}
else if(args.equalsIgnoreCase("file")) {
//建立日志文件
//建立文件日志记录器对象
Logger logger = new FileLogger();
//初始化文件日志记录器,代码省略
return logger;
}
else {
return null;
}
}
}

[color=gray]为了突出设计重点,咱们对上述代码进行了简化,省略了具体日志记录器类的初始化代码。在LoggerFactory类中提供了静态工厂方法createLogger(),用于根据所传入的参数建立各类不一样类型的日志记录器。经过使用简单工厂模式,咱们将日志记录器对象的建立和使用分离,客户端只需使用由工厂类建立的日志记录器对象便可,无须关心对象的建立过程,可是咱们发现,虽然简单工厂模式实现了对象的建立和使用分离,可是仍然存在以下两个问题:
(1) 工厂类过于庞大,包含了大量的if…else…代码,致使维护和测试难度增大;
(2) 系统扩展不灵活,若是增长新类型的日志记录器,必须修改静态工厂方法的业务逻辑,违反了“开闭原则”。
如何解决这两个问题,提供一种简单工厂模式的改进方案?这就是本文所介绍的工厂方法模式的动机之一。[/color]

[size=large]工厂方法模式2[/size]
[color=gray]2 工厂方法模式概述
在简单工厂模式中只提供一个工厂类,该工厂类处于对产品类进行实例化的中心位置,它须要知道每个产品对象的建立细节,并决定什么时候实例化哪个产品类。简单工厂模式最大的缺点是当有新产品要加入到系统中时,必须修改工厂类,须要在其中加入必要的业务逻辑,这违背了“开闭原则”。此外,在简单工厂模式中,全部的产品都由同一个工厂建立,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性,而工厂方法模式则能够很好地解决这一问题。
在工厂方法模式中,咱们再也不提供一个统一的工厂类来建立全部的产品对象,而是针对不一样的产品提供不一样的工厂,系统提供一个与产品等级结构对应的工厂等级结构。工厂方法模式定义以下:
工厂方法模式(Factory Method Pattern):定义一个用于建立对象的接口,让子类决定将哪个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式(Factory Pattern),又可称做虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式(Polymorphic Factory Pattern)。工厂方法模式是一种类建立型模式。
工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法,建立具体的产品对象。工厂方法模式结构如图2所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7927/c624eebe-ef57-363b-972e-dcb1935bca2d.jpeg[/img]
[color=gray]图2 工厂方法模式结构图
在工厂方法模式结构图中包含以下几个角色:
● Product(抽象产品):它是定义产品的接口,是工厂方法模式所建立对象的超类型,也就是产品对象的公共父类。
● ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂建立,具体工厂和具体产品之间一一对应。
● Factory(抽象工厂):在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,全部建立对象的工厂类都必须实现该接口。
● ConcreteFactory(具体工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。
与简单工厂模式相比,工厂方法模式最重要的区别是引入了抽象工厂角色,抽象工厂能够是接口,也能够是抽象类或者具体类,其典型代码以下所示:[/color]
interface Factory {  
public Product factoryMethod();
}

[color=gray]在抽象工厂中声明了工厂方法但并未实现工厂方法,具体产品对象的建立由其子类负责,客户端针对抽象工厂编程,可在运行时再指定具体工厂类,具体工厂类实现了工厂方法,不一样的具体工厂能够建立不一样的具体产品,其典型代码以下所示:[/color]
class ConcreteFactory implements Factory {  
public Product factoryMethod() {
return new ConcreteProduct();
}
}

[color=gray]在实际使用时,具体工厂类在实现工厂方法时除了建立具体产品对象以外,还能够负责产品对象的初始化工做以及一些资源和环境配置工做,例如链接数据库、建立文件等。
在客户端代码中,只需关心工厂类便可,不一样的具体工厂能够建立不一样的产品,典型的客户端类代码片断以下所示:[/color]
……  
Factory factory;
factory = new ConcreteFactory(); //可经过配置文件实现
Product product;
product = factory.factoryMethod();
……

[color=gray]能够经过配置文件来存储具体工厂类ConcreteFactory的类名,更换新的具体工厂时无须修改源代码,系统扩展更为方便。
思考

工厂方法模式中的工厂方法可否为静态方法?为何?[/color]

[size=large]工厂方法模式3[/size]
[color=gray]3 完整解决方案
xxxx公司开发人员决定使用工厂方法模式来设计日志记录器,其基本结构如图3所示:[/color]


[img]http://dl2.iteye.com/upload/attachment/0127/7932/a24a54cb-9478-3555-8db6-a8108374ef21.jpeg[/img]


[color=gray]图3 日志记录器结构图
在图3中,Logger接口充当抽象产品,其子类FileLogger和DatabaseLogger充当具体产品,LoggerFactory接口充当抽象工厂,其子类FileLoggerFactory和DatabaseLoggerFactory充当具体工厂。完整代码以下所示:[/color]
//日志记录器接口:抽象产品  
interface Logger {
public void writeLog();
}

//数据库日志记录器:具体产品
class DatabaseLogger implements Logger {
public void writeLog() {
System.out.println("数据库日志记录。");
}
}

//文件日志记录器:具体产品
class FileLogger implements Logger {
public void writeLog() {
System.out.println("文件日志记录。");
}
}

//日志记录器工厂接口:抽象工厂
interface LoggerFactory {
public Logger createLogger();
}

//数据库日志记录器工厂类:具体工厂
class DatabaseLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//链接数据库,代码省略
//建立数据库日志记录器对象
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}
}

//文件日志记录器工厂类:具体工厂
class FileLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//建立文件日志记录器对象
Logger logger = new FileLogger();
//建立文件,代码省略
return logger;
}
}

[color=gray]编写以下客户端测试代码:[/color]
class Client {  
public static void main(String args[]) {
LoggerFactory factory;
Logger logger;
factory = new FileLoggerFactory(); //可引入配置文件实现
logger = factory.createLogger();
logger.writeLog();
}
}

[color=gray]编译并运行程序,输出结果以下:
文件日志记录。
4 反射与配置文件
为了让系统具备更好的灵活性和可扩展性,xxxx公司开发人员决定对日志记录器客户端代码进行重构,使得能够在不修改任何客户端代码的基础上更换或增长新的日志记录方式。
在客户端代码中将再也不使用new关键字来建立工厂对象,而是将具体工厂类的类名存储在配置文件(如XML文件)中,经过读取配置文件获取类名字符串,再使用Java的反射机制,根据类名字符串生成对象。在整个实现过程当中须要用到两个技术:Java反射机制与配置文件读取。软件系统的配置文件一般为XML文件,咱们可使用DOM (Document Object Model)、SAX (Simple API for XML)、StAX (Streaming API for XML)等技术来处理XML文件。关于DOM、SAX、StAX等技术的详细学习你们能够参考其余相关资料,在此不予扩展。
扩展

关于Java与XML的相关资料,你们能够阅读Tom Myers和Alexander Nakhimovsky所著的《Java XML编程指南》一书或访问developer Works 中国中的“Java XML 技术专题”,参考连接: http://www.ibm.com/developerworks/cn/xml/theme/x-java.html
Java反射(Java Reflection)是指在程序运行时获取已知名称的类或已有对象的相关信息的一种机制,包括类的方法、属性、父类等信息,还包括实例的建立和实例类型的判断等。在反射中使用最多的类是Class,Class类的实例表示正在运行的Java应用程序中的类和接口,其forName(String className)方法能够返回与带有给定字符串名的类或接口相关联的 Class对象,再经过Class对象的newInstance()方法建立此对象所表示的类的一个新实例,即经过一个类名字符串获得类的实例。如建立一个字符串类型的对象,其代码以下:[/color]
//经过类名生成实例对象并将其返回  
Class c=Class.forName("String");
Object obj=c.newInstance();
return obj;

[color=gray]此外,在JDK中还提供了java.lang.reflect包,封装了其余与反射相关的类,此处只用到上述简单的反射代码,在此不予扩展。xxxx公司开发人员建立了以下XML格式的配置文件config.xml用于存储具体日志记录器工厂类类名:[/color]
<!— config.xml -->  
<?xml version="1.0"?>
<config>
<className>FileLoggerFactory</className>
</config>

[color=gray]为了读取该配置文件并经过存储在其中的类名字符串反射生成对象,xxxx公司开发人员开发了一个名为XMLUtil的工具类,其详细代码以下所示:[/color]
//工具类XMLUtil.java  
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;

public class XMLUtil {
//该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
public static Object getBean() {
try {
//建立DOM文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml"));

//获取包含类名的文本节点
NodeList nl = doc.getElementsByTagName("className");
Node classNode=nl.item(0).getFirstChild();
String cName=classNode.getNodeValue();

//经过类名生成实例对象并将其返回
Class c=Class.forName(cName);
Object obj=c.newInstance();
return obj;
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
}

[color=gray]有了XMLUtil类后,能够对日志记录器的客户端代码进行修改,再也不直接使用new关键字来建立具体的工厂类,而是将具体工厂类的类名存储在XML文件中,再经过XMLUtil类的静态工厂方法getBean()方法进行对象的实例化,代码修改以下:[/color]
class Client {  
public static void main(String args[]) {
LoggerFactory factory;
Logger logger;
factory = (LoggerFactory)XMLUtil.getBean(); //getBean()的返回类型为Object,须要进行强制类型转换
logger = factory.createLogger();
logger.writeLog();
}
}

[color=gray]引入XMLUtil类和XML配置文件后,若是要增长新的日志记录方式,只须要执行以下几个步骤:
(1) 新的日志记录器须要继承抽象日志记录器Logger;
(2) 对应增长一个新的具体日志记录器工厂,继承抽象日志记录器工厂LoggerFactory,并实现其中的工厂方法createLogger(),设置好初始化参数和环境变量,返回具体日志记录器对象;
(3) 修改配置文件config.xml,将新增的具体日志记录器工厂类的类名字符串替换原有工厂类类名字符串;
(4) 编译新增的具体日志记录器类和具体日志记录器工厂类,运行客户端测试类便可使用新的日志记录方式,而原有类库代码无须作任何修改,彻底符合“开闭原则”。
经过上述重构可使得系统更加灵活,因为不少设计模式都关注系统的可扩展性和灵活性,所以都定义了抽象层,在抽象层中声明业务方法,而将业务方法的实现放在实现层中。 疑问 思考
有人说:能够在客户端代码中直接经过反射机制来生成产品对象,在定义产品对象时使用抽象类型,一样能够确保系统的灵活性和可扩展性,增长新的具体产品类无须修改源代码,只须要将其做为抽象产品类的子类再修改配置文件便可,根本不须要抽象工厂类和具体工厂类。[/color]

[size=large]工厂方法模式4[/size]
[color=gray]5 重载的工厂方法
xxxx公司开发人员经过进一步分析,发现能够经过多种方式来初始化日志记录器,例如能够为各类日志记录器提供默认实现;还能够为数据库日志记录器提供数据库链接字符串,为文件日志记录器提供文件路径;也能够将参数封装在一个Object类型的对象中,经过Object对象将配置参数传入工厂类。此时,能够提供一组重载的工厂方法,以不一样的方式对产品对象进行建立。固然,对于同一个具体工厂而言,不管使用哪一个工厂方法,建立的产品类型均要相同。如图4所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7934/40d4a016-630f-3460-8db3-ba584aa33bd0.jpeg[/img]
[color=gray]图4 重载的工厂方法结构图
引入重载方法后,抽象工厂LoggerFactory的代码修改以下:[/color]
interface LoggerFactory {  
public Logger createLogger();
public Logger createLogger(String args);
public Logger createLogger(Object obj);
}

[color=gray]具体工厂类DatabaseLoggerFactory代码修改以下:[/color]
class DatabaseLoggerFactory implements LoggerFactory {  
public Logger createLogger() {
//使用默认方式链接数据库,代码省略
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}

public Logger createLogger(String args) {
//使用参数args做为链接字符串来链接数据库,代码省略
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}

public Logger createLogger(Object obj) {
//使用封装在参数obj中的链接字符串来链接数据库,代码省略
Logger logger = new DatabaseLogger();
//使用封装在参数obj中的数据来初始化数据库日志记录器,代码省略
return logger;
}
}

//其余具体工厂类代码省略

[color=gray]在抽象工厂中定义多个重载的工厂方法,在具体工厂中实现了这些工厂方法,这些方法能够包含不一样的业务逻辑,以知足对不一样产品对象的需求。
6 工厂方法的隐藏
有时候,为了进一步简化客户端的使用,还能够对客户端隐藏工厂方法,此时,在工厂类中将直接调用产品类的业务方法,客户端无须调用工厂方法建立产品,直接经过工厂便可使用所建立的对象中的业务方法。
若是对客户端隐藏工厂方法,日志记录器的结构图将修改成图5所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7936/2ba5b9eb-9983-325e-b1fc-3b5ba5af3106.jpeg[/img]
[color=gray]图5 隐藏工厂方法后的日志记录器结构图
在图5中,抽象工厂类LoggerFactory的代码修改以下:[/color]
//改成抽象类  
abstract class LoggerFactory {
//在工厂类中直接调用日志记录器类的业务方法writeLog()
public void writeLog() {
Logger logger = this.createLogger();
logger.writeLog();
}

public abstract Logger createLogger();
}

[color=gray]客户端代码修改以下:[/color]
class Client {  
public static void main(String args[]) {
LoggerFactory factory;
factory = (LoggerFactory)XMLUtil.getBean();
factory.writeLog(); //直接使用工厂对象来调用产品对象的业务方法
}
}

[color=gray]经过将业务方法的调用移入工厂类,能够直接使用工厂对象来调用产品对象的业务方法,客户端无须直接使用工厂方法,在某些状况下咱们也可使用这种设计方案。
7 工厂方法模式总结
工厂方法模式是简单工厂模式的延伸,它继承了简单工厂模式的优势,同时还弥补了简单工厂模式的不足。工厂方法模式是使用频率最高的设计模式之一,是不少开源框架和API类库的核心模式。
主要优势
工厂方法模式的主要优势以下:
(1) 在工厂方法模式中,工厂方法用来建立客户所须要的产品,同时还向客户隐藏了哪一种具体产品类将被实例化这一细节,用户只须要关心所需产品对应的工厂,无须关心建立细节,甚至无须知道具体产品类的类名。
(2) 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它可以让工厂能够自主肯定建立何种产品对象,而如何建立这个对象的细节则彻底封装在具体工厂内部。工厂方法模式之因此又被称为多态工厂模式,就正是由于全部的具体工厂类都具备同一抽象父类。
(3) 使用工厂方法模式的另外一个优势是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其余的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就能够了,这样,系统的可扩展性也就变得很是好,彻底符合“开闭原则”。
主要缺点
工厂方法模式的主要缺点以下:
(1) 在添加新产品时,须要编写新的具体产品类,并且还要提供与之对应的具体工厂类,系统中类的个数将成对增长,在必定程度上增长了系统的复杂度,有更多的类须要编译和运行,会给系统带来一些额外的开销。
(2) 因为考虑到系统的可扩展性,须要引入抽象层,在客户端代码中均使用抽象层进行定义,增长了系统的抽象性和理解难度,且在实现时可能须要用到DOM、反射等技术,增长了系统的实现难度。
适用场景
在如下状况下能够考虑使用工厂方法模式:
(1) 客户端不知道它所须要的对象的类。在工厂方法模式中,客户端不须要知道具体产品类的类名,只须要知道所对应的工厂便可,具体的产品对象由具体工厂类建立,可将具体工厂类的类名存储在配置文件或数据库中。
(2) 抽象工厂类经过其子类来指定建立哪一个对象。在工厂方法模式中,对于抽象工厂类只须要提供一个建立产品的接口,而由其子类来肯定具体要建立的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。[/color]

[size=x-large]抽象工厂模式-Abstract Factory Pattern[/size]
[size=large]抽象工厂模式1[/size]
[color=gray]工厂方法模式经过引入工厂等级结构,解决了简单工厂模式中工厂类职责过重的问题,但因为工厂方法模式中的每一个工厂只生产一类产品,可能会致使系统中存在大量的工厂类,势必会增长系统的开销。此时,咱们能够考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一辈子产,这就是咱们本文将要学习的抽象工厂模式的基本思想。
1 界面皮肤库的初始设计
xxxx公司欲开发一套界面皮肤库,能够对Java桌面软件进行界面美化。为了保护版权,该皮肤库源代码不打算公开,而只向用户提供已打包为jar文件的class字节码文件。用户在使用时能够经过菜单来选择皮肤,不一样的皮肤将提供视觉效果不一样的按钮、文本框、组合框等界面元素,其结构示意图如图1所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7938/f8e2d0be-c208-3e28-862b-1a1d983d6744.jpeg[/img]
[color=gray]图1 界面皮肤库结构示意图
该皮肤库须要具有良好的灵活性和可扩展性,用户能够自由选择不一样的皮肤,开发人员能够在不修改既有代码的基础上增长新的皮肤。
xxxx公司的开发人员针对上述要求,决定使用工厂方法模式进行系统的设计,为了保证系统的灵活性和可扩展性,提供一系列具体工厂来建立按钮、文本框、组合框等界面元素,客户端针对抽象工厂编程,初始结构如图2所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7940/c334b03b-9139-31e7-964e-4366c2d4eab5.jpeg[/img]
[color=gray]图2 基于工厂方法模式的界面皮肤库初始结构图
在图2中,提供了大量工厂来建立具体的界面组件,能够经过配置文件更换具体界面组件从而改变界面风格。可是,此设计方案存在以下问题:
(1) 当须要增长新的皮肤时,虽然不要修改现有代码,可是须要增长大量类,针对每个新增具体组件都须要增长一个具体工厂,类的个数成对增长,这无疑会致使系统愈来愈庞大,增长系统的维护成本和运行开销;
(2) 因为同一种风格的具体界面组件一般要一块儿显示,所以须要为每一个组件都选择一个具体工厂,用户在使用时必须逐个进行设置,若是某个具体工厂选择失误将会致使界面显示混乱,虽然咱们能够适当增长一些约束语句,但客户端代码和配置文件都较为复杂。
如何减小系统中类的个数并保证客户端每次始终只使用某一种风格的具体界面组件?这是xxxx公司开发人员所面临的两个问题,显然,工厂方法模式没法解决这两个问题,别着急,本文所介绍的抽象工厂模式可让这些问题迎刃而解。[/color]

[size=large]抽象工厂模式2[/size]
[color=gray]2 产品等级结构与产品族
在工厂方法模式中具体工厂负责生产具体的产品,每个具体工厂对应一种具体产品,工厂方法具备惟一性,通常状况下,一个具体工厂中只有一个或者一组重载的工厂方法。可是有时候咱们但愿一个工厂能够提供多个产品对象,而不是单一的产品对象,如一个电器工厂,它能够生产电视机、电冰箱、空调等多种电器,而不是只生产某一种电器。为了更好地理解抽象工厂模式,咱们先引入两个概念:
(1) 产品等级结构:产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
(2) 产品族:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不一样产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中,海尔电视机、海尔电冰箱构成了一个产品族。
产品等级结构与产品族示意图如图3所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7942/3186c596-70bb-3959-8565-a591ba445bbe.jpeg[/img]
[color=gray]图3 产品族与产品等级结构示意图
在图3中,不一样颜色的多个正方形、圆形和椭圆形分别构成了三个不一样的产品等级结构,而相同颜色的正方形、圆形和椭圆形构成了一个产品族,每个形状对象都位于某个产品族,并属于某个产品等级结构。图3中一共有五个产品族,分属于三个不一样的产品等级结构。咱们只要指明一个产品所处的产品族以及它所属的等级结构,就能够惟一肯定这个产品。
当系统所提供的工厂生产的具体产品并非一个简单的对象,而是多个位于不一样产品等级结构、属于不一样类型的具体产品时就可使用抽象工厂模式。抽象工厂模式是全部形式的工厂模式中最为抽象和最具通常性的一种形式。抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式须要面对多个产品等级结构,一个工厂等级结构能够负责多个不一样产品等级结构中的产品对象的建立。当一个工厂等级结构能够建立出分属于不一样产品等级结构的一个产品族中的全部对象时,抽象工厂模式比工厂方法模式更为简单、更有效率。抽象工厂模式示意图如图4所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7944/492d667e-b06b-3d35-b6c3-b41ace95e24a.jpeg[/img]
[color=gray]图4 抽象工厂模式示意图
在图4中,每个具体工厂能够生产属于一个产品族的全部产品,例如生产颜色相同的正方形、圆形和椭圆形,所生产的产品又位于不一样的产品等级结构中。若是使用工厂方法模式,图4所示结构须要提供15个具体工厂,而使用抽象工厂模式只须要提供5个具体工厂,极大减小了系统中类的个数。[/color]

[size=large]抽象工厂模式3[/size]
[color=gray]3 抽象工厂模式概述
抽象工厂模式为建立一组对象提供了一种解决方案。与工厂方法模式相比,抽象工厂模式中的具体工厂不仅是建立一种产品,它负责建立一族产品。抽象工厂模式定义以下:
抽象工厂模式(Abstract Factory Pattern):提供一个建立一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象建立型模式。
在抽象工厂模式中,每个具体工厂都提供了多个工厂方法用于产生多种不一样类型的产品,这些产品构成了一个产品族,抽象工厂模式结构如图5所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7946/b3437107-7233-3118-b3c8-e45aa84f082a.jpeg[/img]
[color=gray]图5 抽象工厂模式结构图
在抽象工厂模式结构图中包含以下几个角色:
● AbstractFactory(抽象工厂):它声明了一组用于建立一族产品的方法,每个方法对应一种产品。
● ConcreteFactory(具体工厂):它实现了在抽象工厂中声明的建立产品的方法,生成一组具体产品,这些产品构成了一个产品族,每个产品都位于某个产品等级结构中。
● AbstractProduct(抽象产品):它为每种产品声明接口,在抽象产品中声明了产品所具备的业务方法。
● ConcreteProduct(具体产品):它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。
在抽象工厂中声明了多个工厂方法,用于建立不一样类型的产品,抽象工厂能够是接口,也能够是抽象类或者具体类,其典型代码以下所示:[/color]
abstract class AbstractFactory {  
public abstract AbstractProductA createProductA(); //工厂方法一
public abstract AbstractProductB createProductB(); //工厂方法二
……
}

[color=gray]具体工厂实现了抽象工厂,每个具体的工厂方法能够返回一个特定的产品对象,而同一个具体工厂所建立的产品对象构成了一个产品族。对于每个具体工厂类,其典型代码以下所示:[/color]
class ConcreteFactory1 extends AbstractFactory {  
//工厂方法一
public AbstractProductA createProductA() {
return new ConcreteProductA1();
}

//工厂方法二
public AbstractProductB createProductB() {
return new ConcreteProductB1();
}

……
}

[color=gray]与工厂方法模式同样,抽象工厂模式也可为每一种产品提供一组重载的工厂方法,以不一样的方式对产品对象进行建立。
思考

抽象工厂模式是否符合“开闭原则”?【从增长新的产品等级结构和增长新的产品族两方面进行思考。】[/color]

[size=large]抽象工厂模式4[/size]
[color=gray]4 完整解决方案
xxxx公司开发人员使用抽象工厂模式来重构界面皮肤库的设计,其基本结构如图6所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7948/ad6969a3-6235-3685-97fc-5150bdab55e4.jpeg[/img]
[color=gray]图6 界面皮肤库结构图
在图6中,SkinFactory接口充当抽象工厂,其子类SpringSkinFactory和SummerSkinFactory充当具体工厂,接口Button、TextField和ComboBox充当抽象产品,其子类SpringButton、SpringTextField、SpringComboBox和SummerButton、SummerTextField、SummerComboBox充当具体产品。完整代码以下所示:[/color]
//在本实例中咱们对代码进行了大量简化,实际使用时,界面组件的初始化代码较为复杂,还须要使用JDK中一些已有类,为了突出核心代码,在此只提供框架代码和演示输出。  
//按钮接口:抽象产品
interface Button {
public void display();
}

//Spring按钮类:具体产品
class SpringButton implements Button {
public void display() {
System.out.println("显示浅绿色按钮。");
}
}

//Summer按钮类:具体产品
class SummerButton implements Button {
public void display() {
System.out.println("显示浅蓝色按钮。");
}
}

//文本框接口:抽象产品
interface TextField {
public void display();
}

//Spring文本框类:具体产品
class SpringTextField implements TextField {
public void display() {
System.out.println("显示绿色边框文本框。");
}
}

//Summer文本框类:具体产品
class SummerTextField implements TextField {
public void display() {
System.out.println("显示蓝色边框文本框。");
}
}

//组合框接口:抽象产品
interface ComboBox {
public void display();
}

//Spring组合框类:具体产品
class SpringComboBox implements ComboBox {
public void display() {
System.out.println("显示绿色边框组合框。");
}
}

//Summer组合框类:具体产品
class SummerComboBox implements ComboBox {
public void display() {
System.out.println("显示蓝色边框组合框。");
}
}

//界面皮肤工厂接口:抽象工厂
interface SkinFactory {
public Button createButton();
public TextField createTextField();
public ComboBox createComboBox();
}

//Spring皮肤工厂:具体工厂
class SpringSkinFactory implements SkinFactory {
public Button createButton() {
return new SpringButton();
}

public TextField createTextField() {
return new SpringTextField();
}

public ComboBox createComboBox() {
return new SpringComboBox();
}
}

//Summer皮肤工厂:具体工厂
class SummerSkinFactory implements SkinFactory {
public Button createButton() {
return new SummerButton();
}

public TextField createTextField() {
return new SummerTextField();
}

public ComboBox createComboBox() {
return new SummerComboBox();
}
}

[color=gray]为了让系统具有良好的灵活性和可扩展性,咱们引入了工具类XMLUtil和配置文件,其中,XMLUtil类的代码以下所示:[/color]
import javax.xml.parsers.*;  
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;

public class XMLUtil {
//该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
public static Object getBean() {
try {
//建立文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml"));

//获取包含类名的文本节点
NodeList nl = doc.getElementsByTagName("className");
Node classNode=nl.item(0).getFirstChild();
String cName=classNode.getNodeValue();

//经过类名生成实例对象并将其返回
Class c=Class.forName(cName);
Object obj=c.newInstance();
return obj;
}
catch(Exception e) {
e.printStackTrace();
return null;
}
}
}

[color=gray]配置文件config.xml中存储了具体工厂类的类名,代码以下所示:[/color]
<?xml version="1.0"?>  
<config>
<className>SpringSkinFactory</className>
</config>
编写以下客户端测试代码:
[java] view plain copy
class Client {
public static void main(String args[]) {
//使用抽象层定义
SkinFactory factory;
Button bt;
TextField tf;
ComboBox cb;
factory = (SkinFactory)XMLUtil.getBean();
bt = factory.createButton();
tf = factory.createTextField();
cb = factory.createComboBox();
bt.display();
tf.display();
cb.display();
}
}

[color=gray]编译并运行程序,输出结果以下:[/color]

显示浅绿色按钮。
显示绿色边框文本框。
显示绿色边框组合框。

[color=gray]若是须要更换皮肤,只需修改配置文件便可,在实际环境中,咱们能够提供可视化界面,例如菜单或者窗口来修改配置文件,用户无须直接修改配置文件。若是须要增长新的皮肤,只需增长一族新的具体组件并对应提供一个新的具体工厂,修改配置文件便可使用新的皮肤,原有代码无须修改,符合“开闭原则”。
扩展

在真实项目开发中,咱们一般会为配置文件提供一个可视化的编辑界面,相似Struts框架中的struts.xml编辑器,你们能够自行开发一个简单的图形化工具来修改配置文件,实现真正的纯界面操做。[/color]

[size=large]抽象工厂模式5[/size]
[color=gray]5 “开闭原则”的倾斜性
xxxx公司使用抽象工厂模式设计了界面皮肤库,该皮肤库能够较为方便地增长新的皮肤,可是如今遇到一个很是严重的问题:因为设计时考虑不全面,忘记为单选按钮(RadioButton)提供不一样皮肤的风格化显示,致使不管选择哪一种皮肤,单选按钮都显得那么“格格不入”。xxxx公司的设计人员决定向系统中增长单选按钮,可是发现原有系统竟然不可以在符合“开闭原则”的前提下增长新的组件,缘由是抽象工厂SkinFactory中根本没有提供建立单选按钮的方法,若是须要增长单选按钮,首先须要修改抽象工厂接口SkinFactory,在其中新增声明建立单选按钮的方法,而后逐个修改具体工厂类,增长相应方法以实如今不一样的皮肤中建立单选按钮,此外还须要修改客户端,不然单选按钮没法应用于现有系统。
怎么办?答案是抽象工厂模式没法解决该问题,这也是抽象工厂模式最大的缺点。在抽象工厂模式中,增长新的产品族很方便,可是增长新的产品等级结构很麻烦,抽象工厂模式的这种性质称为“开闭原则”的倾斜性。“开闭原则”要求系统对扩展开放,对修改封闭,经过扩展达到加强其功能的目的,对于涉及到多个产品族与多个产品等级结构的系统,其功能加强包括两方面:
(1) 增长产品族:对于增长新的产品族,抽象工厂模式很好地支持了“开闭原则”,只须要增长具体产品并对应增长一个新的具体工厂,对已有代码无须作任何修改。
(2) 增长新的产品等级结构:对于增长新的产品等级结构,须要修改全部的工厂角色,包括抽象工厂类,在全部的工厂类中都须要增长生产新产品的方法,违背了“开闭原则”。
正由于抽象工厂模式存在“开闭原则”的倾斜性,它以一种倾斜的方式来知足“开闭原则”,为增长新产品族提供方便,但不能为增长新产品结构提供这样的方便,所以要求设计人员在设计之初就可以全面考虑,不会在设计完成以后向系统中增长新的产品等级结构,也不会删除已有的产品等级结构,不然将会致使系统出现较大的修改,为后续维护工做带来诸多麻烦。
6 抽象工厂模式总结
抽象工厂模式是工厂方法模式的进一步延伸,因为它提供了功能更为强大的工厂类而且具有较好的可扩展性,在软件开发中得以普遍应用,尤为是在一些框架和API类库的设计中,例如在Java语言的AWT(抽象窗口工具包)中就使用了抽象工厂模式,它使用抽象工厂模式来实如今不一样的操做系统中应用程序呈现与所在操做系统一致的外观界面。抽象工厂模式也是在软件开发中最经常使用的设计模式之一。
主要优势
抽象工厂模式的主要优势以下:
(1) 抽象工厂模式隔离了具体类的生成,使得客户并不须要知道什么被建立。因为这种隔离,更换一个具体工厂就变得相对容易,全部的具体工厂都实现了抽象工厂中定义的那些公共接口,所以只需改变具体工厂的实例,就能够在某种程度上改变整个软件系统的行为。
(2) 当一个产品族中的多个对象被设计成一块儿工做时,它可以保证客户端始终只使用同一个产品族中的对象。
(3) 增长新的产品族很方便,无须修改已有系统,符合“开闭原则”。
主要缺点
抽象工厂模式的主要缺点以下:
增长新的产品等级结构麻烦,须要对原有系统进行较大的修改,甚至须要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。
适用场景
在如下状况下能够考虑使用抽象工厂模式:
(1) 一个系统不该当依赖于产品类实例如何被建立、组合和表达的细节,这对于全部类型的工厂模式都是很重要的,用户无须关心对象的建立过程,将对象的建立和使用解耦。
(2) 系统中有多于一个的产品族,而每次只使用其中某一产品族。能够经过配置文件等方式来使得用户能够动态改变产品族,也能够很方便地增长新的产品族。
(3) 属于同一个产品族的产品将在一块儿使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品能够是没有任何关系的对象,可是它们都具备一些共同的约束,如同一操做系统下的按钮和文本框,按钮与文本框之间没有直接关系,但它们都是属于某一操做系统的,此时具备一个共同的约束条件:操做系统的类型。
(4) 产品等级结构稳定,设计完成以后,不会向系统中增长新的产品等级结构或者删除已有的产品等级结构。[/color]


[size=x-large]单例模式-Singleton Pattern(用于确保对象的惟一性)[/size]
[size=large]单例模式1[/size]
[color=gray]3.1 单例模式的动机
对于一个软件系统的某些类而言,咱们无须建立多个实例。举个你们都熟知的例子——Windows任务管理器,如图3-1所示,咱们能够作一个这样的尝试,在Windows的“任务栏”的右键弹出菜单上屡次点击“启动任务管理器”,看可否打开多个任务管理器窗口?若是你的桌面出现多个任务管理器,我请你吃饭,微笑(注:电脑中毒或私自修改Windows内核者除外)。一般状况下,不管咱们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在惟一性。为何要这样设计呢?咱们能够从如下两个方面来分析:其一,若是能弹出多个窗口,且这些窗口的内容彻底一致,所有是重复对象,这势必会浪费系统资源,任务管理器须要获取系统运行时的诸多信息,这些信息的获取须要消耗必定的系统资源,包括CPU资源及内存资源等,浪费是可耻的,并且根本没有必要显示多个内容彻底相同的窗口;其二,若是弹出的多个窗口内容不一致,问题就更加严重了,这意味着在某一瞬间系统资源使用状况和进程、服务等信息存在多个状态,例如任务管理器窗口A显示“CPU使用率”为10%,窗口B显示“CPU使用率”为15%,到底哪一个才是真实的呢?这纯属“调戏”用户,偷笑,给用户带来误解,更不可取。因而可知,确保Windows任务管理器在系统中有且仅有一个很是重要。[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7950/7e523178-318a-3049-8a9b-04116eabc96c.gif[/img]
[color=gray]图3-1 Windows任务管理器
回到实际开发中,咱们也常常遇到相似的状况,为了节约系统资源,有时须要确保系统中某个类只有惟一一个实例,当这个惟一实例建立成功以后,咱们没法再建立一个同类型的其余对象,全部的操做都只能基于这个惟一实例。为了确保对象的惟一性,咱们能够经过单例模式来实现,这就是单例模式的动机所在。
3.2 单例模式概述
下面咱们来模拟实现Windows任务管理器,假设任务管理器的类名为TaskManager,在TaskManager类中包含了大量的成员方法,例如构造函数TaskManager(),显示进程的方法displayProcesses(),显示服务的方法displayServices()等,该类的示意代码以下:[/color]
class TaskManager  
{
public TaskManager() {……} //初始化窗口
public void displayProcesses() {……} //显示进程
public void displayServices() {……} //显示服务
……
}

[color=gray]为了实现Windows任务管理器的惟一性,咱们经过以下三步来对该类进行重构:
(1) 因为每次使用new关键字来实例化TaskManager类时都将产生一个新对象,为了确保TaskManager实例的惟一性,咱们须要禁止类的外部直接使用new来建立对象,所以须要将TaskManager的构造函数的可见性改成private,以下代码所示:[/color]
private TaskManager() {……}

[color=gray](2) 将构造函数改成private修饰后该如何建立对象呢?不要着急,虽然类的外部没法再使用new来建立对象,可是在TaskManager的内部仍是能够建立的,可见性只对类外有效。所以,咱们能够在TaskManager中建立并保存这个惟一实例。为了让外界能够访问这个惟一实例,须要在TaskManager中定义一个静态的TaskManager类型的私有成员变量,以下代码所示:[/color]
private static TaskManager tm = null;

[color=gray](3) 为了保证成员变量的封装性,咱们将TaskManager类型的tm对象的可见性设置为private,但外界该如何使用该成员变量并什么时候实例化该成员变量呢?答案是增长一个公有的静态方法,以下代码所示:[/color]
public static TaskManager getInstance()  
{
if (tm == null)
{
tm = new TaskManager();
}
return tm;
}

[color=gray]
在getInstance()方法中首先判断tm对象是否存在,若是不存在(即tm == null),则使用new关键字建立一个新的TaskManager类型的tm对象,再返回新建立的tm对象;不然直接返回已有的tm对象。
须要注意的是getInstance()方法的修饰符,首先它应该是一个public方法,以便供外界其余对象使用,其次它使用了static关键字,即它是一个静态方法,在类外能够直接经过类名来访问,而无须建立TaskManager对象,事实上在类外也没法建立TaskManager对象,由于构造函数是私有的。
思考

为何要将成员变量tm定义为静态变量?
经过以上三个步骤,咱们完成了一个最简单的单例类的设计,其完整代码以下:[/color]
class TaskManager  
{
private static TaskManager tm = null;
private TaskManager() {……} //初始化窗口
public void displayProcesses() {……} //显示进程
public void displayServices() {……} //显示服务
public static TaskManager getInstance()
{
if (tm == null)
{
tm = new TaskManager();
}
return tm;
}
……
}

[color=gray]在类外咱们没法直接建立新的TaskManager对象,但能够经过代码TaskManager.getInstance()来访问实例对象,第一次调用getInstance()方法时将建立惟一实例,再次调用时将返回第一次建立的实例,从而确保实例对象的惟一性。
上述代码也是单例模式的一种最典型实现方式,有了以上基础,理解单例模式的定义和结构就很是容易了。单例模式定义以下: 单例模式(Singleton Pattern):确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象建立型模式。
单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行建立这个实例;三是它必须自行向整个系统提供这个实例。
单例模式是结构最简单的设计模式一,在它的核心结构中只包含一个被称为单例类的特殊类。单例模式结构如图3-2所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7952/7e0717eb-8d0a-3ccb-b88b-1524c91b5543.gif[/img]
[color=gray]单例模式结构图中只包含一个单例角色:
● Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户能够访问它的惟一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,做为外部共享的惟一实例。[/color]

[size=large]单例模式2[/size]
[color=gray]3.3 负载均衡器的设计与实现
xxxx公司承接了一个服务器负载均衡(Load Balance)软件的开发工做,该软件运行在一台负载均衡服务器上,能够将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提升系统的总体处理能力,缩短响应时间。因为集群中的服务器须要动态删减,且客户端请求须要统一分发,所以须要确保负载均衡器的惟一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,不然将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的惟一性是该软件成功的关键。
xxxx公司开发人员经过分析和权衡,决定使用单例模式来设计该负载均衡器,结构图如图3-3所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7954/38deca01-7300-3418-9228-0d27a1ef328b.gif[/img]
[color=gray]在图3-3中,将负载均衡器LoadBalancer设计为单例类,其中包含一个存储服务器信息的集合serverList,每次在serverList中随机选择一台服务器来响应客户端的请求,实现代码以下所示:[/color]
import java.util.*;  

//负载均衡器LoadBalancer:单例类,真实环境下该类将很是复杂,包括大量初始化的工做和业务方法,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class LoadBalancer {
//私有静态成员变量,存储惟一实例
private static LoadBalancer instance = null;
//服务器集合
private List serverList = null;

//私有构造函数
private LoadBalancer() {
serverList = new ArrayList();
}

//公有静态成员方法,返回惟一实例
public static LoadBalancer getLoadBalancer() {
if (instance == null) {
instance = new LoadBalancer();
}
return instance;
}

//增长服务器
public void addServer(String server) {
serverList.add(server);
}

//删除服务器
public void removeServer(String server) {
serverList.remove(server);
}

//使用Random类随机获取服务器
public String getServer() {
Random random = new Random();
int i = random.nextInt(serverList.size());
return (String)serverList.get(i);
}
}

[color=gray]编写以下客户端测试代码:[/color]
class Client {  
public static void main(String args[]) {
//建立四个LoadBalancer对象
LoadBalancer balancer1,balancer2,balancer3,balancer4;
balancer1 = LoadBalancer.getLoadBalancer();
balancer2 = LoadBalancer.getLoadBalancer();
balancer3 = LoadBalancer.getLoadBalancer();
balancer4 = LoadBalancer.getLoadBalancer();

//判断服务器负载均衡器是否相同
if (balancer1 == balancer2 && balancer2 == balancer3 && balancer3 == balancer4) {
System.out.println("服务器负载均衡器具备惟一性!");
}

//增长服务器
balancer1.addServer("Server 1");
balancer1.addServer("Server 2");
balancer1.addServer("Server 3");
balancer1.addServer("Server 4");

//模拟客户端请求的分发
for (int i = 0; i < 10; i++) {
String server = balancer1.getServer();
System.out.println("分发请求至服务器: " + server);
}
}
}

[color=gray]编译并运行程序,输出结果以下:
服务器负载均衡器具备惟一性![/color]
分发请求至服务器:  Server 1
分发请求至服务器: Server 3
分发请求至服务器: Server 4
分发请求至服务器: Server 2
分发请求至服务器: Server 3
分发请求至服务器: Server 2
分发请求至服务器: Server 3
分发请求至服务器: Server 4
分发请求至服务器: Server 4
分发请求至服务器: Server 1

[color=gray]虽然建立了四个LoadBalancer对象,可是它们其实是同一个对象,所以,经过使用单例模式能够确保LoadBalancer对象的惟一性。[/color]

[size=large]单例模式3[/size]
[color=gray]3.4 饿汉式单例与懒汉式单例的讨论
xxxx公司开发人员使用单例模式实现了负载均衡器的设计,可是在实际使用中出现了一个很是严重的问题,当负载均衡器在启动过程当中用户再次启动该负载均衡器时,系统无任何异常,但当客户端提交请求时出现请求分发失败,经过仔细分析发现原来系统中仍是存在多个负载均衡器对象,致使分发时目标服务器不一致,从而产生冲突。为何会这样呢?xxxx公司开发人员百思不得其解。
如今咱们对负载均衡器的实现代码进行再次分析,当第一次调用getLoadBalancer()方法建立并启动负载均衡器时,instance对象为null值,所以系统将执行代码instance= new LoadBalancer(),在此过程当中,因为要对LoadBalancer进行大量初始化工做,须要一段时间来建立LoadBalancer对象。而在此时,若是再一次调用getLoadBalancer()方法(一般发生在多线程环境中),因为instance还没有建立成功,仍为null值,判断条件(instance== null)为真值,所以代码instance= new LoadBalancer()将再次执行,致使最终建立了多个instance对象,这违背了单例模式的初衷,也致使系统运行发生错误。
如何解决该问题?咱们至少有两种解决方案,在正式介绍这两种解决方案以前,先介绍一下单例类的两种不一样实现方式,饿汉式单例类和懒汉式单例类。
1.饿汉式单例类
饿汉式单例类是实现起来最简单的单例类,饿汉式单例类结构图如图3-4所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7956/068b609b-5650-3cee-a485-e97ef9f3f9c8.gif[/img]
[color=gray]从图3-4中能够看出,因为在定义静态变量的时候实例化单例类,所以在类加载的时候就已经建立了单例对象,代码以下所示:[/color]
class EagerSingleton {   
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }

public static EagerSingleton getInstance() {
return instance;
}
}

[color=gray]当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的惟一实例将被建立。若是使用饿汉式单例来实现负载均衡器LoadBalancer类的设计,则不会出现建立多个单例对象的状况,可确保单例对象的惟一性。
2.懒汉式单例类与线程锁定
除了饿汉式单例,还有一种经典的懒汉式单例,也就是前面的负载均衡器LoadBalancer类的实现方式。懒汉式单例类结构图如图3-5所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7958/ab55783f-1b1b-3739-bc7a-7bf079873581.gif[/img]
[color=gray]从图3-5中能够看出,懒汉式单例在第一次调用getInstance()方法时实例化,在类加载时并不自行实例化,这种技术又称为延迟加载(Lazy Load)技术,即须要的时候再加载实例,为了不多个线程同时调用getInstance()方法,咱们可使用关键字synchronized,代码以下所示:[/color]
class LazySingleton {   
private static LazySingleton instance = null;

private LazySingleton() { }

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

[color=gray]该懒汉式单例类在getInstance()方法前面增长了关键字synchronized进行线程锁,以处理多个线程同时访问的问题。可是,上述代码虽然解决了线程安全问题,可是每次调用getInstance()时都须要进行线程锁定判断,在多线程高并发访问环境中,将会致使系统性能大大下降。如何既解决线程安全问题又不影响系统性能呢?咱们继续对懒汉式单例进行改进。事实上,咱们无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new LazySingleton();”进行锁定便可。所以getInstance()方法能够进行以下改进:[/color]
public static LazySingleton getInstance() {   
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}

[color=gray]问题貌似得以解决,事实并不是如此。若是使用以上代码来实现单例,仍是会存在单例对象不惟一。缘由以下:
假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能经过instance == null的判断。因为实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例建立代码,线程B处于排队等待状态,必须等待线程A执行完毕后才能够进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经建立,将继续建立新的实例,致使产生多个单例对象,违背单例模式的设计思想,所以须要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)。使用双重检查锁定实现的懒汉式单例类完整代码以下所示:[/color]
class LazySingleton {   
private volatile static LazySingleton instance = null;

private LazySingleton() { }

public static LazySingleton getInstance() {
//第一重判断
if (instance == null) {
//锁定代码块
synchronized (LazySingleton.class) {
//第二重判断
if (instance == null) {
instance = new LazySingleton(); //建立单例实例
}
}
}
return instance;
}
}

[color=gray]须要注意的是,若是使用双重检查锁定来实现懒汉式单例类,须要在静态成员变量instance以前增长修饰符volatile,被volatile修饰的成员变量能够确保多个线程都可以正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。因为volatile关键字会屏蔽Java虚拟机所作的一些代码优化,可能会致使系统运行效率下降,所以即便使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
扩展

IBM公司高级软件工程师Peter Haggar 2004年在IBM developerWorks上发表了一篇名为《双重检查锁定及单例模式——全面理解这一失效的编程习语》的文章,对JDK 1.5以前的双重检查锁定及单例模式进行了全面分析和阐述,参考连接:http://www.ibm.com/developerworks/cn/java/j-dcl.html
3.饿汉式单例类与懒汉式单例类比较
饿汉式单例类在类被加载时就将本身实例化,它的优势在于无须考虑多线程访问问题,能够确保实例的惟一性;从调用速度和反应时间角度来说,因为单例对象一开始就得以建立,所以要优于懒汉式单例。可是不管系统在运行时是否须要使用该单例对象,因为在类加载时该对象就须要建立,所以从资源利用效率角度来说,饿汉式单例不及懒汉式单例,并且在系统加载时因为须要建立饿汉式单例对象,加载时间可能会比较长。
懒汉式单例类在第一次使用时建立,无须一直占用系统资源,实现了延迟加载,可是必须处理好多个线程同时访问的问题,特别是当单例类做为资源控制器,在实例化时必然涉及资源初始化,而资源初始化颇有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,须要经过双重检查锁定等机制进行控制,这将致使系统性能受到必定影响。[/color]

[size=large]单例模式4[/size]
[color=gray]3.5 一种更好的单例实现方法

饿汉式单例类不能实现延迟加载,无论未来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,并且性能受影响。可见,不管是饿汉式单例仍是懒汉式单例都存在这样那样的问题,有没有一种方法,可以将两种单例的缺点都克服,而将二者的优势合二为一呢?答案是:Yes!下面咱们来学习这种更好的被称之为Initialization Demand Holder (IoDH)的技术。

在IoDH中,咱们在单例类中增长一个静态(static)内部类,在该内部类中建立单例对象,再将该单例对象经过getInstance()方法返回给外部使用,实现代码以下所示:[/color]
//Initialization on Demand Holder  
class Singleton {
private Singleton() {
}

private static class HolderClass {
private final static Singleton instance = new Singleton();
}

public static Singleton getInstance() {
return HolderClass.instance;
}

public static void main(String args[]) {
Singleton s1, s2;
s1 = Singleton.getInstance();
s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
}

[color=gray]编译并运行上述代码,运行结果为:true,即建立的单例对象s1和s2为同一对象。因为静态单例对象没有做为Singleton的成员变量直接实例化,所以类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。因为getInstance()方法没有任何线程锁定,所以其性能不会形成任何影响。

经过使用IoDH,咱们既能够实现延迟加载,又能够保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言自己的特性相关,不少面向对象语言不支持IoDH)。[/color]

[size=large]单例模式5[/size]
[color=gray]3.6 单例模式总结
单例模式做为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率至关高,在不少应用软件和框架中都得以普遍应用。
1.主要优势
单例模式的主要优势以下:
(1) 单例模式提供了对惟一实例的受控访问。由于单例类封装了它的惟一实例,因此它能够严格控制客户怎样以及什么时候访问它。
(2) 因为在系统内存中只存在一个对象,所以能够节约系统资源,对于一些须要频繁建立和销毁的对象单例模式无疑能够提升系统的性能。
(3) 容许可变数目的实例。基于单例模式咱们能够进行扩展,使用与单例控制类似的方法来得到指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
2.主要缺点
单例模式的主要缺点以下:
(1) 因为单例模式中没有抽象层,所以单例类的扩展有很大的困难。
(2) 单例类的职责太重,在必定程度上违背了“单一职责原则”。由于单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的建立和产品的自己的功能融合到一块儿。
(3) 如今不少面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,所以,若是实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将从新实例化,这将致使共享的单例对象状态的丢失。
3.适用场景
在如下状况下能够考虑使用单例模式:
(1) 系统只须要一个实例对象,如系统要求提供一个惟一的序列号生成器或资源管理器,或者须要考虑资源消耗太大而只容许建立一个对象。
(2) 客户调用类的单个实例只容许使用一个公共访问点,除了该公共访问点,不能经过其余途径访问该实例。
[/color]

[size=x-large]原型模式-Prototype Pattern(用于对象的克隆)[/size]
[size=large]原型模式1[/size]
[color=gray]张纪中版《西游记》以出乎意料的造型和雷人的台词遭到广大观众朋友的热议,咱们在此对该话题不做过多讨论。但不管是哪一个版本的《西游记》,孙悟空都是其中的一号雄性主角,关于他(或它)拔毛变小猴的故事几乎人人皆知,孙悟空能够用猴毛根据本身的形象,复制(又称“克隆”或“拷贝”)出不少跟本身长得如出一辙的“身外身”来。在设计模式中也存在一个相似的模式,能够经过一个原型对象克隆出多个如出一辙的对象,该模式称之为原型模式。

7.1 大同小异的工做周报

xxxx公司一直使用自行开发的一套OA (Office Automatic,办公自动化)系统进行平常工做办理,但在使用过程当中,愈来愈多的人对工做周报的建立和编写模块产生了抱怨。追其缘由,xxxx公司的OA管理员发现,因为某些岗位每周工做存在重复性,工做周报内容都大同小异,如图7-1工做周报示意图。这些周报只有一些小地方存在差别,可是现行系统每周默认建立的周报都是空白报表,用户只能经过从新输入或不断复制粘贴来填写重复的周报内容,极大下降了工做效率,浪费宝贵的时间。如何快速建立相同或者类似的工做周报,成为xxxx公司OA开发人员面临的一个新问题。[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7960/cd8dc5f1-6aba-35cb-8308-3f773d038808.gif[/img]
[color=gray]图7-1 工做周报示意图

xxxx公司的开发人员经过对问题进行仔细分析,决定按照以下思路对工做周报模块进行从新设计和实现:

(1)除了容许用户建立新周报外,还容许用户将建立好的周报保存为模板;

(2)用户在再次建立周报时,能够建立全新的周报,还能够选择合适的模板复制生成一份相同的周报,而后对新生成的周报根据实际状况进行修改,产生新的周报。

只要按照如上两个步骤进行处理,工做周报的建立效率将得以大大提升。这个过程让咱们想到平时常常进行的两个电脑基本操做:复制和粘贴,快捷键一般为Ctrl + C和Ctrl + V,经过对已有对象的复制和粘贴,咱们能够建立大量的相同对象。如何在一个面向对象系统中实现对象的复制和粘贴呢?不用着急,本章咱们介绍的原型模式正为解决此类问题而诞生。

7.2 原型模式概述

在使用原型模式时,咱们须要首先建立一个原型对象,再经过复制这个原型对象来建立更多同类型的对象。试想,若是连孙悟空的模样都不知道,怎么拔毛变小猴子呢?原型模式的定义以下: 原型模式(Prototype Pattern):使用原型实例指定建立对象的种类,而且经过拷贝这些原型建立新的对象。原型模式是一种对象建立型模式。

原型模式的工做原理很简单:将一个原型对象传给那个要发动建立的对象,这个要发动建立的对象经过请求原型对象拷贝本身来实现建立过程。因为在软件系统中咱们常常会遇到须要建立多个相同或者类似对象的状况,所以原型模式在真实开发中的使用频率仍是很是高的。原型模式是一种“另类”的建立型模式,建立克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。

须要注意的是经过克隆方法所建立的对象是全新的对象,它们在内存中拥有新的地址,一般对克隆所产生的对象进行修改对原型对象不会形成任何影响,每个克隆对象都是相互独立的。经过不一样的方式修改能够获得一系列类似但不彻底相同的对象。

原型模式的结构如图7-2所示:

图7-2 原型模式结构图

在原型模式结构图中包含以下几个角色:

●Prototype(抽象原型类):它是声明克隆方法的接口,是全部具体原型类的公共父类,能够是抽象类也能够是接口,甚至还能够是具体实现类。

● ConcretePrototype(具体原型类):它实如今抽象原型类中声明的克隆方法,在克隆方法中返回本身的一个克隆对象。

● Client(客户类):让一个原型对象克隆自身从而建立一个新的对象,在客户类中只须要直接实例化或经过工厂方法等方式建立一个原型对象,再经过调用该对象的克隆方法便可获得多个相同的对象。因为客户类针对抽象原型类Prototype编程,所以用户能够根据须要选择具体原型类,系统具备较好的可扩展性,增长或更换具体原型类都很方便。

原型模式的核心在于如何实现克隆方法,下面将介绍两种在Java语言中经常使用的克隆实现方法:

1.通用实现方法

通用的克隆实现方法是在具体原型类的克隆方法中实例化一个与自身类型相同的对象并将其返回,并将相关的参数传入新建立的对象中,保证它们的成员属性相同。示意代码以下所示:[/color]
class ConcretePrototype implements Prototype
{
private String attr; //成员属性
public void setAttr(String attr)
{
this.attr = attr;
}
public String getAttr()
{
return this.attr;
}
public Prototype clone() //克隆方法
{
Prototype prototype = new ConcretePrototype(); //建立新对象
prototype.setAttr(this.attr);
return prototype;
}
}

[color=gray]在客户类中咱们只须要建立一个ConcretePrototype对象做为原型对象,而后调用其clone()方法便可获得对应的克隆对象,以下代码所示:[/color]
Prototype obj1  = new ConcretePrototype();
obj1.setAttr("vahoa");
Prototype obj2 = obj1.clone();

[color=gray]这种方法可做为原型模式的通用实现,它与编程语言特性无关,任何面向对象语言均可以使用这种形式来实现对原型的克隆。

Java语言提供的clone()方法
学过Java语言的人都知道,全部的Java类都继承自java.lang.Object。事实上,Object类提供一个clone()方法,能够将一个Java对象复制一份。所以在Java中能够直接使用Object提供的clone()方法来实现对象的克隆,Java语言中的原型模式实现很简单。

须要注意的是可以实现克隆的Java类必须实现一个标识接口Cloneable,表示这个Java类支持被复制。若是一个类没有实现这个接口可是调用了clone()方法,Java编译器将抛出一个CloneNotSupportedException异常。以下代码所示:[/color]
class ConcretePrototype implements  Cloneable
{
……
public Prototype clone()
{
  Object object = null;
  try {
     object = super.clone();
  } catch (CloneNotSupportedException exception) {
     System.err.println("Not support cloneable");
  }
  return (Prototype )object;
}
……
}

[color=gray]在客户端建立原型对象和克隆对象也很简单,以下代码所示:[/color]
Prototype obj1  = new ConcretePrototype();
Prototype obj2 = obj1.clone();

[color=gray]通常而言,Java语言中的clone()方法知足:

(1) 对任何对象x,都有x.clone() != x,即克隆对象与原型对象不是同一个对象;

(2) 对任何对象x,都有x.clone().getClass() == x.getClass(),即克隆对象与原型对象的类型同样;

(3) 若是对象x的equals()方法定义恰当,那么x.clone().equals(x)应该成立。

为了获取对象的一份拷贝,咱们能够直接利用Object类的clone()方法,具体步骤以下:

(1) 在派生类中覆盖基类的clone()方法,并声明为public;

(2) 在派生类的clone()方法中,调用super.clone();

(3)派生类需实现Cloneable接口。

此时,Object类至关于抽象原型类,全部实现了Cloneable接口的类至关于具体原型类。[/color]

[size=large]原型模式2[/size]
[color=gray]7.3 完整解决方案
xxxx公司开发人员决定使用原型模式来实现工做周报的快速建立,快速建立工做周报结构图如图7-3所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7964/e3176b13-c44b-3e69-a0fe-583cc1684754.gif[/img]
[color=gray]图7-3 快速建立工做周报结构图
在图7-3中,WeeklyLog充当具体原型类,Object类充当抽象原型类,clone()方法为原型方法。WeeklyLog类的代码以下所示:[/color]
//工做周报WeeklyLog:具体原型类,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class WeeklyLog implements Cloneable
{
private String name;
private String date;
private String content;
public void setName(String name) {
this.name = name;
}
public void setDate(String date) {
this.date = date;
}
public void setContent(String content) {
this.content = content;
}
public String getName() {
return (this.name);
}
public String getDate() {
return (this.date);
}
public String getContent() {
return (this.content);
}
//克隆方法clone(),此处使用Java语言提供的克隆机制
public WeeklyLog clone()
{
Object obj = null;
try
{
obj = super.clone();
return (WeeklyLog)obj;
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持复制!");
return null;
}
}
}

[color=gray]编写以下客户端测试代码:[/color]
class Client
{
public static void main(String args[])
{
WeeklyLog log_previous = new WeeklyLog(); //建立原型对象
log_previous.setName("张无忌");
log_previous.setDate("第12周");
log_previous.setContent("这周工做很忙,天天加班!");

System.out.println("****周报****");
System.out.println("周次:" + log_previous.getDate());
System.out.println("姓名:" + log_previous.getName());
System.out.println("内容:" + log_previous.getContent());
System.out.println("--------------------------------");

WeeklyLog log_new;
log_new = log_previous.clone(); //调用克隆方法建立克隆对象
log_new.setDate("第13周");
System.out.println("****周报****");
System.out.println("周次:" + log_new.getDate());
System.out.println("姓名:" + log_new.getName());
System.out.println("内容:" + log_new.getContent());
}
}

[color=gray]编译并运行程序,输出结果以下:[/color]

****周报****
周次:第12周
姓名:张无忌
内容:这周工做很忙,天天加班!
--------------------------------
****周报****
周次:第13周
姓名:张无忌
内容:这周工做很忙,天天加班!

[color=gray]经过已建立的工做周报能够快速建立新的周报,而后再根据须要修改周报,无须再从头开始建立。原型模式为工做流系统中任务单的快速生成提供了一种解决方案。[/color]


[size=large]原型模式3[/size]
[color=gray]7.4 带附件的周报
经过引入原型模式,xxxx公司OA系统支持工做周报的快速克隆,极大提升了工做周报的编写效率,受到员工的一致好评。但有员工又发现一个问题,有些工做周报带有附件,例如经理助理“小龙女”的周报一般附有本周项目进展报告汇总表、本周客户反馈信息汇总表等,若是使用上述原型模式来复制周报,周报虽然能够复制,可是周报的附件并不能复制,这是因为什么缘由致使的呢?如何才能实现周报和附件的同时复制呢?咱们在本节将讨论如何解决这些问题。
在回答这些问题以前,先介绍一下两种不一样的克隆方法,浅克隆(ShallowClone)和深克隆(DeepClone)。在Java语言中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括int、double、byte、boolean、char等简单数据类型,引用类型包括类、接口、数组等复杂类型。浅克隆和深克隆的主要区别在因而否支持引用类型的成员变量的复制,下面将对二者进行详细介绍。
1.浅克隆
在浅克隆中,若是原型对象的成员变量是值类型,将复制一份给克隆对象;若是原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来讲,在浅克隆中,当对象被复制时只复制它自己和其中包含的值类型的成员变量,而引用类型的成员对象并无复制,如图7-4所示:
[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7976/e5651836-2cf7-3b96-a213-6506bf2382a9.gif[/img]
[color=gray]图7-4 浅克隆示意图
在Java语言中,经过覆盖Object类的clone()方法能够实现浅克隆。为了让你们更好地理解浅克隆和深克隆的区别,咱们首先使用浅克隆来实现工做周报和附件类的复制,其结构如图7-5所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7978/4457920e-e8e6-3751-abb7-a667f28d1f7c.gif[/img]
[color=gray]图7-5 带附件的周报结构图(浅克隆)
附件类Attachment代码以下:[/color]
//附件类
class Attachment
{
private String name; //附件名
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
public void download()
{
System.out.println("下载附件,文件名为" + name);
}
}

[color=gray]修改工做周报类WeeklyLog,修改后的代码以下:[/color]
//工做周报WeeklyLog
class WeeklyLog implements Cloneable
{
//为了简化设计和实现,假设一份工做周报中只有一个附件对象,实际状况中能够包含多个附件,能够经过List等集合对象来实现
private Attachment attachment;
private String name;
private String date;
private String content;
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
public void setName(String name) {
this.name = name;
}
public void setDate(String date) {
this.date = date;
}
public void setContent(String content) {
this.content = content;
}
public Attachment getAttachment(){
return (this.attachment);
}
public String getName() {
return (this.name);
}
public String getDate() {
return (this.date);
}
public String getContent() {
return (this.content);
}
//使用clone()方法实现浅克隆
public WeeklyLog clone()
{
Object obj = null;
try
{
obj = super.clone();
return (WeeklyLog)obj;
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持复制!");
return null;
}
}
}

[color=gray]客户端代码以下所示:[/color]
class Client
{
public static void main(String args[])
{
WeeklyLog log_previous, log_new;
log_previous = new WeeklyLog(); //建立原型对象
Attachment attachment = new Attachment(); //建立附件对象
log_previous.setAttachment(attachment); //将附件添加到周报中
log_new = log_previous.clone(); //调用克隆方法建立克隆对象
//比较周报
System.out.println("周报是否相同? " + (log_previous == log_new));
//比较附件
System.out.println("附件是否相同? " + (log_previous.getAttachment() == log_new.getAttachment()));
}
}

[color=gray]编译并运行程序,输出结果以下:[/color]

周报是否相同? false
附件是否相同? true

[color=gray]因为使用的是浅克隆技术,所以工做周报对象复制成功,经过“==”比较原型对象和克隆对象的内存地址时输出false;可是比较附件对象的内存地址时输出true,说明它们在内存中是同一个对象。
2.深克隆
在深克隆中,不管原型对象的成员变量是值类型仍是引用类型,都将复制一份给克隆对象,深克隆将原型对象的全部引用对象也复制一份给克隆对象。简单来讲,在深克隆中,除了对象自己被复制外,对象所包含的全部成员变量也将复制,如图7-6所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7980/8ad7bfea-6fa9-32d9-af83-37b2825c4e8b.gif[/img]
[color=gray]图7-6 深克隆示意图
在Java语言中,若是须要实现深克隆,能够经过序列化(Serialization)等方式来实现。序列化就是将对象写到流的过程,写到流中的对象是原有对象的一个拷贝,而原对象仍然存在于内存中。经过序列化实现的拷贝不只能够复制对象自己,并且能够复制其引用的成员对象,所以经过序列化将对象写到一个流中,再从流里将其读出来,能够实现深克隆。须要注意的是可以实现序列化的对象其类必须实现Serializable接口,不然没法实现序列化操做。下面咱们使用深克隆技术来实现工做周报和附件对象的复制,因为要将附件对象和工做周报对象都写入流中,所以两个类均须要实现Serializable接口,其结构如图7-7所示:
[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7982/fcc7e061-f3c7-3e19-bf78-fbb72b3390e7.gif[/img]
[color=gray]图7-7 带附件的周报结构图(深克隆)
修改后的附件类Attachment代码以下:[/color]
import  java.io.*;
//附件类
class Attachment implements Serializable
{
private String name; //附件名
public void setName(String name)
{
this.name = name;
}
public String getName()
{
return this.name;
}
public void download()
{
System.out.println("下载附件,文件名为" + name);
}
}

[color=gray]工做周报类WeeklyLog再也不使用Java自带的克隆机制,而是经过序列化来从头实现对象的深克隆,咱们须要从新编写clone()方法,修改后的代码以下:[/color]
import  java.io.*;
//工做周报类
class WeeklyLog implements Serializable
{
private Attachment attachment;
private String name;
private String date;
private String content;
public void setAttachment(Attachment attachment) {
this.attachment = attachment;
}
public void setName(String name) {
this.name = name;
}
public void setDate(String date) {
this.date = date;
}
public void setContent(String content) {
this.content = content;
}
public Attachment getAttachment(){
return (this.attachment);
}
public String getName() {
return (this.name);
}
public String getDate() {
return (this.date);
}
public String getContent() {
return (this.content);
}
//使用序列化技术实现深克隆
public WeeklyLog deepClone() throws IOException, ClassNotFoundException, OptionalDataException
{
//将对象写入流中
ByteArrayOutputStream bao=new ByteArrayOutputStream();
ObjectOutputStream oos=new ObjectOutputStream(bao);
oos.writeObject(this);

//将对象从流中取出
ByteArrayInputStream bis=new ByteArrayInputStream(bao.toByteArray());
ObjectInputStream ois=new ObjectInputStream(bis);
return (WeeklyLog)ois.readObject();
}
}

[color=gray]客户端代码以下所示:[/color]
class Client
{
public static void main(String args[])
{
WeeklyLog log_previous, log_new = null;
log_previous = new WeeklyLog(); //建立原型对象
Attachment attachment = new Attachment(); //建立附件对象
log_previous.setAttachment(attachment); //将附件添加到周报中
try
{
log_new = log_previous.deepClone(); //调用深克隆方法建立克隆对象
}
catch(Exception e)
{
System.err.println("克隆失败!");
}
//比较周报
System.out.println("周报是否相同? " + (log_previous == log_new));
//比较附件
System.out.println("附件是否相同? " + (log_previous.getAttachment() == log_new.getAttachment()));
}
}

[color=gray]编译并运行程序,输出结果以下:[/color]

周报是否相同? false
附件是否相同? false

[color=gray]从输出结果能够看出,因为使用了深克隆技术,附件对象也得以复制,所以用“==”比较原型对象的附件和克隆对象的附件时输出结果均为false。深克隆技术实现了原型对象和克隆对象的彻底独立,对任意克隆对象的修改都不会给其余对象产生影响,是一种更为理想的克隆实现方式。

扩展

Java语言提供的Cloneable接口和Serializable接口的代码很是简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其做用是告诉JRE这些接口的实现类是否具备某个功能,如是否支持克隆、是否支持序列化等。[/color]

[size=large]原型模式[/size]
[color=gray]7.5 原型管理器的引入和实现
原型管理器(Prototype Manager)是将多个原型对象存储在一个集合中供客户端使用,它是一个专门负责克隆对象的工厂,其中定义了一个集合用于存储原型对象,若是须要某个原型对象的一个克隆,能够经过复制集合中对应的原型对象来得到。在原型管理器中针对抽象原型类进行编程,以便扩展。其结构如图7-8所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7986/bfdb4072-72d8-3fbe-9fda-81fd92a37e7f.gif[/img]
[color=gray]图7-8 带原型管理器的原型模式
下面经过模拟一个简单的公文管理器来介绍原型管理器的设计与实现: xxxx公司在平常办公中有许多公文须要建立、递交和审批,例如《可行性分析报告》、《立项建议书》、《软件需求规格说明书》、《项目进展报告》等,为了提升工做效率,在OA系统中为各种公文均建立了模板,用户能够经过这些模板快速建立新的公文,这些公文模板须要统一进行管理,系统根据用户请求的不一样生成不一样的新公文。
咱们使用带原型管理器的原型模式实现公文管理器的设计,其结构如图7-9所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/7988/03c0875a-6a8d-352a-909a-61f4230bc3e7.gif[/img]
[color=gray]图7-9 公文管理器结构图
如下是实现该功能的一些核心代码,考虑到代码的可读性,咱们对全部的类都进行了简化:[/color]
import java.util.*;

//抽象公文接口,也可定义为抽象类,提供clone()方法的实现,将业务方法声明为抽象方法
interface OfficialDocument extends Cloneable
{
public OfficialDocument clone();
public void display();
}

//可行性分析报告(Feasibility Analysis Report)类
class FAR implements OfficialDocument
{
public OfficialDocument clone()
{
OfficialDocument far = null;
try
{
far = (OfficialDocument)super.clone();
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持复制!");
}
return far;
}

public void display()
{
System.out.println("《可行性分析报告》");
}
}

//软件需求规格说明书(Software Requirements Specification)类
class SRS implements OfficialDocument
{
public OfficialDocument clone()
{
OfficialDocument srs = null;
try
{
srs = (OfficialDocument)super.clone();
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持复制!");
}
return srs;
}

public void display()
{
System.out.println("《软件需求规格说明书》");
}
}

//原型管理器(使用饿汉式单例实现)
class PrototypeManager
{
//定义一个Hashtable,用于存储原型对象
private Hashtable ht=new Hashtable();
private static PrototypeManager pm = new PrototypeManager();

//为Hashtable增长公文对象
private PrototypeManager()
{
ht.put("far",new FAR());
ht.put("srs",new SRS());
}

//增长新的公文对象
public void addOfficialDocument(String key,OfficialDocument doc)
{
ht.put(key,doc);
}

//经过浅克隆获取新的公文对象
public OfficialDocument getOfficialDocument(String key)
{
return ((OfficialDocument)ht.get(key)).clone();
}

public static PrototypeManager getPrototypeManager()
{
return pm;
}
}

[color=gray]客户端代码以下所示:[/color]
class Client
{
public static void main(String args[])
{
//获取原型管理器对象
PrototypeManager pm = PrototypeManager.getPrototypeManager();

OfficialDocument doc1,doc2,doc3,doc4;

doc1 = pm.getOfficialDocument("far");
doc1.display();
doc2 = pm.getOfficialDocument("far");
doc2.display();
System.out.println(doc1 == doc2);

doc3 = pm.getOfficialDocument("srs");
doc3.display();
doc4 = pm.getOfficialDocument("srs");
doc4.display();
System.out.println(doc3 == doc4);
}
}

[color=gray]编译并运行程序,输出结果以下:[/color]

《可行性分析报告》
《可行性分析报告》
false
《软件需求规格说明书》
《软件需求规格说明书》
false

[color=gray]
在PrototypeManager中定义了一个Hashtable类型的集合对象,使用“键值对”来存储原型对象,客户端能够经过Key(如“far”或“srs”)来获取对应原型对象的克隆对象。PrototypeManager类提供了相似工厂方法的getOfficialDocument()方法用于返回一个克隆对象。在本实例代码中,咱们将PrototypeManager设计为单例类,使用饿汉式单例实现,确保系统中有且仅有一个PrototypeManager对象,有利于节省系统资源,并能够更好地对原型管理器对象进行控制。
[/color]
[color=gray]7.6 原型模式总结
原型模式做为一种快速建立大量相同或类似对象的方式,在软件开发中应用较为普遍,不少软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操做就是原型模式的典型应用,下面对该模式的使用效果和适用状况进行简单的总结。
1.主要优势
原型模式的主要优势以下:
(1) 当建立新的对象实例较为复杂时,使用原型模式能够简化对象的建立过程,经过复制一个已有实例能够提升新实例的建立效率。
(2) 扩展性较好,因为在原型模式中提供了抽象原型类,在客户端能够针对抽象原型类进行编程,而将具体原型类写在配置文件中,增长或减小产品类对原有系统都没有任何影响。
(3) 原型模式提供了简化的建立结构,工厂方法模式经常须要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不须要这样,原型模式中产品的复制是经过封装在原型类中的克隆方法实现的,无须专门的工厂类来建立产品。
(4) 可使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在须要的时候使用(如恢复到某一历史状态),可辅助实现撤销操做。
2.主要缺点
原型模式的主要缺点以下:
(1) 须要为每个类配备一个克隆方法,并且该克隆方法位于一个类的内部,当对已有的类进行改造时,须要修改源代码,违背了“开闭原则”。
(2) 在实现深克隆时须要编写较为复杂的代码,并且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
3.适用场景 在如下状况下能够考虑使用原型模式:
(1) 建立新对象成本较大(如初始化须要占用较长的时间,占用太多的CPU资源或网络资源),新的对象能够经过原型模式对已有对象进行复制来得到,若是是类似对象,则能够对其成员变量稍做修改。
(2) 若是系统要保存对象的状态,而对象的状态变化很小,或者对象自己占用内存较少时,可使用原型模式配合备忘录模式来实现。
(3) 须要避免使用分层次的工厂类来建立分层次的对象,而且类的实例对象只有一个或不多的几个组合状态,经过复制原型对象获得新实例可能比使用构造函数建立一个新实例更加方便。[/color]

[size=x-large]建造者模式-Builder Pattern(用于复杂对象的组装与建立)[/size]
[size=large]建造者模式1[/size]
[color=gray]没有人买车会只买一个轮胎或者方向盘,你们买的都是一辆包含轮胎、方向盘和发动机等多个部件的完整汽车。如何将这些部件组装成一辆完整的汽车并返回给用户,这是建造者模式须要解决的问题。建造者模式又称为生成器模式,它是一种较为复杂、使用频率也相对较低的建立型模式。建造者模式为客户端返回的不是一个简单的产品,而是一个由多个部件组成的复杂产品。
8.1 游戏角色设计
xxxx公司游戏开发小组决定开发一款名为《Vahoa王者》的网络游戏,该游戏采用主流的RPG(Role Playing Game,角色扮演游戏)模式,玩家能够在游戏中扮演虚拟世界中的一个特定角色,角色根据不一样的游戏情节和统计数据(如力量、魔法、技能等)具备不一样的能力,角色也会随着不断升级而拥有更增强大的能力。
做为RPG游戏的一个重要组成部分,须要对游戏角色进行设计,并且随着该游戏的升级将不断增长新的角色。不一样类型的游戏角色,其性别、脸型、服装、发型等外部特性都有所差别,例如“天使”拥有美丽的面容和披肩的长发,并身穿一袭白裙;而“恶魔”极其丑陋,留着光头并穿一件刺眼的黑衣。 xxxx公司决定开发一个小工具来建立游戏角色,能够建立不一样类型的角色并能够灵活增长新的角色。
xxxx公司的开发人员经过分析发现,游戏角色是一个复杂对象,它包含性别、脸型等多个组成部分,不一样的游戏角色其组成部分有所差别,如图8-1所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/8002/a63465db-cbbb-316d-ac1c-66d432022870.gif[/img]
[color=gray]图8-1 几种不一样的游戏角色造型 (注:本图中的游戏角色造型来源于网络,特此说明)
不管是何种造型的游戏角色,它的建立步骤都大同小异,都须要逐步建立其组成部分,再将各组成部分装配成一个完整的游戏角色。如何一步步建立一个包含多个组成部分的复杂对象,建造者模式为解决此类问题而诞生。
8.2 建造者模式概述
建造者模式是较为复杂的建立型模式,它将客户端与包含多个组成部分(或部件)的复杂对象的建立过程分离,客户端无须知道复杂对象的内部组成部分与装配方式,只须要知道所需建造者的类型便可。它关注如何一步一步建立一个的复杂对象,不一样的具体建造者定义了不一样的建立过程,且具体建造者相互独立,增长新的建造者很是方便,无须修改已有代码,系统具备较好的扩展性。
建造者模式定义以下:
建造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得一样的构建过程能够建立不一样的表示。建造者模式是一种对象建立型模式。
建造者模式一步一步建立一个复杂的对象,它容许用户只经过指定复杂对象的类型和内容就能够构建它们,用户不须要知道内部的具体构建细节。建造者模式结构如图8-2所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/8004/b576697f-c216-31d7-aa74-9b04605a8c8a.gif[/img]
[color=gray]图8-2 建造者模式结构图
在建造者模式结构图中包含以下几个角色:
● Builder(抽象建造者):它为建立一个产品Product对象的各个部件指定抽象接口,在该接口中通常声明两类方法,一类方法是buildPartX(),它们用于建立复杂对象的各个部件;另外一类方法是getResult(),它们用于返回复杂对象。Builder既能够是抽象类,也能够是接口。
●ConcreteBuilder(具体建造者):它实现了Builder接口,实现各个部件的具体构造和装配方法,定义并明确它所建立的复杂对象,也能够提供一个方法返回建立好的复杂产品对象。+

●Product(产品角色):它是被构建的复杂对象,包含多个组成部件,具体建造者建立该产品的内部表示并定义它的装配过程。
● Director(指挥者):指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,能够在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端通常只须要与指挥者进行交互,在客户端肯定具体建造者的类型,并实例化具体建造者对象(也能够经过配置文件和反射机制),而后经过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。
在建造者模式的定义中提到了复杂对象,那么什么是复杂对象?简单来讲,复杂对象是指那些包含多个成员属性的对象,这些成员属性也称为部件或零件,如汽车包括方向盘、发动机、轮胎等部件,电子邮件包括发件人、收件人、主题、内容、附件等部件,一个典型的复杂对象类代码示例以下:[/color]
class Product  {
private String partA; //定义部件,部件能够是任意类型,包括值类型和引用类型
private String partB;
private String partC;
//partA的Getter方法和Setter方法省略
//partB的Getter方法和Setter方法省略
//partC的Getter方法和Setter方法省略
}

[color=gray]在抽象建造者类中定义了产品的建立方法和返回方法,其典型代码以下:[/color]
abstract class Builder {
//建立产品对象
protected Product product=new Product();

public abstract void buildPartA();
public abstract void buildPartB();
public abstract void buildPartC();

//返回产品对象
public Product getResult() {
return product;
}
}

[color=gray]在抽象类Builder中声明了一系列抽象的buildPartX()方法用于建立复杂产品的各个部件,具体建造过程在ConcreteBuilder中实现,此外还提供了工厂方法getResult(),用于返回一个建造好的完整产品。
在ConcreteBuilder中实现了buildPartX()方法,经过调用Product的setPartX()方法能够给产品对象的成员属性设值。不一样的具体建造者在实现buildPartX()方法时将有所区别,如setPartX()方法的参数可能不同,在有些具体建造者类中某些setPartX()方法无须实现(提供一个空实现)。而这些对于客户端来讲都无须关心,客户端只需知道具体建造者类型便可。
在建造者模式的结构中还引入了一个指挥者类Director,该类主要有两个做用:一方面它隔离了客户与建立过程;另外一方面它控制产品的建立过程,包括某个buildPartX()方法是否被调用以及多个buildPartX()方法调用的前后次序等。指挥者针对抽象建造者编程,客户端只须要知道具体建造者的类型,便可经过指挥者类调用建造者的相关方法,返回一个完整的产品对象。在实际生活中也存在相似指挥者同样的角色,如一个客户去购买电脑,电脑销售人员至关于指挥者,只要客户肯定电脑的类型,电脑销售人员能够通知电脑组装人员给客户组装一台电脑。指挥者类的代码示例以下:
[/color]
class Director {
private Builder builder;

public Director(Builder builder) {
this.builder=builder;
}

public void setBuilder(Builder builder) {
this.builder=builer;
}

//产品构建与组装方法
public Product construct() {
builder.buildPartA();
builder.buildPartB();
builder.buildPartC();
return builder.getResult();
}
}

[color=gray]在指挥者类中能够注入一个抽象建造者类型的对象,其核心在于提供了一个建造方法construct(),在该方法中调用了builder对象的构造部件的方法,最后返回一个产品对象。
对于客户端而言,只需关心具体的建造者便可,通常状况下,客户端类代码片断以下所示:[/color]
……
Builder builder = new ConcreteBuilder(); //可经过配置文件实现
Director director = new Director(builder);
Product product = director.construct();
……

[color=gray]能够经过配置文件来存储具体建造者类ConcreteBuilder的类名,使得更换新的建造者时无须修改源代码,系统扩展更为方便。在客户端代码中,无须关心产品对象的具体组装过程,只需指定具体建造者的类型便可。
建造者模式与抽象工厂模式有点类似,可是建造者模式返回一个完整的复杂产品,而抽象工厂模式返回一系列相关的产品;在抽象工厂模式中,客户端经过选择具体工厂来生成所需对象,而在建造者模式中,客户端经过指定具体建造者类型并指导Director类如何去生成对象,侧重于一步步构造一个复杂对象,而后将结果返回。若是将抽象工厂模式当作一个汽车配件生产厂,生成不一样类型的汽车配件,那么建造者模式就是一个汽车组装厂,经过对配件进行组装返回一辆完整的汽车。[/color]

[color=gray]这里给出一个思考:[/color]

[color=gray]若是没有指挥者类Director,客户端将如何构建复杂产品?[/color]

[size=large]建造者模式2[/size]
[color=gray]8.3 完整解决方案

xxxx公司开发人员决定使用建造者模式来实现游戏角色的建立,其基本结构如图8-3所示:[/color]

[img]http://dl2.iteye.com/upload/attachment/0127/8012/12826471-c5d5-39ba-9d73-ebf9c5e8aead.gif[/img]
[color=gray]图8-3 游戏角色建立结构图

在图8-3中,ActorController充当指挥者,ActorBuilder充当抽象建造者,HeroBuilder、AngelBuilder和DevilBuilder充当具体建造者,Actor充当复杂产品。完整代码以下所示: //Actor角色类:复杂产品,考虑到代码的可读性,只列出部分红员属性,且成员属性的类型均为String,真实状况下,有些成员属性的类型需自定义[/color]
class Actor
{
private String type; //角色类型
private String sex; //性别
private String face; //脸型
private String costume; //服装
private String hairstyle; //发型

public void setType(String type) {
this.type = type;
}
public void setSex(String sex) {
this.sex = sex;
}
public void setFace(String face) {
this.face = face;
}
public void setCostume(String costume) {
this.costume = costume;
}
public void setHairstyle(String hairstyle) {
this.hairstyle = hairstyle;
}
public String getType() {
return (this.type);
}
public String getSex() {
return (this.sex);
}
public String getFace() {
return (this.face);
}
public String getCostume() {
return (this.costume);
}
public String getHairstyle() {
return (this.hairstyle);
}
}

//角色建造器:抽象建造者
abstract class ActorBuilder
{
protected Actor actor = new Actor();

public abstract void buildType();
public abstract void buildSex();
public abstract void buildFace();
public abstract void buildCostume();
public abstract void buildHairstyle();

//工厂方法,返回一个完整的游戏角色对象
public Actor createActor()
{
return actor;
}
}

//英雄角色建造器:具体建造者
class HeroBuilder extends ActorBuilder
{
public void buildType()
{
actor.setType("英雄");
}
public void buildSex()
{
actor.setSex("男");
}
public void buildFace()
{
actor.setFace("英俊");
}
public void buildCostume()
{
actor.setCostume("盔甲");
}
public void buildHairstyle()
{
actor.setHairstyle("飘逸");
}
}

//天使角色建造器:具体建造者
class AngelBuilder extends ActorBuilder
{
public void buildType()
{
actor.setType("天使");
}
public void buildSex()
{
actor.setSex("女");
}
public void buildFace()
{
actor.setFace("漂亮");
}
public void buildCostume()
{
actor.setCostume("白裙");
}
public void buildHairstyle()
{
actor.setHairstyle("披肩长发");
}
}

//恶魔角色建造器:具体建造者
class DevilBuilder extends ActorBuilder
{
public void buildType()
{
actor.setType("恶魔");
}
public void buildSex()
{
actor.setSex("妖");
}
public void buildFace()
{
actor.setFace("丑陋");
}
public void buildCostume()
{
actor.setCostume("黑衣");
}
public void buildHairstyle()
{
actor.setHairstyle("光头");
}
}

[color=gray]指挥者类ActorController定义了construct()方法,该方法拥有一个抽象建造者ActorBuilder类型的参数,在该方法内部实现了游戏角色对象的逐步构建,代码以下所示:[/color]
//游戏角色建立控制器:指挥者
class ActorController
{
//逐步构建复杂产品对象
public Actor construct(ActorBuilder ab)
{
Actor actor;
ab.buildType();
ab.buildSex();
ab.buildFace();
ab.buildCostume();
ab.buildHairstyle();
actor=ab.createActor();
return actor;
}
}

[color=gray]为了提升系统的灵活性和可扩展性,咱们将具体建造者类的类名存储在配置文件中,并经过工具类XMLUtil来读取配置文件并反射生成对象,XMLUtil类的代码以下所示:[/color]
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import java.io.*;
class XMLUtil
{
//该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
public static Object getBean()
{
try
{
//建立文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("config.xml"));

//获取包含类名的文本节点
NodeList nl = doc.getElementsByTagName("className");
Node classNode=nl.item(0).getFirstChild();
String cName=classNode.getNodeValue();

//经过类名生成实例对象并将其返回
Class c=Class.forName(cName);
Object obj=c.newInstance();
return obj;
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
}

[color=gray]配置文件config.xml中存储了具体建造者类的类名,代码以下所示:[/color]
<?xml version="1.0"?>
<config>
<className>AngelBuilder</className>
</config>
编写以下客户端测试代码:
class Client
{
public static void main(String args[])
{
ActorBuilder ab; //针对抽象建造者编程
ab = (ActorBuilder)XMLUtil.getBean(); //反射生成具体建造者对象

ActorController ac = new ActorController();
Actor actor;
actor = ac.construct(ab); //经过指挥者建立完整的建造者对象

String type = actor.getType();
System.out.println(type + "的外观:");
System.out.println("性别:" + actor.getSex());
System.out.println("面容:" + actor.getFace());
System.out.println("服装:" + actor.getCostume());
System.out.println("发型:" + actor.getHairstyle());
}
}

[color=gray]编译并运行程序,输出结果以下:[/color]

天使的外观:
性别:女
面容:漂亮
服装:白裙
发型:披肩长发

[color=gray]在建造者模式中,客户端只需实例化指挥者类,指挥者类针对抽象建造者编程,客户端根据须要传入具体的建造者类型,指挥者将指导具体建造者一步一步构造一个完整的产品(逐步调用具体建造者的buildX()方法),相同的构造过程能够建立彻底不一样的产品。在游戏角色实例中,若是须要更换角色,只须要修改配置文件,更换具体角色建造者类便可;若是须要增长新角色,能够增长一个新的具体角色建造者类做为抽象角色建造者的子类,再修改配置文件便可,原有代码无须修改,彻底符合“开闭原则”。[/color]

[size=large]建造者模式3[/size]
[color=gray]8.4 关于Director的进一步讨论
指挥者类Director在建造者模式中扮演很是重要的做用,简单的Director类用于指导具体建造者如何构建产品,它按必定次序调用Builder的buildPartX()方法,控制调用的前后次序,并向客户端返回一个完整的产品对象。下面咱们讨论几种Director的高级应用方式:
1.省略Director
在有些状况下,为了简化系统结构,能够将Director和抽象建造者Builder进行合并,在Builder中提供逐步构建复杂产品对象的construct()方法。因为Builder类一般为抽象类,所以能够将construct()方法定义为静态(static)方法。若是将游戏角色设计中的指挥者类ActorController省略,ActorBuilder类的代码修改以下:[/color]
abstract class ActorBuilder
{
protected static Actor actor = new Actor();

public abstract void buildType();
public abstract void buildSex();
public abstract void buildFace();
public abstract void buildCostume();
public abstract void buildHairstyle();

public static Actor construct(ActorBuilder ab)
{
ab.buildType();
ab.buildSex();
ab.buildFace();
ab.buildCostume();
ab.buildHairstyle();
return actor;
}
}

[color=gray]对应的客户端代码也将发生修改,其代码片断以下所示:[/color]
……
ActorBuilder ab;
ab = (ActorBuilder)XMLUtil.getBean();

Actor actor;
actor = ActorBuilder.construct(ab);
……


[color=gray]除此以外,还有一种更简单的处理方法,能够将construct()方法的参数去掉,直接在construct()方法中调用buildPartX()方法,代码以下所示:[/color]
abstract class ActorBuilder
{
protected Actor actor = new Actor();

public abstract void buildType();
public abstract void buildSex();
public abstract void buildFace();
public abstract void buildCostume();
public abstract void buildHairstyle();

public Actor construct()
{
this.buildType();
this.buildSex();
this.buildFace();
this.buildCostume();
this.buildHairstyle();
return actor;
}
}

[color=gray]客户端代码代码片断以下所示:[/color]
……
ActorBuilder ab;
ab = (ActorBuilder)XMLUtil.getBean();

Actor actor;
actor = ab.construct();
……

[color=gray]此时,construct()方法定义了其余buildPartX()方法调用的次序,为其余方法的执行提供了一个流程模板,这与咱们在后面要学习的模板方法模式很是相似。
以上两种对Director类的省略方式都不影响系统的灵活性和可扩展性,同时还简化了系统结构,但加剧了抽象建造者类的职责,若是construct()方法较为复杂,待构建产品的组成部分较多,建议仍是将construct()方法单独封装在Director中,这样作更符合“单一职责原则”。+

2.钩子方法的引入
建造者模式除了逐步构建一个复杂产品对象外,还能够经过Director类来更加精细地控制产品的建立过程,例如增长一类称之为钩子方法(HookMethod)的特殊方法来控制是否对某个buildPartX()的调用。
钩子方法的返回类型一般为boolean类型,方法名通常为isXXX(),钩子方法定义在抽象建造者类中。例如咱们能够在游戏角色的抽象建造者类ActorBuilder中定义一个方法isBareheaded(),用于判断某个角色是否为“光头(Bareheaded)”,在ActorBuilder为之提供一个默认实现,其返回值为false,代码以下所示:[/color]
abstract class ActorBuilder
{
protected Actor actor = new Actor();

public abstract void buildType();
public abstract void buildSex();
public abstract void buildFace();
public abstract void buildCostume();
public abstract void buildHairstyle();

//钩子方法
public boolean isBareheaded()
{
return false;
}

public Actor createActor()
{
return actor;
}
}

[color=gray]若是某个角色无须构建头发部件,例如“恶魔(Devil)”,则对应的具体建造器DevilBuilder将覆盖isBareheaded()方法,并将返回值改成true,代码以下所示:[/color]
class DevilBuilder extends ActorBuilder
{
public void buildType()
{
actor.setType("恶魔");
}
public void buildSex()
{
actor.setSex("妖");
}
public void buildFace()
{
actor.setFace("丑陋");
}
public void buildCostume()
{
actor.setCostume("黑衣");
}
public void buildHairstyle()
{
actor.setHairstyle("光头");
}
//覆盖钩子方法
public boolean isBareheaded()
{
return true;
}
}

[color=gray]此时,指挥者类ActorController的代码修改以下:[/color]
class ActorController{       public  Actor construct(ActorBuilder ab)       {              Actor  actor;              ab.buildType();              ab.buildSex();              ab.buildFace();              ab.buildCostume();         //经过钩子方法来控制产品的构建              if(!ab.isBareheaded())              {                     ab. buildHairstyle();              }              actor=ab.createActor();              return  actor;       }}
[color=gray]当在客户端代码中指定具体建造者类型并经过指挥者来实现产品的逐步构建时,将调用钩子方法isBareheaded()来判断游戏角色是否有头发,若是isBareheaded()方法返回true,即没有头发,则跳过构建发型的方法buildHairstyle();不然将执行buildHairstyle()方法。经过引入钩子方法,咱们能够在Director中对复杂产品的构建进行精细的控制,不只指定buildPartX()方法的执行顺序,还能够控制是否须要执行某个buildPartX()方法。 8.5 建造者模式总结 建造者模式的核心在于如何一步步构建一个包含多个组成部件的完整对象,使用相同的构建过程构建不一样的产品,在软件开发中,若是咱们须要建立复杂对象并但愿系统具有很好的灵活性和可扩展性能够考虑使用建造者模式。 1.主要优势 建造者模式的主要优势以下: (1) 在建造者模式中,客户端没必要知道产品内部组成的细节,将产品自己与产品的建立过程解耦,使得相同的建立过程能够建立不一样的产品对象。 (2) 每个具体建造者都相对独立,而与其余的具体建造者无关,所以能够很方便地替换具体建造者或增长新的具体建造者,用户使用不一样的具体建造者便可获得不一样的产品对象。因为指挥者类针对抽象建造者编程,增长新的具体建造者无须修改原有类库的代码,系统扩展方便,符合“开闭原则” (3) 能够更加精细地控制产品的建立过程。将复杂产品的建立步骤分解在不一样的方法中,使得建立过程更加清晰,也更方便使用程序来控制建立过程。 2.主要缺点 建造者模式的主要缺点以下: (1) 建造者模式所建立的产品通常具备较多的共同点,其组成部分类似,若是产品之间的差别性很大,例如不少组成部分都不相同,不适合使用建造者模式,所以其使用范围受到必定的限制。 (2) 若是产品的内部变化复杂,可能会致使须要定义不少具体建造者类来实现这种变化,致使系统变得很庞大,增长系统的理解难度和运行成本。 3.适用场景 在如下状况下能够考虑使用建造者模式: (1) 须要生成的产品对象有复杂的内部结构,这些产品对象一般包含多个成员属性。 (2) 须要生成的产品对象的属性相互依赖,须要指定其生成顺序。 (3) 对象的建立过程独立于建立该对象的类。在建造者模式中经过引入了指挥者类,将建立过程封装在指挥者类中,而不在建造者类和客户类中。 (4) 隔离复杂对象的建立和使用,并使得相同的建立过程能够建立不一样的产品。[/color]