-
设计模式之简单工厂模式(Simple Factory Pattern)
一、简单工厂模式的由来
所有设计模式都是为解决某类问题而产生的,那么简单工厂模式是为解决什么问题呢?我们假设有以下业务场景:
在一个学生选课系统中,文科生用户选课时,我们要获得文科生的所有课程列表;理科生用户选课时,获得理科生的所有课程列表;体育生用户选课时,获得体育生的所有课程列表;那我们的逻辑怎么写呢?很简单,我们用伪代码实现如下:
if(userType is 文科生){
return 文科生课程;
}else if(userType is 理科生){
return 理科生课程;
}else if(userType is 体育生){
return 体育生课程;
}
这时我们发现,不光在学生选课时需要这段逻辑,在学生查询课程时,也需要这段代码,于是我们理所应当的将这段代码复制到了查询课程的方法中,如果老师查询课程的方法中也要这段代码,那继续复制到老师查看课程的方法中。。。。。。
这就造成了重复性代码,如何让这段代码只写一遍,多处可以复用呢?我们可以将这段代码抽取成公共方法,将学生类型定义成参数。在调用的地方,通过传入不同的参数类型,返回不同的课程列表。这样就实现了一次代码,多次复用的效果。
以上的这种优化过程,其实就是简单工厂模式的生成过程。
二、什么是简单工厂模式?
在简单工厂模式中,程序员创建或者获得对象,不再用new Object()的方式进行,而是给工厂类传入参数,工厂类根据程序员传入参数的不同,返回不同的对象。
三、简单工厂模式的角色
简单工厂模式中,有以下几种角色,这些角色撑起了简单工程模式的实现:
- 产品规范:提供规范,供其它产品子类来实现这些规范;可以是接口,抽象类或者父类;
- 产品实现:产品规范的具体实现类;实现或者继承产品规范的接口,抽象类或父类;
- 调度类(工厂类):有了产品规范,也有了产品实现,那用哪个产品呢,由调度类来调度决定。
- 产品使用者:使用产品的地方,传入想使用的产品参数,通过调度类给产品使用者返回产品。
其中,产品规范可以是接口,抽象类或者父类,总之,它的作用就是定义产品的规范,想生产这个产品,必须遵循这套规范,以此达到统一的标准。产品实现类是子类,来实现接口的方法,定义具体的产品。产品实现类可以有很多个,只要实现了产品规范,都是产品的实现类。工厂类,对具体的产品实现类进行调度,通过逻辑判断,决定输出哪个产品。这里的产品使用者,就是我们敲代码的程序员,我们想要使用某一个对象或其他产品,只需往工厂类里传入相应参数,就能返回了想要的产品。
由此可见,设计模式是针对程序员服务的,程序员只需输入参数,就能获得产品。设计模式并不是针对软件用户的东西,所以,我们在学习设计模式时,一定要把这个观念树立起来:在设计模式中,用户就是我们敲业务代码的程序员,而我们在写设计模式时,我们服务的对象,也就成了业务代码程序员,而不是普通的软件用户了。
四、简单工厂模式的实现
下面,我们自己实现一套简单工厂模式的代码。由上面可知,实现该模式,其实就是实现产品规范、产品实现类、调度工厂类、使用者类几个角色即可。
首先,定义产品规范,产品规范可以是接口,抽象类或父类,这里,我们用接口定义。
/** * 产品规范接口:定义生产家具规范 */ public interface Furniture { /** * 成产家具的方法 */ public void createFurniture(); }
然后,我们定义两个产品的实现类,实现createFurniture()规范
public class Table implements Furniture{ @Override public void createFurniture() { System.out.println("生产桌子"); } } public class Chair implements Furniture{ @Override public void createFurniture() { System.out.println("生产椅子"); } }
然后,我们定义调度工厂类,工厂类需要进行调度,判断返回哪种家具。
注意:工厂类的调度方法,通常用static静态方法,方便使用者调用
/** * 工厂类,用于调度家具,返回程序员想要的家具 */ public class FurnitureFactory { /** * 调度方法,根据传入的type参数,判断返回哪种家具 * @param type * @return */ public static Furniture getFurniture(String type){ Furniture f=null; if("桌子".equals(type)){ f=new Table(); }else if("椅子".equals(type)){ f=new Chair(); } return f; } }
最后,定义调用类,来调用工厂,返回想要的家具
/** * 产品使用者 */ public class FurnitureUser { public static void main(String[] args) { //甲程序员需要桌子,传入桌子参数,获得桌子 Furniture f1=FurnitureFactory.getFurniture("桌子"); f1.createFurniture(); //乙程序员需要椅子,传入椅子参数,获得椅子 Furniture f2=FurnitureFactory.getFurniture("椅子"); f2.createFurniture(); } }
这样,一个简单工厂模式,就实现了。由此可见,作为简单工厂模式的使用者,即业务程序员而言,我们只关注给工厂类传参,获取对象即可。而作为简单工厂模式的设计者,我们需要定义产品规范,定义调度工厂类的调度逻辑。至于产品实现类,可以由多个第三方来实现。
五、缺点
缺点一:
我们继续上面的例子,进行进一步开发。加入现在我们对家具进行扩展,要生产沙发,我们需要怎么做呢?
首先,产品规范无需改动。
然后,产品的实现类,我们要进行扩展,新建沙发类,来实现沙发产品
public class Sofa implements Furniture{ @Override public void createFurniture() { System.out.println("生产沙发"); } }
接下来,工厂类里,因为我们新加了产品,所以,在调度方法中,需要把新的产品逻辑写进去
/** * 工厂类,用于调度家具,返回程序员想要的家具 */ public class FurnitureFactory { /** * 调度方法,根据传入的type参数,判断返回哪种家具 * @param type * @return */ public static Furniture getFurniture(String type){ Furniture f=null; if("桌子".equals(type)){ f=new Table(); }else if("椅子".equals(type)){ f=new Chair(); }else if("沙发".equals(type)){ f=new Sofa(); } return f; } }
这样,我们在使用者中,就可以生产沙发了
/** * 产品使用者 */ public class FurnitureUser { public static void main(String[] args) { //甲程序员需要桌子,传入桌子参数,获得桌子 Furniture f1=FurnitureFactory.getFurniture("桌子"); f1.createFurniture(); //乙程序员需要椅子,传入椅子参数,获得椅子 Furniture f2=FurnitureFactory.getFurniture("椅子"); f2.createFurniture(); //丙程序员需要沙发 Furniture f3=FurnitureFactory.getFurniture("沙发"); f3.createFurniture(); } }
由此可见,我们新加一个产品,需要新加一个产品类,这个无可厚非。但是我们还需要在工厂类中,修改调度逻辑,把新加的产品逻辑写进去。这个操作,就违反了开闭原则。
开闭原则:对外支持扩展,对内不允许修改。
新产品类的增加,体现了对外扩展的支持,但是修改调度类,又违背了不对内修改的原则。这就是简单工厂模式的缺点之一。
缺点二:
在上面的角色图中我们可以看出,工厂类连接了产品和使用者,是简单工厂模式的核心。加入工厂类出现了问题,那么所有角色都处于了瘫痪状态。
缺点三:
我们上述所讲的简单工厂模式,是标准模式,在实际应用中,很多时候,我们会对简单工厂模式进行变形,例如,产品规范我们用抽象类来定义,在产品规范中,我们有定义产品调度的方法,这时,这个抽象类,就具有了产品规范和工厂调度两个角色。还拿上面的代码举例子,我们可以这样写:
/** * 产品规范+产品调度:既定义产品的规范,又有产品调度的方法,合二为一 */ public abstract class Furniture { /** * 定义规范 */ public abstract void createFurniture(); /** * 调度方法 */ public static Furniture getFurniture(String type){ Furniture f=null; if("桌子".equals(type)){ f=new Table(); }else if("椅子".equals(type)){ f=new Chair(); }else if("沙发".equals(type)){ f=new Sofa(); } return f; } }
这样,Furniture就兼具了两种角色。那么它的实现类,就变成了继承Furniture类即可。这样改造,也是一种简单工厂模式的实现。
在这种实现中,就违反了单一责任原则。但是在某种情况下,我们可以违背其中的原则,不要求生搬硬套。这里只是举例说明,在很多框架实现中,都用了这种模式的实现方式。
六、改进
针对违背开闭原则的缺点,我们对代码进行改进。之所以违背了开闭原则,是因为工厂类里的调度方法,在新加产品时需要修改。那么,有什么办法可以不对其进行修改呢?思路就是要动态加载其产品类,这样,无论多少个产品,动态加载进来,就无需改动代码了。
如何进行动态加载呢?我们可以采用xml配置的方式,将产品实现类加入xml里,在工厂类里,通过反射,创建xml里配置的产品类。我们还可以使用注解,在产品实现类上定义注解,然后工厂类扫描注解,通过反射动态加载产品类。这样,我们就只需要修改xml或者在产品类中加注解就行了,无需修改工厂类的方法,这就满足了开闭原则。这种思路是不是很熟悉呢?我们在spring框架的使用中,不是就可以在xml中配置类,也可以通过注解声明类吗?没错,spring就是通过这种方式满足开闭原则的。
配置xml文件和自定义注解的方式实现简单工厂模式的代码,大家自行写一下 (●'◡'●)
七、优点
1.面向接口编程,体现了面向对象的思想;
2.用户在使用时可以直接根据工厂类去创建所需的实例,而无需了解这些对象是如何创建以及如何组织的。有利于整个软件体系结构的优化。
八、简单工厂模式在JDK中的应用
下面,我们来演示一下,在JDK中,用到的简单工厂模式。
首先,我们讲解Calendar类中用到的简单工厂模式:
上面说到,简单工厂模式由产品规范、产品实现、工厂调度、产品使用几个角色组成,那我们就一一找到这几个角色。
创建Calendar日历对象,我们是通过Calendar.getInstance()方法获得,在这里,我们就是产品使用者,调用getInstance()方法,获得产品。相应的,那Calendar就是工厂类了,因为使用者直接面对的是工厂。getInstance()方法就是工厂类的调度方法了,我们看其源码:
可以看到,getInstance方法是个静态方法,我记得上面说到的调度类通常是静态方法吗