设计模式-3种工厂模式

设计工具

现在比较好的工具主要有Enterprise Architect和Visual Paradigm等大型UML设计工具。他们都美观使用,功能强大,支持 UML 到各种编程语言的正向和逆向生成。

Enterprise Architect的IDE整合插件比较旧,而且只支持32位的eclipse。在整合的过程中特别容易出现问题。所以我选用Visual Paradigm作为开发工具。

Visual Paradigm还支持鼠标手势,再也不用为找不到控件发愁了。

关系

继承关系

继承关系:继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己的新功能的能力。java中用关键字extends明确标识。

具体表现:

  • 一个类是另一个类的子类
  • 一个接口是另一个的子接口

UML类图:在UML的类图中,继承用一条带空心三角箭头的实线表示,从子类指向父类,或者子接口指向父接口。

实现关系

实现关系:实现指的是一个class类实现interface接口(可以是多个)的功能,实现是类与接口之间最常见的关系。Java中此类关系通过关键字implements明确标识

具体表现:

  • 一个类实现一个接口
  • 一个抽象类实现一个接口

UML类图:在UML的类图中,实现用一条带空心三角箭头的虚线表示,从类指向实现的接口。

依赖关系

依赖关系:其中一个模型元素是独立的,另一个模型元素不是独立的,它依赖于独立的模型元素,如果独立的模型元素改变了,将影响依赖于它的模型元素。

具体表现:

  • 一个类使用另一个类的对象作为操作的参数,
  • 一个类用另一个类的对象作为它的数据成员,
  • 一个类向另一个类发消息等。

UML类图:在UML的类图中,用带箭头的虚线连接有依赖关系的两个类,箭头指向独立的类

关联关系

关联关系:关联体现的是两个类之间语义级别的一种强依赖关系,比如我和我的朋友,这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。关联可以是单向、双向的。

具体表现:

  • 被关联类B以类的属性形式出现在关联类A中
  • 关联类A引用了一个类型为被关联类B的全局变量。

UML类图:在UML类图设计中,关联关系用由关联类A指向被关联类B的带箭头实线表示,在关联的两端可以标注关联双方的角色和多重性标记。

依赖关系和关联关系的区别(有待商榷):

  • 语义上:关联关系是一种更强的依赖关系

  • 代码上:

    • 关联关系是作为成员变量。
    • 依赖关系往往是一个类作为参数出现在另一个类方法的参数表中,利用该参数发消息。

聚合关系

聚合关系: 聚合是关联关系的一种特例,它体现的是整体与部分的关系,即has-a的关系。此时整体与部分之间是可分离的,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。

具体表现:和关联关系是一致的,只能从语义级别来区分。

UML类图:在UML类图设计中,聚合关系以空心菱形加实线箭头表示。

组合关系

组合关系:组合也是关联关系的一种特例,它体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合。它同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束,“同生共死”。

具体表现:和关联关系是一致的,只能从语义级别来区分。

UML类图:在UML类图设计中,组合关系以实心菱形加实线箭头表示。

设计原则

  • 单一职责原则:每个类只完成一个功能,或者只有一个能引起类变化的原因。
  • 开闭原则:对扩展开放,对修改关闭。
  • Liskov替换原则:子类可以替换父类出现在任何父类出现的地方。
  • 依赖倒置原则:细节应该依赖抽象,而不是抽象依赖细节;应该依赖接口编程,而不是依赖实现编程。
  • 接口隔离:使用多个接口,而不是使用单一的总接口->不应该以来不需要的接口。
  • 合成服用原则:尽量使用组合或聚合,而不是继承达到复用的目的。
  • 迪米特原则:一个实体类应该经量少的与其他类发生接触。

简单工厂模式

场景分析 - 绘图

Sunny软件公司欲基于Java语言开发一套图表库,该图表库可以为应用系统提供各种不同外观的图表,例如柱状图、饼状图、折线图等。Sunny软件公司图表库设计人员希望为应用系统开发人员提供一套灵活易用的图表库,而且可以较为方便地对图表库进行扩展,以便能够在将来增加一些新类型的图表。

初始设计方案,将所有图表的实现代码封装在一个Chart类中,其框架代码如下所示:

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")) {
			//显示折线图
		}	
	}
}

初始方案的问题分析:

  1. Chart类包含多个图表的功能,违反单一职责原则,不利于类的重用和维护。
  2. 当需要添加新的显示类型时,需要修改Chart类,违反开闭原则,不利于类的重用和维护。
  3. 大量的if else语句,代码相当冗长,代码越长,阅读难度、维护难度和测试难度也越大;而且大量条件语句的存在还将影响系统的性能,程序在执行过程中需要做大量的条件判断。
  4. 初始化代码都写在构造函数中将导致构造函数非常庞大,对象在创建时需要进行条件判断,降低了对象创建的效率。
  5. 客户端只能通过new关键字来直接创建Chart对象,Chart类与客户端类耦合度较高,对象的创建和使用无法分离。
  6. 客户端在创建Chart对象之前可能还需要进行大量初始化设置,例如设置柱状图的颜色、高度等,如果在Chart类的构造函数中没有提供一个默认设置,那就只能由客户端来完成初始设置,这些代码在每次创建Chart对象时都会出现,导致代码的重复。

