简单工厂
前面说到了简单工厂的本质是选择实现,说白了是由一个专门的类去负责生产我们所需要的对象,从而将对象的创建从代码中剥离出来,实现松耦合。假设需求是要创建一个文件导出工具,以简单工厂实现这个需求:
//定义接口
public interface FileOper{
public Boolean exceptFile(String data);
}
//具体实现类
public class XMLFileOp implment FileOper{
public Boolean exceptFile(String data){
System.out.println("导出一个xml文件");
return true;
}
}
//工具工厂
public class Factory{
public static FileOper createFileOp(){
return new XMLFileOp();
}
}
//使用
public Class Test{
public static void main(String args[]){
FileOper op = Factory.createFileOp();
op.exceptFile("测试");
}
}
后续需求变动,除了要导出XML外,还可能用到导出Text文件,按着这个思路,需要再创建一个子类,用于导出Text:
//新建Text导出子类
public Class TxtFileOp implment FileOper{
public Boolean ExceptFile(String data){
System.out.println("导出txt文件");
return true;
}
}
这时候我们还是通过工厂来获取这个对象,只需将Factory中追加一个else if 即可通过传参来获取想要的对象了。
//工具工厂
public class Factory{
public static FileOper createFileOp(String type){
if("xml".equals(type)){
return new XMLFileOp();
}else{
return new TextFileOp();
}
}
}
简单工厂的不足
仔细分析上面的场景,事实上在实现导出文件的业务逻辑中,它根本不知道要使用哪一种导出文件的格式,因此这个对象根本就不应该和具体导出文件的对象耦合在一起,它只需要面向导出文件接口(FileOper)就好,这是工厂的思想使用简单工厂模式没有问题,但是随着后续来的需求,面对新的类,简单工厂便不能提供动态的扩展,必须要去修改内部的代码,破坏了开闭原则。简单工厂虽然对依赖对象的主体实现了解耦,可是它本身内部却耦合较为严重。
工厂方法模式
Product:工厂方法所创建的具体对象的统一接口。
Factory:为该类产品的抽象工厂,其内部有声明的工厂方法,工厂方法多为抽象方法,且返回一个Product对象
ProductOne:为具体的产品,也就是Product的具体实现类,真正的工厂产物。
SpecificFactory:具体的工厂,用于生产指定类型的Product,例如图中它只负责生产 ProductOne这个对象。
样例代码
public class AbstractFactoryTest {
public static void main(String[] args) {
try {
Product a;
AbstractFactory af;
af = (AbstractFactory) ReadXML1.getObject();
a = af.newProduct();
a.show();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
//抽象产品:提供了产品的接口
interface Product {
public void show();
}
//具体产品1:实现抽象产品中的抽象方法
class ConcreteProduct1 implements Product {
public void show() {
System.out.println("具体产品1显示...");
}
}
//具体产品2:实现抽象产品中的抽象方法
class ConcreteProduct2 implements Product {
public void show() {
System.out.println("具体产品2显示...");
}
}
//抽象工厂:提供了厂品的生成方法
interface AbstractFactory {
public Product newProduct();
}
//具体工厂1:实现了厂品的生成方法
class ConcreteFactory1 implements AbstractFactory {
public Product newProduct() {
System.out.println("具体工厂1生成-->具体产品1...");
return new ConcreteProduct1();
}
}
//具体工厂2:实现了厂品的生成方法
class ConcreteFactory2 implements AbstractFactory {
public Product newProduct() {
System.out.println("具体工厂2生成-->具体产品2...");
return new ConcreteProduct2();
}
}
基于XML解析的外部配置文件
class ReadXML1 {
//该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
public static Object getObject() {
try {
//创建文档对象
DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = dFactory.newDocumentBuilder();
Document doc;
doc = builder.parse(new File("src/FactoryMethod/config1.xml"));
//获取包含类名的文本节点
NodeList nl = doc.getElementsByTagName("className");
Node classNode = nl.item(0).getFirstChild();
String cName = "FactoryMethod." + classNode.getNodeValue();
//System.out.println("新类名:"+cName);
//通过类名生成实例对象并将其返回
Class<?> c = Class.forName(cName);
Object obj = c.newInstance();
return obj;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
上面是一个较为初级也较为经典的工厂方法模板,工厂方法还有另一种用法,即抽象的工厂父类除了创建对象的方法之外,还包含其他的一些方法,而工厂父类通常使用这些方法来完成某些任务。
文件导出的例子
回到上面用简单工厂实现的文件导出例子,用工厂模式实现则是:
/**
* 文件导出接口
* 实现将指定数据的导出
* 扩展:实现该接口,可指定生产具体文件类型
* @author GCC
*/
public interface ExportFileApi {
/**
* 导出文件
* @param data 待导出数据
* @return boolean
*/
boolean exportFile(String data);
}
/**
* 生产Excel文件导出器
* @author GCC
*/
public class ExportExcelFile implements ExportFileApi {
@Override
public boolean exportFile(String data) {
//todo 处理数据
return false;
}
}
/**
* 导出功能口,工厂
* @author GCC
*/
public abstract class ExportFileOperate {
public Logger logger = Logger.getLogger(ExportFileOperate.class);
//使用产品对象来实现一定功能的方法,这里是实现数据导出
public void export(String data){
ExportFileApi exportoper = methodFactory();
if(exportoper.exportFile(data)){
logger.info("文件导出成功");
return;
}
logger.error("文件导出失败");
}
//工厂方法
protected abstract ExportFileApi methodFactory();
}
/**
* 将指定数据导出为Excel文件
* @author GCC
*/
public class ExportExcelFileFactory extends ExportFileOperate {
@Override
protected ExportFileApi methodFactory() {
return new ExportExcelFile();
}
}
/**
* 客户用例
*/
public class App
{
static Logger logger = Logger.getLogger(App.class);
public static void main( String[] args )
{
ExportFileOperate ex = new ExportExcelFileFactory();
ex.export("测试数据");
}
}
这里可能会有疑惑,这个实现方式怎么和前面提到的工厂方法模式的标准样例不一样? 其实这是工厂方法模式的另一种结构,确切地说这才是真正意义上的工厂方法模式(上面的模板只是工厂方法的正常形态)。
这一种的实现方式是 :父类会是一个抽象类,里面包含创建所需对象的抽象方法(代码样例中ExportFileOperat类的methodFactory()方法,这里ExportFileOperat类就是所谓的工厂类,需要补充的是设计模型的使用,不要拘泥于命名名称,可以根据实际需求来进行相应的变化),这些抽象方法就是工厂方法模式中的工厂方法。父类里面,通常会有使用这些产品对象来实现一定的功能的方法(代码样例中ExportFileOperat类的export()方法)。而这些方法所实现的功能通常都是公共功能,不管子类选择了何种具体的产品实现,这些方法总能正常运行。
之所以会有上面两种方式,主要原因在于工厂方法对于客户端的支持,这里需要弄清楚一个问题,谁在使用工厂方法所创建的对象?
事实上,在工厂方法模式里,应该是工厂中的其他方法来使用工厂所创建的对象,为了方便,工厂方法创建的对象也可直接提供给外部的客户端来调用,但工厂方法的本意是由Factory抽象父类内部的方法来使用工厂方法创建的对象。
以下这幅时序图,即说明了客户端调用factory的两种方式。
其实客户端应该使用Factory对象,或者是由Factory所创建出来的产品对象,对于客户端使用Factory对象,这个时候工厂方法创建的对象,是Factory中的某些方法在用,对于使用那些由Factory创建出来的对象,这个时候工厂方法创建的对象,是构成客户端所需对象的一部分。
工厂方法模式与简单工厂的区别
下面以计算器的例子来看一下,如果用简单工厂模式来做,它的结构是这样的:
public class SimpleFactory {
public Calculator create(String type){
switch (type){
case "+":
//返回一个具有加法功能的计算器对象
return new AddCalculator();
case "-":
//返回一个具有减法功能的计算器对象
return new DeCalculator();
default:return null;
}
}
}
为了工厂更完整,采用传参的静态工厂方式来实现,这样我简单工厂里将通过Switch语句来管控生产哪一种计算类,当新增乘法的功能的需求时,就得实现计算器接口,完成一个乘法类,同时去简单工厂的代码里,追加一个case。
而使用工厂模式:
使用工厂方法的模式来做这个功能,这块的类图则如上图一样,我的工厂代码里不需要Switch了,只需要一个具有生产计算器对象的抽象方法的抽象工厂类即可,当我需要一个乘法能力的计算器时,实现计算器接口,完成乘法类,然后继承抽象工厂,完成一个乘法的工厂子类,然后再用乘法的工厂子类来创建乘法类。然后再去修改客户端。
经过对比,发现升级版的工厂方法怎么比简单工厂还复杂了,明显简单工厂更方便。
其实回到设计原则视角就能解答这个困惑,这是一个解耦的过程,简单工厂模式最大的优点在于工厂类中包含了必要的逻辑判断,根据客户的选择动态的实例化相关的产品类,对于客户端来说,除去了与具体产品对象的依赖。但问题就是随着你的新需求,如果使用简单工厂,那么你就不得不去破坏开闭原则,而看起来改动更为复杂的工厂方法模式,你并不需要对以前的代码进行改动,只需要继承,扩展即可。仔细观察一下,简单工厂是让客户端与依赖对象进行解耦,而工厂方式模式又是对工厂的一层解耦,原本内部耦合性较强的if else,变成了由客户端或者配置文件来控制,工厂方法将简单工厂内部的逻辑判断移到了使用它的外部(客户端或者配置文件)来控制。本来扩展是需要修改工厂类源代码的,现在变成了客户端修改调用或者配置文件中的一个参数。
工厂方法模式的意义
工厂方法模式的主要思想是让父类在不知情的情况下,完成自身功能的调用,而具体的实现则延迟到子类来做;或者说是在静态工厂中,将其原本耦合的if else抽离出来,配合配置文档使用,将写死的if else灵活实现(配置文件并不是默认必须要有的)。这样在设计的时候,不用去考虑具体的实现,需要某个对象,把它通过工厂方法返回就好,在使用这些对象实现功能的时候还是通过接口来操作,这里就有一点IOC的韵味了。
工厂方法模式与IOC、DI
什么是IOC、DI
说起这个问题,就会想到被Spring的bean配置文件支配的恐惧,你说自己学Java,对方一定会问你Spring,说到Spring,肯定避不开“什么是IOC,什么是DI?”这个让人头痛的问题,那么,到底什么是IOC,DI?
看完工厂的设计思想,对这个问题才开始了真正的思考。
IOC——控制反转
DI——依赖注入
除了上面脱口而出的回答,我想我们这些面向对象的程序猿们,应该有个更深入的理解,到底什么是控制反转,什么是依赖注入。要想理解上面两个概念,必须把问题拆开来看,先搞清楚基本的问题几个问题:
主客体是谁,或者说参与这个概念的都有谁?
什么叫依赖?为什么会有依赖?
什么叫注入?注入的是什么?谁注入谁?
控制反转,谁控制谁,控制的是什么,既然叫反转,正转是啥?
下面我们一个个来解决问题:
1、参与者,说起参与者,一般我们在这个概念中是有三个参与者,具体某个类,容器,某个对象所依赖的外部资源(另一个对象),就好比我有三个类,A,B,C,A对象我们把它想象成一个客户端,B是它需要的一个外部的资源,C是一个叫容器的第三方。
2、什么叫依赖,这个就比较好说了,你有一个A类,但是你A类的成员变量有一个是B类的实例声明,那么A就依赖于B,也就是说,A如果想正常运转(或者说功能正常),必须得依赖于它的成员变量B,至于为什么会有依赖,那也好理解了,面向对象就是将功能封装,每个对象都功能单一,这样有些复杂的对象需要实现复杂的功能,就必须需要其他类的协同。
3、注入,就是说,A你的成员变量B只是声明了一个变量,它对于对象A来说,只是一个引用,一个字符,本身并没有实体,你可以new 一下这个变量的构造函数,才能使这个变量真正有灵魂,又或者用它来承接外部传进来的同类实体,这里外部传进来B的方式就叫注入,注入的是这个变量类型 具体的实例化对象。谁来注入,当然是容器C来注入给A,将B注入给A
4、简单来说就是容器来控制A,控制的就是A所依赖的对象B实例的创建,反转是与正转对应来说的,什么是正转呢,A类中有个对象B的成员变量,正常情况下,A类中功能用到B对象的时候,A要主动去获取一个B对象,例如new一下,这种情况被称为正向的。这样就比较好理解反转了,A不再去主动获取B对象了,而是被动的等待B的到来(注入),等待容器C获取一个B的实例,然后反向的注入进A。
所以,综上来看,控制反转和依赖注入其实说的是同一件事,说白了就是对象创建这个责任归谁的问题,依赖注入是从应用程序的角度去描述,应用程序依赖外部容器去创建并注入它所需要的外部资源对象。控制反转是从容器的角度出发,容器控制应用程序,由容器反向地向应用程序注入其所需要的外部对象。
其实IOC/DI并不是一种代码实现,更多的它是一种思想,它从思想上完成了 “主从换位” 的变化,应用程序本来是主体,占绝对地位,它需要什么都会主动出击去获取,过强的控制欲导致了它耦合过重,而在IOC/DI思想中,应用程序变成被动的等待容器的注入,需要啥只能提出来,什么时候给,给什么样子的,主动权完全交给了容器,较强的实现了解耦,程序的灵活性也就高了
工厂方法与IOC、DI思想
从某个角度来看,工厂方法模式跟IOC/DI的思想很贴近。
上面也说过了,IOC/DI就是让应用程序不再主动获取外部资源,而是被动等待第三方的注入,那么在编写程序的时候,一旦遇到需要外部资源的地方,就会开一个窗口,提供给容器一个注入的途径,让容器注入进来,细节这里就不过多赘述了,自己去找Spring聊吧。 下面用IOC/DI和工厂方法来实现一个样例对比一下。
用IOC/DI来实现一个类Person:
public class Person {
private String name;
//依赖文件操作对象
private FileUtil fileUtil;
private int age;
//提供set方法,供外部注入
public void setFileUtil(FileUtil fileUtil){
this.fileUtil = fileUtil;
}
public void opFile(String fileurl){
fileUtil.createFile(fileurl);
}
}
这就是IOC/DI思想来实现的一个类,我依赖FileUtil,没事,我不管,我提供给你一个set的注入途径,剩下的我不管了,我就默认我用FileUtil的时候,它是真真切切存在在堆中的对象。(本质是当外部的容器,创建Person对象的时候,会发现它依赖FileUtil,然后容器去获取一个FileUtil,通过Person提供的Set方法,将获取的FileUtil对象注入进去,然后一个完整的Person对象就被制造出来了,这个过程Person角度来看,Person是无感的)
使用工厂方法来搞上面的例子:
public abstract class Person {
private String name;
private int age;
//交给子类去实现我的依赖
public abstract FileUtil getFileutil();
public void opFile(String fileurl){
getFileutil().createFile(fileurl);
}
}
这里,Person类也是需要用到FileUtil,但是它也不需要主动的去new一下FileUtil,而是通过抽象方法的形式,将FileUtil的实例化延申到子类去实现,其实就是变相地提供了一种注入渠道(标准bean中是通过set方法,容器调用Set方法注入需要的对象,而工厂方法则是通过实现子类,即让子类来完成依赖对象的注入)
仔细体会这两种写法,对比他们的实现,在思想层面来看,会发现,工厂方法模式和IOC/DI的思想是相似的,都是“主动变被动”,“主位换从位”,从而获得了更加灵活的程序结构。