设计模式--抽象工厂之小解

[color=red]古文新编(级别:初级)[/color]

[color=brown][u]I. 动 机
II. 虚拟案例
III. 针对中国企业为系统建模
IV. 针对美国企业为系统建模
V. 引入工厂类
VI. 可配置化(切换)的工厂类
VII. 符合开闭原则的可配置化工厂类
VIII. 抽象工厂类
IX. 总结[/u][/color]

[color=darkred][b][size=medium]I. 动 机[/size][/b][/color]
设计模式相信大家早已不再陌生,尤其在Java语言被广泛使用以后 ,GoF设计模式更是被广大Java程序员所熟知。
抽象工厂模式作为GoF模式中最重要、最基本的一个 ,几乎无处不被使用。但是你真正地完全理解了抽象工厂设计模式了吗?要更好地理解它,不单要知道怎么去调用它,更重要的是如何写出一个抽象工厂框架。

看了许多抽象工厂设计模式的中文文档,总觉得阐述得还不够简单,文档格式的组织也不够良好,提供的代码不够完整,这都导致了读者要么没有耐心读完整篇文档,要么不能真正实践例子代码。

于是有了"新解"的想法(一厢情愿吧),即虚拟一个尽可能直观、简单的案例,尝试从一个更加通俗的角度去诠释它,并提供完整的可以运行的Eclipse工程去调试运行它,非常适合设计模式初学者。

[color=darkred][b][size=medium]II. 虚拟案例[/size][/b][/color]
为中国某企业开发的财务系统,系统涉及大量的财务运算。其中有一项为了员工每月净工资,其规则如下:
[quote]
基本工资 = ¥4000
奖金 = 基本工资 * 10%
个人所得税 = (基本工资 + 奖金) * 20%
中国员工的净工资 = (基本工资 + 奖金 - 个人所得税)
[/quote]
承担此开发任务的公司是一个叫MaxDO的软件公司,软件系统代号为Softo。

[color=darkred][b][size=medium]III. 针对中国企业为系统建模[/size][/b][/color]
为Softo系统建模如下:
[img]/upload/attachment/73037/3d35e276-a921-309d-8197-57d8c27805c6.png[/img]
[align=center]图1.[/align]
根据上面分析,ChineseSalary为Softo系统的业务规则类,公开caculate方法;query_chinese_salary.jsp调用业务规则类查询工资单,承担了客户端的角色。根据代码职责的不同进行人员分工:
1 Java Developer : 开发Java业务规则类(工资计算)
1 Jsps Developer: 编写前端页面进行工资查询

代码清单:
ChineseSalary.java
package com.maxdo.softo.service.impl;

public class ChineseSalary {
double baseSalary = 4000;

private double getTax() {
return (this.baseSalary + this.getBonus())* 0.2;
}

private double getBonus() {
return this.baseSalary * 0.1;
}

public double calculate() {
return this.baseSalary + this.getBonus() - this.getTax();
}
}


query_chinese_salary.jsp

<%@page import="com.maxdo.softo.service.impl.*" %>
<% ChineseSalary salary = new ChineseSalary(); %>
Your salary is <%=salary.calculate()%> RMB


运行query_chinese_salary.jsp结果如下:
Your salary is 3520.0 RMB

[color=darkred][b][size=medium]IV. 针对美国企业为系统建模[/size][/b][/color]
[color=darkred][b]1.问题提出[/b][/color]
为了拓展国际市场,MaxDO要把该系统移植给美国公司使用。美国企业的月净工资和中国有所不同,计算公式如下:
[quote]基本工资 = $4000
奖金 = 基本工资 * 15%
个人所得税 = (基本工资 * 15% + 奖金 * 25%)
员工的净工资 = (基本工资 + 奖金 - 个人所得税)
[/quote]

[color=darkred][b]2.解决方案[/b][/color]
参照为中国企业的建模经验,类似地建立AmericanSalary类。 修改后的系统结构如下:
[img]/upload/attachment/73039/6fd765df-d74a-3362-abf3-b0a06426fe01.png[/img]
[align=center]图2[/align]
代码清单
1. AmericanSalary

package com.maxdo.softo.service.impl;