简单工厂概述

简单工厂模式并不属于GoF 23个经典设计模式,但通常将它作为学习其他工厂模式的基础,它的设计思想很简单,其基本流程如下:

  1. 将需要创建的各种不同对象(例如各种不同的Chart对象)的相关代码封装到不同的类中,这些类称为具体产品类,而将它们公共的代码进行抽象和提取后封装在一个抽象产品类中,每一个具体产品类都是抽象产品类的子类
  2. 然后提供一个工厂类用于创建各种产品,在工厂类中提供一个创建产品的工厂方法,该方法可以根据所传入的参数不同创建不同的具体产品对象;
  3. 客户端只需调用工厂类的工厂方法并传入相应的参数即可得到一个产品对象。

简单工厂模式(Simple Factory Pattern):定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式

简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。简单工厂模式结构比较简单,其核心是工厂类的设计,其结构如图所示:

简单工厂

在简单工厂模式结构图中包含如下几个角色:

  • Factory(工厂角色):工厂角色即工厂类,它是简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑;工厂类可以被外界直接调用,创建所需的产品对象;在工厂类中提供了静态的工厂方法factoryMethod(),它的返回类型为抽象产品类型Product。
  • Product(抽象产品角色):它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象。
  • ConcreteProduct(具体产品角色):它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。每一个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法。

在简单工厂模式中,客户端通过工厂类来创建一个产品类的实例,而无须直接使用new关键字来创建对象,它是工厂模式家族中最简单的一员。

在使用简单工厂模式时,首先需要对产品类进行重构,不能设计一个包罗万象的产品类,而需根据实际情况设计一个产品层次结构,将所有产品类公共的代码移至抽象产品类,并在抽象产品类中声明一些抽象方法,以供不同的具体产品类来实现,典型的抽象产品类代码如下所示:

abstract class Product{
	//公有方法,一致的表现
	public void method(){
		//实现共同有的方法
	}

	//不同的产品有着不同的表现
	public void methodDiff(){
		//在具体产品中实现
	}
}

在具体产品类中实现了抽象产品类中声明的抽象业务方法,不同的具体产品类可以提供不同的实现,典型的具体产品类代码如下所示:

public class ConcreteProductA extends Product{
	//特有表现的方法实现
	public void methodDiff(){
		//在具体产品中实现
	}
}

public class ConcreteProductB{

	特有表现的方法实现
	public void methodDiff(){
		//在具体产品中实现
	}
}

简单工厂模式的核心是工厂类,在没有工厂类之前,客户端一般会使用new关键字来直接创建产品对象,而在引入工厂类之后,客户端可以通过工厂类来创建产品,在简单工厂模式中,工厂类提供了一个静态工厂方法供客户端使用,根据所传入的参数不同可以创建不同的产品对象,典型的工厂类代码如下所示:

public class Factory{

	public static Product craeteProduct(String type){
		Product prodcut = null;
		if(type.equalsIgnoreCase("A")){
			prodcut = new ConcreteProductA();
			//设置初始化设置,例如本例中的颜色,高度
		}
		else if(type.equalsIgnoreCase("B")){
			prodcut = new ConcreteProductA();
			//设置初始化设置,例如本例中的颜色,高度
		}
		
		return prodcut;
	}
}

在客户端代码中,我们通过调用工厂类的工厂方法即可得到产品对象,典型代码如下所示:

public class Client{

	public static void main(String[] args){
		Prodcut product;
		prodcut = Factory.createProduct("A");//通过工厂创建产品类
		a.method();//使用一致表现的方法
		a.methodDiff();//使用表现不同的方法
	}
}

场景问题解决方案

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

重构

Chart接口充当抽象产品类,其子类HistogramChartPieChartLineChart充当具体产品类,ChartFactory充当工厂类。完整代码如下所示:

interface class Chart{
	//展示的抽象方法
	public void display();
}

public class HistogramChart implements Chart{
	//构造函数
	public HistogramChart(){
		System.out.println("创建HistogramChart!");
	}
	
	public void display(){
		//HistogramChart展示的具体实现
		System.out.println("展示HistogramChart!");
	}
}


public class LineChart implements Chart{
	//构造函数
	public LineChart(){
		System.out.println("创建LineChart!");
	}
	
	public void display(){
		//LineChart 展示的具体实现
		System.out.println("展示LineChart!");
	}
}

