设计模式——从工厂方法模式到 IOC/DI思想
回顾简单工厂
前面说到了简单工厂的本质是选择实现,说白了是由一个专门的类去负责生产我们所需要的对象,从而将对象的创建从代码中剥离出来,实现松耦合。我们来看一个例子:
我们要创建一个文件导出工具
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文件的导出,后续,我们可以自己实现一个txt文件的导出类,只需要实现FileOper接口就好:
public Class TxtFileOp implment FileOper{
public Boolean ExceptFile(String data){
System.out.println("导出txt文件");
return true;
}
}
这时候我们还是通过工厂来获取这个对象,只需将Factory中追加一个else if 即可通过传参来获取想要的对象了。
工厂方法模式
仔细分析上面的场景,事实上在实现导出文件的业务逻辑中,它根本不知道要使用哪一种导出文件的格式,因此这个对象根本就不应该和具体导出文件的对象耦合在一起,它只需要面向导出文件接口(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; }
/** * 将指定数据导出为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?
想想之前没有学习设计模式,刚学会使用Java就被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的思想是相似的,都是“主动变被动”,“主位换从位”,从而获得了更加灵活的程序结构。