public class AmericanSalary {
double baseSalary = 4000;

private double getTax() {
return (this.baseSalary * 0.15 + this.getBonus()* 0.25);
}

private double getBonus() {
return this.baseSalary * 0.15;
}http://koda.iteye.com/admin/blogs/323289/edit

public double calculate() {
return this.baseSalary + this.getBonus() - this.getTax();
}
}


2. query_american_salary.jsp
<%@page import="com.maxdo.softo.service.impl.*" %>
<% AmericanSalary salary = new AmericanSalary(); %>
Your salary is $<%=salary.calculate()%>


运行query_american_salary.jsp结果如下:
Your salary is $3850.0

[color=darkred][b][size=medium]V. 引入工厂模式[/size][/b][/color]
[color=darkred][b]1.问题提出[/b][/color]
到目前为止,且不谈代码的结构和质量,我们可以说:系统的工资计算模块已经构建好了。让我们再次回顾一下该系统的发展历程:
起初,我们只考虑将Softo系统运行于中国企业。但随着MaxDO公司业务向海外拓展, MaxDO需要将该系统移植给美国使用。为了之个移植时,MaxDO为美国企业新建业务规则类AmericanSalary,然后通知JSP开发人员(有可能是合作公司哦)增加一个客户端jsp页面并告知新的业务规则的调用方法.

按照这种开发模式,试想一个规模问题:某天如果该系统移植到印度、日本、比利时..., 将会出现什么样的开发模式?
每移植到一个新的国家,业务规则便增加了一个,这个无可厚非,但是每一次你都不得不请求JSP人员增加调用页面;而在jsp开发人员看来,每一次的移植他做的工作都很重复很无聊:

AmericanSalary.calculate();
ChineseSalary.calculate();
IndianSalary.calculate();
...

这真的有必要吗? 我关心的只是
Salary.calulate();


令人崩溃的是,财务系统中不只有工资计算这样一个规则根据国别不同存在差异,几乎每一个业务系统规则都有差异,极端地讲,它意味着这样一个可怕的事实:假设该系统的前端JSP页面有90个,那么系统移植到10个国家将有 90 * 10 = 900个jsp页面。

真的是一个很糟糕的系统:)

[color=darkred][b]2.解决方案:抽象出接口,增加静态工厂类[/b][/color]
规模化导致系统不可维护,切实减少客户端开发人员的工作量,怎们办?没错,客户端真正关心的仅仅是
Salary.calulate();

下面是新的系统结构:
[img]/upload/attachment/73042/c9bb8806-e5e9-31cd-8ceb-771924988e58.png[/img]
[align=center]图3[/align]
基于业务规则本身的共性,AmercianSalary,ChineseSalary能抽象出一个接口叫Salary. 但是Salary是不能实例化的,客户端JSP如何调用呢?为此我们定义了一个辅助类
Factory(很形象吧,Salary像是一个产品,Factory来生产它)提供一个静态方法为

public class Factory{
public static Salary createSalary(){
return new ChineseSalary();
// 当系统移植到美国使用时,改变 return new ChineseSalary()
// 到 return new AmericanSalary();
}
}

如此一来,客户端调用代码看起来就是

Salary s = Factory.createSalary();
s.calculate();

看起来真不错,JSP人员再也不抱怨了,他说:现在我只要一个salary_query.jsp来查询工资。

[color=red]仔细想想,问题真的解决了吗?根本没有,仅仅是问题转移![/color]
按照上面的方案,如果系统切换到美国公司使用,业务规则开发人员不得不修改Factory.createSalary()方法,将
return new ChineseSalary() 修改为 return new AmericanSalary();然后[color=red]重新编译打包[/color],从全局来看,这次重构问题非但没有解决问题,而且问题显得更加复杂起来了。
为了避免每次移植修改代码,那么可以为每个国家运行的Softo生成不同版本,发行不同版本的光盘,其实光盘内容的差异仅仅是 Factory类的createSalary的差异。维护成本越来越高了。

[color=darkred][b][size=medium]VI. 可配置化(切换)的工厂类[/size][/b][/color]
[color=darkred][b]1.问题提出[/b][/color]
上面末尾已经阐述了:问题仅仅是转移。createSalary()的切换工作,仍然引发系统移植时致命的问题(发行大量不同国家版本的的光盘...)。