public class PieChart implements Chart{
	//构造函数
	public PieChart(){
		System.out.println("创建PieChart!");
	}
	
	public void display(){
		//PieChart  展示的具体实现
		System.out.println("展示PieChart!");
	}
}

//工厂
public class ChartFactory{
	public static Chart getChart(String chartType){
		Chart chart = null;
		if(chartType.equalsIgnoreCase("histograme")){
			chart = new HistogramChart();
		}
		if(chartType.equalsIgnoreCase("line")){
			chart = new LineChart();
		}
		if(chartType.equalsIgnoreCase("pie")){
			chart = new PieChart();
		}
		
		return chart;
	}
}

客户端:


public class Clinet {
	
	public static void main(String[] args) {
		Chart chart;
		chart = ChartFactory.getChart("histograme");
		chart.display();
		chart = ChartFactory.getChart("line");
		chart.display();
		chart = ChartFactory.getChart("pie");
		chart.display();
	}
}

输出:

创建HistogramChart!
展示HistogramChart!
创建LineChart!
展示LineChart!
创建PieChart!
展示PieChart!

进一步改进方案

Sunny软件公司开发人员发现在创建具体Chart对象时,每更换一个Chart对象都需要修改客户端代码中静态工厂方法的参数,客户端代码将要重新编译,这对于客户端而言,违反了“开闭原则”。

有没有一种方法能够在不修改客户端代码的前提下更换具体产品对象呢?答案是肯定的,下面将介绍一种常用的实现方式。

静态工厂方法的参数存储在XMLproperties格式的配置文件中,如下config.xml所示:

<?xml version="1.0"?>  
<config>  
    <chartType>histogram</chartType>  
</config> 

再通过一个工具类XMLUtil来读取配置文件中的字符串参数,XMLUtil类的代码如下所示:

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;
        }
    }
}

客户端调用如下:

class Client {
	public static void main(String args[]) {
		Chart chart;
		String type = XMLUtil.getChartType(); //读取配置文件中的参数
		chart = ChartFactory.getChart(type); //创建产品对象
		chart.display();
	}
}

不难发现,在上述客户端代码中不包含任何与具体图表对象相关的信息,如果需要更换具体图表对象,只需修改配置文件config.xml,无须修改任何源代码,符合 开闭原则 。

思考:在上述设计中,需要添加新的Chart的时候,还是符合 开闭原则 的吗?该怎么修改?

简单工厂总结

简单工厂模式的主要优点如下:

  1. 工厂类包含必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的职责,而仅仅“消费”产品,简单工厂模式实现了对象创建和使用的分离。
  2. 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以在一定程度减少使用者的记忆量。
  3. 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。

简单工厂模式的主要缺点如下:

  1. 由于工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受到影响。
  2. 使用简单工厂模式势必会增加系统中类的个数(引入了新的工厂类),增加了系统的复杂度和理解难度。
  3. 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
  4. 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构

适用场景 在以下情况下可以考虑使用简单工厂模式:

  1. 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
  2. 客户端只知道传入工厂类的参数,对于如何创建对象并不关心。

练习

使用简单工厂模式设计一个可以创建不同几何形状(如圆形、方形和三角形等)的绘图工具,每个几何图形都具有绘制draw()和擦除erase()两个方法,要求在绘制不支持的几何图形时,提示一个UnSupportedShapeException。

简单工厂设计类图:

绘图工厂

代码实现如下:


public interface Shape {

	/**
	 * 绘图接口
	 */
	public void draw();

	/**
	 * 擦除图形接口
	 */
	public void erase();

}


public class Circle implements Shape {

	public Circle(){

	}

	public void finalize() throws Throwable {

	}

	/**
	 * 绘图接口
	 */
	public void draw(){

	}

	/**
	 * 擦除图形接口
	 */
	public void erase(){

	}

}

...

public class UnSupportedShapeException extends Exception {

	public UnSupportedShapeException() {
		super();
	}

	public UnSupportedShapeException(String message, Throwable cause, boolean enableSuppression,
			boolean writableStackTrace) {
		super(message, cause, enableSuppression, writableStackTrace);
	}

	public UnSupportedShapeException(String message, Throwable cause) {
		super(message, cause);
	}

	public UnSupportedShapeException(String message) {
		super(message);
	}

	public UnSupportedShapeException(Throwable cause) {
		super(cause);
	}
	
}


public class ShapeFactory {

