设计模式之简单工厂模式(Simple Factory Pattern)

时间:2023-03-08 20:13:46
设计模式之简单工厂模式(Simple Factory Pattern)

一、简单工厂模式的由来

  所有设计模式都是为解决某类问题而产生的,那么简单工厂模式是为解决什么问题呢?我们假设有以下业务场景:

  在一个学生选课系统中,文科生用户选课时,我们要获得文科生的所有课程列表;理科生用户选课时,获得理科生的所有课程列表;体育生用户选课时,获得体育生的所有课程列表;那我们的逻辑怎么写呢?很简单,我们用伪代码实现如下:

if(userType is 文科生){

    return 文科生课程;

}else if(userType is 理科生){

    return 理科生课程;

}else if(userType is 体育生){

   return 体育生课程;

}

  这时我们发现,不光在学生选课时需要这段逻辑,在学生查询课程时,也需要这段代码,于是我们理所应当的将这段代码复制到了查询课程的方法中,如果老师查询课程的方法中也要这段代码,那继续复制到老师查看课程的方法中。。。。。。

  这就造成了重复性代码,如何让这段代码只写一遍,多处可以复用呢?我们可以将这段代码抽取成公共方法,将学生类型定义成参数。在调用的地方,通过传入不同的参数类型,返回不同的课程列表。这样就实现了一次代码,多次复用的效果。

  以上的这种优化过程,其实就是简单工厂模式的生成过程。

二、什么是简单工厂模式?

  在简单工厂模式中,程序员创建或者获得对象,不再用new Object()的方式进行,而是给工厂类传入参数,工厂类根据程序员传入参数的不同,返回不同的对象。

三、简单工厂模式的角色

  简单工厂模式中,有以下几种角色,这些角色撑起了简单工程模式的实现:

  •   产品规范:提供规范,供其它产品子类来实现这些规范;可以是接口,抽象类或者父类;
  •   产品实现:产品规范的具体实现类;实现或者继承产品规范的接口,抽象类或父类;
  •   调度类(工厂类):有了产品规范,也有了产品实现,那用哪个产品呢,由调度类来调度决定。
  •   产品使用者:使用产品的地方,传入想使用的产品参数,通过调度类给产品使用者返回产品。

设计模式之简单工厂模式(Simple Factory Pattern)

  其中,产品规范可以是接口,抽象类或者父类,总之,它的作用就是定义产品的规范,想生产这个产品,必须遵循这套规范,以此达到统一的标准。产品实现类是子类,来实现接口的方法,定义具体的产品。产品实现类可以有很多个,只要实现了产品规范,都是产品的实现类。工厂类,对具体的产品实现类进行调度,通过逻辑判断,决定输出哪个产品。这里的产品使用者,就是我们敲代码的程序员,我们想要使用某一个对象或其他产品,只需往工厂类里传入相应参数,就能返回了想要的产品。

  由此可见,设计模式是针对程序员服务的,程序员只需输入参数,就能获得产品。设计模式并不是针对软件用户的东西,所以,我们在学习设计模式时,一定要把这个观念树立起来:在设计模式中,用户就是我们敲业务代码的程序员,而我们在写设计模式时,我们服务的对象,也就成了业务代码程序员,而不是普通的软件用户了。

四、简单工厂模式的实现

  下面,我们自己实现一套简单工厂模式的代码。由上面可知,实现该模式,其实就是实现产品规范、产品实现类、调度工厂类、使用者类几个角色即可。

  首先,定义产品规范,产品规范可以是接口,抽象类或父类,这里,我们用接口定义。

/**
* 产品规范接口:定义生产家具规范
*/
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()方法就是工厂类的调度方法了,我们看其源码:

设计模式之简单工厂模式(Simple Factory Pattern)

  可以看到,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()里调度的,关键代码如下:

设计模式之简单工厂模式(Simple Factory Pattern)

  createCalendar()里,通过逻辑判断,调度返回哪个产品类。那么至此,产品规范类也就有了,那就是返回的Calendar。所以,在这个简单工厂模式中,Calendar既充当了工厂类角色,又充当了产品规范角色。Calendar是一个抽象类。

  那么产品实现是谁呢,就是createCalendar()里的产品1,产品2,产品3.我们随便打开一个类,看其源码:

设计模式之简单工厂模式(Simple Factory Pattern)

  可以看到,产品继承了产品规范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,可以看到输出结果:

设计模式之简单工厂模式(Simple Factory Pattern)

  说明我们自己写的驱动运行了。

  接下来,请大家思考一下,JDBC的简单工厂模式,是怎么实现开闭原则的呢?

  首先,我们定义新的产品类,实现Driver接口规范,这是对外扩展必须的操作,没有问题。

  然后,我们需要在产品类里,调用DriverManager的registerDriver方法,将我们的产品注册到DriverManager中,在registerDriver方法中,就把新的产品,加入到了我们上面提到的registeredDrivers集合中,这样,就无需修改工厂类代码,直接用新产品了。

  所以,JDBC是通过在新产品中注册的方式,实现了开闭原则,这种方式,值得大家借鉴。

  好了,简单工厂模式就为大家分享到这里