[color=darkred][b]2.解决方案[/b][/color]
引入使用配置文件,配置文件中定义一个标识符
代码清单:新增config.properties文件到源代码根目录,内容如下:
[quote]salary=Chinese[/quote]

然后修正Factory类的createSalary方法()

代码清单:Factory.java

package com.maxdo.softo.service;

import com.maxdo.softo.service.impl.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class Factory {
public static public Salary createSalary() {
Properties prop = new Properties();
InputStream stream = null;
try {
stream = Factory.class.getResourceAsStream("/config.properties");
prop.load(stream);
}catch(IOException e){
e.printStackTrace();
}
String salaryName = prop.getProperty("salary");
Salary salary = null;
if(salaryName.equals("Chinese")) {
return new ChineseSalary();
} else if(salaryName.equals("American")) {
return new AmericanSalary();
}
return salary;
}
}


观察上面代码20~25行,每当系统移植时,修改配置文件的标示符'salary',Factory通过一个条件分支就可以初始化相应的Salary。

如此一来,将Salary具体实例化切换转换到配置文件之后,得到了两个好处:
1. 只需要一个代码版本的光盘。
2. 系统切换方法是:每当发布系统到相应的国家,只需要配置人员(而不是开发者)修改配置文件就可以了。甚至你可以写一个页面通过下拉框选择国别处罚配置文件的更改。

[color=darkred][b][size=medium]VII. 符合开闭原则的可配置化工厂类[/size][/b][/color]
[color=darkred][b]1.问题提出[/b][/color]
前面的方案接近完美:) 试想:依照前面的方案,如果系统现在移植到日本,要做哪些事情?
1. 增加JapaneseSalary实现Salary接口
2. 修改Factory类方法,增加一个条件分支像这样:

if(salaryName.equals("Chinese")) {
return new ChineseSalary();
} else if(salaryName.equals("American")) {
return new AmericanSalary();
// 下面是新增的行
} else if(salaryName.equals("Japanese")) {
return new AmericanSalary();
}

重新编译原来的代码。

无论如何,系统的扩展要求增加业务规则类JapaneseSalary没有异议,也是不可避免的,IV提供的方案有Salary接口为其提供支持。问题在于:Factory的createSalary()被修改了,导致软件生成了一个新的版本,大多数情况,这也是可以接受的。
但是在某种情况,这么做是完全不可能的。如果方案IV的Softo是MaxDo的闭源软件,Tasito软件公司购买它并扩展了Japanese的业务,那么他如何做到修改Factory类呢?

在软件开发中,有一个开闭(Open-Close)原则,就是说:软件功能的增加应尽可能通过扩展元代的代码(对扩展开发open),而不是修改存在的代码来完成(对修改开发close)

在方案IV中对Factory类的修改就违反了这个开闭原则

[color=darkred][b]2.解决方案[/b][/color]
稍加改进,就可以符合开闭原则

代码清单:Factory.java

package com.maxdo.softo.service;

import com.maxdo.softo.service.impl.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

public class Factory {
public static Salary createSalary() {
Salary instance = null;
Properties prop = new Properties();
InputStream stream = null;
try {
stream = Factory.class.getResourceAsStream("/config.properties");
prop.load(stream);
}catch(IOException e){
e.printStackTrace();
}
String salaryName = prop.getProperty("salary");
try {
instance = (Salary)Class.forName(salaryName).newInstance();
} catch (Exception e) {
e.printStackTrace();
instance = null;
}
return instance;
}
}


代码清单:修改config.properties文件到源代码根目录,内容如下:
[quote]salary=com.maxdo.softo.service.impl.ChineseSalary[/quote]

为了理解该方案,实际上只需要一个关键之知识点--Reflection:
Class.forName("YourClassName").newInstance();


如此这般,再有系统移植工作,只要配置人员更改一下配置文件的实际的Salary实现类名就一切搞定了。Perfect:)

[color=darkred][b][size=medium]VIII. 抽象工厂类[/size][/b][/color]