	/**
	 * 
	 * [[@param](http://my.oschina.net/u/2303379)](http://my.oschina.net/u/2303379) shapeType
	 * [[@throws](http://my.oschina.net/throws)](http://my.oschina.net/throws) UnSupportedShapeException 
	 */
	public static Shape getShape(String shapeType) throws UnSupportedShapeException {
		Shape shape;
		if (shapeType.equalsIgnoreCase("circle")) {
			shape = new Circle();
		} else if (shapeType.equalsIgnoreCase("rectangle")) {
			shape = new Rectangle();
		} else if (shapeType.equalsIgnoreCase("triangle")) {
			shape = new Triangle();
		} else {
			throw new UnSupportedShapeException();
		}

		return shape;
	}

}


客户端代码:

public class Client {

	public static void main(String[] args) {
		Shape shape;
		try {
			shape  = ShapeFactory.getShape("circle");
			shape.draw();
			shape.erase();
		} catch (UnSupportedShapeException e) {
			e.printStackTrace();
		}
	}

}

工厂模式

简单工厂模式虽然简单,但存在一个很严重的问题。

当系统中需要引入新产品时,由于静态工厂方法通过所传入参数的不同来创建不同的产品,这必定要修改工厂类的源代码,将违背“开闭原则”,如何实现增加新产品而不影响已有代码?

工厂方法模式应运而生。

场景分析 - 日志记录器

日志记录器的设计。

Sunny软件公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系统的运行日志,如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,Sunny公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是Sunny公司开发人员面临的一个难题。

Sunny公司的开发人员通过对该需求进行分析,发现该日志记录器有两个设计要点:

  1. 需要封装日志记录器的初始化过程,这些初始化工作较为复杂,例如需要初始化其他相关的类,还有可能需要读取配置文件(例如连接数据库或创建文件),导致代码较长,如果将它们都写在构造函数中,会导致构造函数庞大,不利于代码的修改和维护;
  2. 用户可能需要更换日志记录方式,在客户端代码中需要提供一种灵活的方式来选择日志记录器,尽量在不修改源代码的基础上更换或者增加日志记录方式。

Sunny公司开发人员最初使用简单工厂模式对日志记录器进行了设计,初始结构如图所示:

logger简单工厂

LoggerFactory充当创建日志记录器的工厂,提供了工厂方法createLogger()用于创建日志记录器,Logger是抽象日志记录器接口,其子类为具体日志记录器。其中,工厂类LoggerFactory代码片段如下所示:

//日志记录器工厂
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;
		}
	}
}

工厂方法模式概述

在简单工厂模式中只提供一个工厂类,该工厂类处于对产品类进行实例化的中心位置,它需要知道每一个产品对象的创建细节,并决定何时实例化哪一个产品类。

简单工厂模式**最大的缺点是当有新产品要加入到系统中时,必须修改工厂类,需要在其中加入必要的业务逻辑,这违背了“开闭原则”。**此外,在简单工厂模式中,所有的产品都由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性,而工厂方法模式则可以很好地解决这一问题。

在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。工厂方法模式定义如下:

工厂方法模式(Factory Method Pattern):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式(Factory Pattern),又可称作虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式(Polymorphic Factory Pattern)。工厂方法模式是一种类创建型模式

工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法,创建具体的产品对象。工厂方法模式结构如图:

在工厂方法模式结构图中包含如下几个角色:

  • Product(抽象产品):它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。
  • ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。
  • Factory(抽象工厂):在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。
  • ConcreteFactory(具体工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。

与简单工厂模式相比,工厂方法模式最重要的区别是引入了抽象工厂角色,抽象工厂可以是接口,也可以是抽象类或者具体类,其典型代码如下所示:

interface Factory {
    public Product factoryMethod();
}

在抽象工厂中声明了工厂方法但并未实现工厂方法,具体产品对象的创建由其子类负责,客户端针对抽象工厂编程,可在运行时再指定具体工厂类,具体工厂类实现了工厂方法,不同的具体工厂可以创建不同的具体产品,其典型代码如下所示:

class ConcreteFactory implements Factory {  
    public Product factoryMethod() {  
        return new ConcreteProduct();  
    }  
}  

在实际使用时,具体工厂类在实现工厂方法时除了创建具体产品对象之外,还可以负责产品对象的初始化工作以及一些资源和环境配置工作,例如连接数据库、创建文件等。

在客户端代码中,只需关心工厂类即可,不同的具体工厂可以创建不同的产品,典型的客户端类代码片段如下所示:

......
Factory factory;
factory = new ConcreteFactory(); //可通过配置文件实现
Product product;
product = factory.factoryMethod();
......

可以通过配置文件来存储具体工厂类ConcreteFactory的类名,更换新的具体工厂时无须修改源代码,系统扩展更为方便。

思考:工厂方法模式中的工厂方法能否为静态方法?为什么? 不可以为静态方法。因为工厂方法模式的核心是引入抽象工厂角色,如果使用静态方法,工厂不能在运行时确定具体的工厂,影响系统的扩展性。

场景问题解决方案

Sunny公司开发人员决定使用工厂方法模式来设计日志记录器,其基本结构如图所示:

Logger接口充当抽象产品,其子类FileLoggerDatabaseLogger充当具体产品,LoggerFactory接口充当抽象工厂,其子类FileLoggerFactoryDatabaseLoggerFactory充当具体工厂。完整代码如下所示:

//日志记录器接口:抽象产品
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;
	}	
}

