设计模式之简单工厂模式(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方法是个静态方法,我记得上面说到的调度类通常是静态方法吗😊
在getInstance()中,调用了createCalendar()方法,我们继续看这个方法的源码:
private static Calendar createCalendar(TimeZone zone, Locale aLocale) { CalendarProvider provider = LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale) .getCalendarProvider(); if (provider != null) { try { return provider.getInstance(zone, aLocale); } catch (IllegalArgumentException iae) { // fall back to the default instantiation } } Calendar cal = null; if (aLocale.hasExtensions()) { String caltype = aLocale.getUnicodeLocaleType("ca"); if (caltype != null) { switch (caltype) { case "buddhist": cal = new BuddhistCalendar(zone, aLocale); break; case "japanese": cal = new JapaneseImperialCalendar(zone, aLocale); break; case "gregory": cal = new GregorianCalendar(zone, aLocale); break; } } } if (cal == null) { // If no known calendar type is explicitly specified, // perform the traditional way to create a Calendar: // create a BuddhistCalendar for th_TH locale, // a JapaneseImperialCalendar for ja_JP_JP locale, or // a GregorianCalendar for any other locales. // NOTE: The language, country and variant strings are interned. if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") { cal = new BuddhistCalendar(zone, aLocale); } else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja" && aLocale.getCountry() == "JP") { cal = new JapaneseImperialCalendar(zone, aLocale); } else { cal = new GregorianCalendar(zone, aLocale); } } return cal; }
由上面的代码可以看出,真正的调度,是在createCalendar()里调度的,关键代码如下:
createCalendar()里,通过逻辑判断,调度返回哪个产品类。那么至此,产品规范类也就有了,那就是返回的Calendar。所以,在这个简单工厂模式中,Calendar既充当了工厂类角色,又充当了产品规范角色。Calendar是一个抽象类。
那么产品实现是谁呢,就是createCalendar()里的产品1,产品2,产品3.我们随便打开一个类,看其源码:
可以看到,产品继承了产品规范Calendar类,实现了其规范。
这样,简单工厂模式的几个角色,就都找出来了。
下面,我们看JDBC是如何利用简单工厂模式的。我们先看一下原生JDBC连接数据库的代码,这里,我们主要讲设计模式,所以,只展示部分JDBC代码:
import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public class JDBCDemo { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/mybatis"; String username = "root"; String password = "123456"; try { Class.forName("com.mysql.jdbc.Driver");// 指定连接类型 Connection conn = DriverManager.getConnection(url, username, password);// 获取连接 PreparedStatement statement = conn.prepareStatement("");// 准备执行语句 } catch (Exception e) { e.printStackTrace(); } } }
我们需要先引入mysql的驱动包,然后执行JDBC代码,才能连接mysql数据库。如果我们想连接Oracle数据库,则需要引入Oracle的驱动包,进行Oracle数据库连接。
同样的,我们找简单工厂模式的几个关键角色。首先,产品使用者就是写JDBC代码的程序员了。工厂是谁呢,没错,工厂是产品使用者调用的,肯定是在JDBC代码里出现的一个对象。稍加分析便可以知道,是DriverManager对象。调度方法就是getConnection()方法。
我们点进源码查看,DriverManager的getConnection()里调用了另一个私有的getConnection()方法,我们看这个私有的getConnection方法的关键源代码:
// Worker method called by the public getConnection() methods. private static Connection getConnection( ......省略......
for(DriverInfo aDriver : registeredDrivers) { // If the caller does not have permission to load the driver then // skip it. if(isDriverAllowed(aDriver.driver, callerCL)) { try { println(" trying " + aDriver.driver.getClass().getName()); Connection con = aDriver.driver.connect(url, info); if (con != null) { // Success! println("getConnection returning " + aDriver.driver.getClass().getName()); return (con); } } catch (SQLException ex) { if (reason == null) { reason = ex; } } } else { println(" skipping: " + aDriver.getClass().getName()); } } ......省略...... }
可以看出,getConnection方法循环遍历了一个registeredDrivers集合,满足一定的条件,返回Connection对象。这里就是在做调度工作,返回相应的产品。继续看源码可以得知,registeredDrivers是DriverManager维护的一个集合,DriverManager还提供了registerDriver()方法,来供产品注册驱动,注册的驱动,会加入到registeredDrivers集合中,供DriverManager工厂调度使用。
那么,谁是产品角色呢,就是我们引入的mysql驱动包,就是产品。他们都遵循了Driver接口规范。Driver接口是jdk定义的一个数据库连接的规范接口。至此,所有的角色都已经找到,可以看出,JDBC里也应用到了简单工厂模式。
是不是还有同学对JDBC模式的简单工厂模式很懵逼呢?那接下来,我们亲手实现一个产品类,来帮助大家理解。
我们都知道,连接mysql数据库,引入mysql数据库的驱动,连接oracle数据库,引入oracle数据库的驱动,这些都是产品的实现类。那我们能不能引入自定义的驱动呢?答案是肯定的。下面我们就按照Driver产品规范,自己实现一个产品类。
首先,创建产品实现类,并实现Driver规范:
import java.sql.*;
import java.util.Properties;
import java.util.logging.Logger;
public class SelfDriver implements Driver {
//这里需要注册到DriverManager里,DriverManager是工厂类,需要调度新产品,只有注册了才可以调度。
static {
try {
DriverManager.registerDriver(new SelfDriver());
} catch (SQLException e) {
e.printStackTrace();
}
}
//实现接口规范,这里我们输出自定义数据库字样,来判断是否走了我们自定义产品的方法。
@Override
public Connection connect(String url, Properties info) throws SQLException {
System.out.println("连接了自定义数据库");
return null;
}
@Override
public boolean acceptsURL(String url) throws SQLException {
return false;
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException {
return new DriverPropertyInfo[0];
}
@Override
public int getMajorVersion() {
return 0;
}
@Override
public int getMinorVersion() {
return 0;
}
@Override
public boolean jdbcCompliant() {
return false;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
产品创建好了,也注册到了工厂类中,那接下来,我们在JDBC中使用我们的产品 即可。如下代码:
import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; public class JDBCDemo { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/mybatis"; String username = "root"; String password = "123456"; try { //这里注册我们自己的产品驱动 Class.forName("demo.SelfDriver");// 指定连接类型 Connection conn = DriverManager.getConnection(url, username, password);// 获取连接 PreparedStatement statement = conn.prepareStatement("");// 准备执行语句 } catch (Exception e) { e.printStackTrace(); } } }
运行JDBC,可以看到输出结果:
说明我们自己写的驱动运行了。
接下来,请大家思考一下,JDBC的简单工厂模式,是怎么实现开闭原则的呢?
首先,我们定义新的产品类,实现Driver接口规范,这是对外扩展必须的操作,没有问题。
然后,我们需要在产品类里,调用DriverManager的registerDriver方法,将我们的产品注册到DriverManager中,在registerDriver方法中,就把新的产品,加入到了我们上面提到的registeredDrivers集合中,这样,就无需修改工厂类代码,直接用新产品了。
所以,JDBC是通过在新产品中注册的方式,实现了开闭原则,这种方式,值得大家借鉴。
好了,简单工厂模式就为大家分享到这里😃😃😃