[color=darkred][b]1.问题提出[/b][/color]
可以这么说,就工资计算问题,方案V已经完美了。在这个模式中,我们创建Salary类不再是new的方式,而是通过一个辅助类的方法来创建,我们很形象地称这个辅助类为工厂类,createSalary方法叫工厂方法,显然ChineseSalary,AmericanSalary就是我们生产出来的产品了。

既然是工厂,就不应该仅仅生产出锤子、钳子,也许还有勺子。所以一个典型的Factory可能像这样有多个工厂方法
伪代码:
public class Factory {
public static Spoon createSpoon() { ... }
public static Salary createHammer() { ... }
public static Salary createHatchet() { ... }
}


试想这时如果依国别不同,各国"生产"的产品规格都不同,那么Factory代码看起来会是怎样?
伪代码:

public class Factory {
public static Spoon createSpoon() {
// read identifier to spoon variable
spoon = readFromPropertyFile("spoon");
if(spoon.equals("Chinese")) {
return new ChineseSpoon();
} else if(spoon.equals("American")) {
return new AmericanSpoon();
}
public static Hammer createHammer() {
// read identifier to hammer variable
hammer = readFromPropertyFile("hammer");
if(hammer.equals("Chinese")) {
return new ChineseHammer();
} else if(hammer.equals("American")) {
return new AmericanHammer();
}
public static Hatchet createHatchet() {
// read identifier to hatchet variable
hatchet = readFromPropertyFile("hatchet");
if(hatchet.equals("Chinese")) {
return new ChineseHatchet();
} else if(hatchet.equals("American")) {
return new AmericanHatchet();
}
}

相应地config.properties文件内容为:

spoon=om.maxdo.softo.service.impl.ChineseSpoon
hammer=om.maxdo.softo.service.impl.ChineseHammer
hatchet=om.maxdo.softo.service.impl.ChineseHatchet


哇,丑陋的Factory代码(大量的if/else),越来越复杂的配置文件(系统配置人员后来也疯了:))

[color=darkred][b]2. 解决方案:工厂类也可以抽象出来[/b][/color]
前面的问题说明,工厂类也是可以抽象出来的!中国的工厂生产中国的产品,美国的工厂生产美国的产品,于是产生下面的新的系统结构
[img]/upload/attachment/73041/d8cc3fef-b95e-3b39-9de1-3bf23636d217.png[/img]
[align=center]图4[/align]
这个就是抽象工厂模式!

代码清单:业务规则类

// (1) AbstractFactory ///
package com.maxdo.softo.service;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

abstract public class AbstractFactory {
abstract public Salary createSalary();

public static AbstractFactory getInstance() {
AbstractFactory instance = null;
Properties prop = new Properties();
InputStream stream = null;
try {
stream = AbstractFactory.class.getResourceAsStream("/config.properties");
prop.load(stream);
}catch(IOException e){
e.printStackTrace();
}
String factoryName = prop.getProperty("factoryName");
try {
instance = (AbstractFactory)Class.forName(factoryName).newInstance();
} catch (Exception e) {
e.printStackTrace();
instance = null;
}
return instance;
}
}
// (2) AmericanFactory
package com.maxdo.sample.service.impl;
import com.maxdo.softo.service.impl.AmericanSalary;
public class AmericanFactory extends AbstractFactory {
public Salary createSalary() {
return new AmericanSalary();
}
}
// (3). ChineseSalary ///
package com.maxdo.sample.service.impl;
import com.maxdo.softo.service.impl.ChineseSalary;
public class ChineseFactory extends AbstractFactory {
public Salary createSalary() {
return new ChineseSalary();
}
}


代码清单:客户端JSP文件

<%@page import="com.maxdo.softo.service.*" %>
<%
/ (4). query_salary.jsp /
AbstractFactory factory = AbstractFactory.getInstance();
Salary salary = factory.createSalary();
%>
Your salary is <%=salary.calculate()%> RMB


[color=darkred][b][size=medium]IX. 总结[/size][/b][/color]
现实的应用将有许多Factory的变种,而且可能要求Factory模式更加安全、高效,所以需要多线程安全和单体模式考虑。以后有时间可以探讨。
另外可以去Eclipse插件站下载一个叫PatternBox的插件来辅助学习Design Pattern.
最终版的代码可以在附件中下载(AbstractFactory.zip)

蝈蝈龙 2009.02.07
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值