客户端测试:


class Client {
	public static void main(String args[]) {
		LoggerFactory factory;
		Logger logger;
		factory = new FileLoggerFactory(); //可引入配置文件实现
		logger = factory.createLogger();
		logger.writeLog();
	}
}

反射与配置文件

为了让系统具有更好的灵活性和可扩展性,Sunny公司开发人员决定对日志记录器客户端代码进行重构,使得可以在不修改任何客户端代码的基础上更换或增加新的日志记录方式。

在客户端代码中将不再使用new关键字来创建工厂对象,而是将具体工厂类的类名存储在配置文件(如XML文件)中,通过读取配置文件获取类名字符串,再使用Java的反射机制,根据类名字符串生成对象。在整个实现过程中需要用到两个技术:Java反射机制与配置文件读取。软件系统的配置文件通常为XML文件,我们可以使用DOM (Document Object Model)、SAX (Simple API for XML)、StAX (Streaming API for XML)等技术来处理XML文件。

Java反射(Java Reflection)是指在程序运行时获取已知名称的类或已有对象的相关信息的一种机制,包括类的方法、属性、父类等信息,还包括实例的创建和实例类型的判断等。在反射中使用最多的类是Class,Class类的实例表示正在运行的Java应用程序中的类和接口,其forName(String className)方法可以返回与带有给定字符串名的类或接口相关联的 Class对象,再通过Class对象的newInstance()方法创建此对象所表示的类的一个新实例,即通过一个类名字符串得到类的实例。如创建一个字符串类型的对象,其代码如下:

Class clazz = Class.forName("String");
Object object = clazz.newInstance();
return object;

在JDK中还提供了java.lang.reflect包,封装了其他与反射相关的类。

Sunny公司开发人员创建了如下XML格式的配置文件config.xml用于存储具体日志记录器工厂类类名:

<?xml version="1.0"?>
<config>
	<className>FileLoggerFactory</className>
</config>

为了读取该配置文件并通过存储在其中的类名字符串反射生成对象,Sunny公司开发人员开发了一个名为XMLUtil的工具类,其详细代码如下所示:

//工具类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;
         }
    }
}

使用Class.forName时,一定要注意包名(路径默认为src)。

有了XMLUtil类后,可以对日志记录器的客户端代码进行修改,不再直接使用new关键字来创建具体的工厂类,而是将具体工厂类的类名存储在XML文件中,再通过XMLUtil类的静态工厂方法getBean()方法进行对象的实例化,代码修改如下:


class Client {
	public static void main(String args[]) {
		LoggerFactory factory;
		Logger logger;
		factory = (LoggerFactory)XMLUtil.getBean(); //getBean()的返回类型为Object,需要进行强制类型转换
		logger = factory.createLogger();
		logger.writeLog();
	}
}

引入XMLUtil类和XML配置文件后,如果要增加新的日志记录方式,只需要执行如下几个步骤:

  1. 新的日志记录器需要继承抽象日志记录器Logger;
  2. 对应增加一个新的具体日志记录器工厂,继承抽象日志记录器工厂LoggerFactory,并实现其中的工厂方法createLogger(),设置好初始化参数和环境变量,返回具体日志记录器对象;
  3. 修改配置文件config.xml,将新增的具体日志记录器工厂类的类名字符串替换原有工厂类类名字符串;
  4. 编译新增的具体日志记录器类和具体日志记录器工厂类,运行客户端测试类即可使用新的日志记录方式,而原有类库代码无须做任何修改,完全符合“开闭原则”。

通过上述重构可以使得系统更加灵活,由于很多设计模式都关注系统的可扩展性和灵活性,因此都定义了抽象层,在抽象层中声明业务方法,而将业务方法的实现放在实现层中。

有人说:可以在客户端代码中直接通过反射机制来生成产品对象,在定义产品对象时使用抽象类型,同样可以确保系统的灵活性和可扩展性,增加新的具体产品类无须修改源代码,只需要将其作为抽象产品类的子类再修改配置文件即可,根本不需要抽象工厂类和具体工厂类。 试思考这种做法的可行性?如果可行,这种做法是否存在问题?为什么?

首先谈谈我们为什么使用工厂方法?为了在添加产品的时候不需要修改代码,满足“开闭原则”。本方法虽然也是可以满足开闭原则的,但是某些产品的构造并不是一气呵成的,需要一定的参数配置,使用反射,只能满足简单产品的情况,产品复杂了就处理不了了。即使是可以处理,并定会有很多额外的穿件准备工作散落在代码中,对待妈的可读性有很大的影响,也会造成创建蔓延。参考创建对象与使用对象——谈谈工厂的作用 ,该文中提到:与一个对象相关的职责通常有三类:对象本身所具有的职责、创建对象的职责和使用对象的职责。将这三种职责分离是好的变成习惯。对代码的复用有好处。两个类A和B之间的关系应该仅仅是A创建B或是A使用B,而不能两种关系都有。

重载工厂方法

Sunny公司开发人员通过进一步分析,发现可以通过多种方式来初始化日志记录器,例如可以为各种日志记录器提供默认实现;还可以为数据库日志记录器提供数据库连接字符串,为文件日志记录器提供文件路径;也可以将参数封装在一个Object类型的对象中,通过Object对象将配置参数传入工厂类。此时,可以提供一组重载的工厂方法,以不同的方式对产品对象进行创建。当然,对于同一个具体工厂而言,无论使用哪个工厂方法,创建的产品类型均要相同。如图:

引入重载方法后,抽象工厂LoggerFactory的代码修改如下:


interface LoggerFactory {
	public Logger createLogger();
	public Logger createLogger(String args);
	public Logger createLogger(Object obj);
}

具体工厂类DatabaseLoggerFactory代码修改如下:


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;
	}	
}

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

在抽象工厂中定义多个重载的工厂方法,在具体工厂中实现了这些工厂方法,这些方法可以包含不同的业务逻辑,以满足对不同产品对象的需求。

工厂方法隐藏

为了进一步简化客户端的使用,还可以对客户端隐藏工厂方法,此时,在工厂类中将直接调用产品类的业务方法,客户端无须调用工厂方法创建产品,直接通过工厂即可使用所创建的对象中的业务方法。 如果对客户端隐藏工厂方法,日志记录器的结构图将修改为:

抽象工厂中的业务代码主要实现默认的logger,然后调用默认logger的业务方法:

//改为抽象类
abstract class LoggerFactory {
    //在工厂类中直接调用日志记录器类的业务方法writeLog()
	public void writeLog() {
		Logger logger = this.createLogger();
		logger.writeLog();
	}
	
	public abstract Logger createLogger();	
}

客户端代码修改如下:


class Client {
	public static void main(String args[]) {
		LoggerFactory factory;
		factory = (LoggerFactory)XMLUtil.getBean();
		factory.writeLog(); //直接使用工厂对象来调用产品对象的业务方法
	}
}

工厂方法的总结

主要优点:

  1. 在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
  2. 基于工厂角色和产品角色的多态性设计是工厂方法模式的关键。它能够让工厂可以自主确定创建何种产品对象,而如何创建这个对象的细节则完全封装在具体工厂内部。工厂方法模式之所以又被称为多态工厂模式,就正是因为所有的具体工厂类都具有同一抽象父类。
  3. 使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了,这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。

主要缺点:

  1. 在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
  2. 由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。

适用场景:

  1. 客户端不知道它所需要的对象的类。在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建,可将具体工厂类的类名存储在配置文件或数据库中。
  2. 抽象工厂类通过其子类来指定创建哪个对象。在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。

练习

使用工厂方法模式设计一个程序来读取各种不同类型的图片格式,针对每一种图片格式都设计一个图片读取器,如GIF图片读取器用于读取GIF格式的图片、JPG图片读取器用于读取JPG格式的图片。需充分考虑系统的灵活性和可扩展性。

此题中,图片读取器不知道它具体要读的图片格式,我们可以使用工厂方法,抽象出读图器和图片。并针对每种图片有相关的读图器具体实现。 类图如下:

图片读取器类图

抽象工厂

工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是我们本文将要学习的抽象工厂模式的基本思想。

场景分析 - 界面皮肤库的初始设计

Sunny软件公司欲开发一套界面皮肤库,可以对Java桌面软件进行界面美化。为了保护版权,该皮肤库源代码不打算公开,而只向用户提供已打包为jar文件的class字节码文件。用户在使用时可以通过菜单来选择皮肤,不同的皮肤将提供视觉效果不同的按钮、文本框、组合框等界面元素,其结构示意图如图所示:

界面皮肤库

该皮肤库需要具备良好的灵活性和可扩展性,用户可以自由选择不同的皮肤,开发人员可以在不修改既有代码的基础上增加新的皮肤。

Sunny软件公司的开发人员针对上述要求,决定使用工厂方法模式进行系统的设计,为了保证系统的灵活性和可扩展性,提供一系列具体工厂来创建按钮、文本框、组合框等界面元素,客户端针对抽象工厂编程,初始结构如图所示:

工厂模式皮肤

在图中,提供了大量工厂来创建具体的界面组件,可以通过配置文件更换具体界面组件从而改变界面风格。但是,此设计方案存在如下问题:

  1. 当需要增加新的皮肤时,虽然不要修改现有代码,但是需要增加大量类,针对每一个新增具体组件都需要增加一个具体工厂,类的个数成对增加,这无疑会导致系统越来越庞大,增加系统的维护成本和运行开销;
  2. 由于同一种风格的具体界面组件通常要一起显示,因此需要为每个组件都选择一个具体工厂,用户在使用时必须逐个进行设置,如果某个具体工厂选择失误将会导致界面显示混乱,虽然我们可以适当增加一些约束语句,但客户端代码和配置文件都较为复杂。

如何减少系统中类的个数并保证客户端每次始终只使用某一种风格的具体界面组件?这是Sunny公司开发人员所面临的两个问题,显然,工厂方法模式无法解决这两个问题,抽象工厂模式可以让这些问题迎刃而解。

产品等级结构和产品族

  1. 产品等级结构:产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。

  2. 产品族:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中,海尔电视机、海尔电冰箱构成了一个产品族。

理解:产品等级结构相当于同种类型的产品,继承于同一个基类的产品(例子中的:SpringButton和SummerButton属于统一产品等级);产品族(不是字面上的意思)相当于同一个工厂生产的产品(SpringButton,SpringTextField和SpringComboBox属于同一产品族,都是SpringFactory生产)。

产品等级结构

在上图中,不同颜色的多个正方形、圆形和椭圆形分别构成了三个不同的产品等级结构,而相同颜色的正方形、圆形和椭圆形构成了一个产品族,每一个形状对象都位于某个产品族,并属于某个产品等级结构。图中一共有五个产品族,分属于三个不同的产品等级结构。我们只要指明一个产品所处的产品族以及它所属的等级结构,就可以唯一确定这个产品。

当系统所提供的工厂生产的具体产品并不是一个简单的对象,而是多个位于不同产品等级结构、属于不同类型的具体产品时就可以使用抽象工厂模式。

抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形式。 抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建。当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、更有效率。抽象工厂模式示意图如图所示:

每一个具体工厂可以生产属于一个产品族的所有产品,例如生产颜色相同的正方形、圆形和椭圆形,所生产的产品又位于不同的产品等级结构中。如果使用工厂方法模式,如图所示结构需要提供15个具体工厂,而使用抽象工厂模式只需要提供5个具体工厂,极大减少了系统中类的个数。

抽象工厂模式概述

抽象工厂模式为创建一组对象提供了一种解决方案。与工厂方法模式相比,抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品(即一个工厂生产不同种类的产品)。

抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式

在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的产品,这些产品构成了一个产品族,抽象工厂模式结构如图所示:

抽象工厂

在抽象工厂模式结构图中包含如下几个角色:

  • AbstractFactory(抽象工厂):它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。
  • ConcreteFactory(具体工厂):它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。
  • AbstractProduct(抽象产品):它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。
  • ConcreteProduct(具体产品):它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。

在抽象工厂中,声明了多个工厂方法,用于创建不同类型的产品,抽象工厂可以是接口,也可以是抽象类或者具体类,其典型代码如下所示:

abstract class AbstractFactory {
public abstract AbstractProductA createProductA(); //工厂方法一
public abstract AbstractProductB createProductB(); //工厂方法二
......
}

具体工厂实现了抽象工厂,每一个具体的工厂方法可以返回一个特定的产品对象,而同一个具体工厂所创建的产品对象构成了一个产品族。对于每一个具体工厂类,其典型代码如下所示:

class ConcreteFactory1 extends AbstractFactory {
    //工厂方法一
public AbstractProductA createProductA() {
    return new ConcreteProductA1();
}

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

......
}

与工厂方法模式一样,抽象工厂模式也可为每一种产品提供一组重载的工厂方法,以不同的方式对产品对象进行创建。

思考:抽象工厂模式是否符合“开闭原则”?【从增加新的产品等级结构和增加新的产品族两方面进行思考】 增加新的产品等级结构:不符合。增加新的产品族意味着工厂 加 生产一种产品,需要修改代码。 增加新的产品族:符合。需要增加一个工厂和新的产品等级全部类(新的产品等级:继承原来的每个等级结构中基类的产品类)。

一个产品族唯一对应一个工厂

场景问题解决方案

抽象工厂解决方案

SkinFactory接口充当抽象工厂,其子类SpringSkinFactory和SummerSkinFactory充当具体工厂,接口Button、TextField和ComboBox充当抽象产品,其子类SpringButton、SpringTextField、SpringComboBox和SummerButton、SummerTextField、SummerComboBox充当具体产品。完整代码如下所示:

//在本实例中我们对代码进行了大量简化,实际使用时,界面组件的初始化代码较为复杂,还需要使用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();
	}
}

为了让系统具备良好的灵活性和可扩展性,我们引入了配置文件,配置文件config.xml中存储了具体工厂类的类名,代码如下所示:

<?xml version="1.0"?>
<config>
	<className>SpringSkinFactory</className>
</config>

工具类XMLUtil和配置文件,其中,XMLUtil类的代码如下所示:


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;
       	}
	}
}

客户端调用


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();
	}
}

客户端调用结果:

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

如果需要更换皮肤,只需修改配置文件即可,在实际环境中,我们可以提供可视化界面,例如菜单或者窗口来修改配置文件,用户无须直接修改配置文件。如果需要增加新的皮肤,只需增加一族新的具体组件并对应提供一个新的具体工厂,修改配置文件即可使用新的皮肤,原有代码无须修改,符合“开闭原则”。

在真实项目开发中,我们通常会为配置文件提供一个可视化的编辑界面,类似Struts框架中的struts.xml编辑器,大家可以自行开发一个简单的图形化工具来修改配置文件,实现真正的纯界面操作。

“开闭原则”的倾斜性

Sunny公司使用抽象工厂模式设计了界面皮肤库,该皮肤库可以较为方便地增加新的皮肤,但是现在遇到一个非常严重的问题:由于设计时考虑不全面,忘记为单选按钮(RadioButton)提供不同皮肤的风格化显示,导致无论选择哪种皮肤,单选按钮都显得那么“格格不入”。

Sunny公司的设计人员决定向系统中增加单选按钮,但是发现原有系统居然不能够在符合“开闭原则”的前提下增加新的组件,原因是抽象工厂SkinFactory中根本没有提供创建单选按钮的方法,如果需要增加单选按钮,首先需要修改抽象工厂接口SkinFactory,在其中新增声明创建单选按钮的方法,然后逐个修改具体工厂类,增加相应方法以实现在不同的皮肤中创建单选按钮,此外还需要修改客户端,否则单选按钮无法应用于现有系统。

怎么办?答案是抽象工厂模式无法解决该问题,这也是抽象工厂模式最大的缺点。

在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式的这种性质称为“开闭原则”的倾斜性。

“开闭原则”要求系统对扩展开放,对修改封闭,通过扩展达到增强其功能的目的,对于涉及到多个产品族与多个产品等级结构的系统,其功能增强包括两方面:

  1. 增加产品族:对于增加新的产品族,抽象工厂模式很好地支持了“开闭原则”,只需要增加具体产品并对应增加一个新的具体工厂,对已有代码无须做任何修改。
  2. 增加新的产品等级结构:对于增加新的产品等级结构,需要修改所有的工厂角色,包括抽象工厂类,在所有的工厂类中都需要增加生产新产品的方法,违背了“开闭原则”。

正因为抽象工厂模式存在“开闭原则”的倾斜性,它以一种倾斜的方式来满足“开闭原则”,为增加新产品族提供方便,但不能为增加新产品结构提供这样的方便,因此要求设计人员在设计之初就能够全面考虑,不会在设计完成之后向系统中增加新的产品等级结构,也不会删除已有的产品等级结构,否则将会导致系统出现较大的修改,为后续维护工作带来诸多麻烦。

抽象工厂模式总结

抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了功能更为强大的工厂类并且具备较好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和API类库的设计中,例如在Java语言的AWT(抽象窗口工具包)中就使用了抽象工厂模式,它使用抽象工厂模式来实现在不同的操作系统中应用程序呈现与所在操作系统一致的外观界面。抽象工厂模式也是在软件开发中最常用的设计模式之一。

主要优点

  1. 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
  2. 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象
  3. 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。

主要缺点:

增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。

适用场景:

  1. 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是很重要的,用户无须关心对象的创建过程,将对象的创建和使用解耦。
  2. 系统中有多于一个的产品族,而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。
  3. 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品可以是没有任何关系的对象,但是它们都具有一些共同的约束,如同一操作系统下的按钮和文本框,按钮与文本框之间没有直接关系,但它们都是属于某一操作系统的,此时具有一个共同的约束条件:操作系统的类型。
  4. 产品等级结构稳定,设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。

练习

Sunny软件公司欲推出一款新的手机游戏软件,该软件能够支持Symbian、Android和Windows Mobile等多个智能手机操作系统平台,针对不同的手机操作系统,该游戏软件提供了不同的游戏操作控制(OperationController)类和游戏界面控制(InterfaceController)类,并提供相应的工厂类来封装这些类的初始化过程。软件要求具有较好的扩展性以支持新的操作系统平台,为了满足上述需求,试采用抽象工厂模式对其进行设计。

由于操作系统是变化因素,而游戏相关类比较固定(只有两个),为了方便不同操作系统的移植,设计UML图如下:

游戏控制

转载于:https://my.oschina.net/hgfdoing/blog/717227

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值