名词解释

工厂模式又称工厂方法模式,是一种创建型设计模式,其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。

这种设计模式也是 Java 开发中最常见的一种模式,它的主要意图是定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

简单说就是为了提供代码结构的扩展性,屏蔽每一个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调用即可,同时,这也是去掉众多ifelse的方式。当然这可能也有一些缺点,比如需要实现的类非常多,如何去维护,怎样减低开发成本。但这些问题都可以在后续的设计模式结合使用中,逐步降低。

举个例子

女娲造人的故事

东汉《风俗通》记录了一则神话故事:“开天辟地,未有人民,女娲搏黄土做人”,讲述的内容就是大家非常熟悉的女娲造人的故事。开天辟地之初,大地上并没有生物,只有苍茫大地,纯粹而洁净的自然环境,寂静而又寂寞,于是女娲决定创造一个新物种(即人类)来增加世界的繁荣,怎么制造呢?

别忘了女娲是神仙,没有办不到的事情,造人的过程是这样的:首先,女娲采集黄土捏成人的形状,然后放到八卦炉中烧制,最后放置到大地上生长,工艺过程是没有错的,但是意外随时都会发生:

第一次烤泥人,感觉应该熟了,往大地上一放,哇,没烤熟!于是一个白人诞生了!(这也是缺乏经验的最好证明。)

第二次烤泥人,上一次没烤熟,这次多烤一会儿,放到世间一看,嘿,熟过头了,于是黑人诞生了!

第三次烤泥人,一边烧制一边察看,直到表皮微黄,嘿,刚刚好,于是黄色人种出现了!

这个造人过程是比较有意思的,是不是可以通过软件开发来实现这个过程呢?古人云:“三人行,必有我师焉”,在面向对象的思维中,万物皆对象,是对象我们就可以通过软件设计来实现。首先对造人过程进行分析,该过程涉及三个对象:女娲、八卦炉、三种不同肤色的人。女娲可以使用场景类Client来表示,八卦炉类似于一个工厂,负责制造生产产品(即人类),三种不同肤色的人,他们都是同一个接口下的不同实现类,都是人嘛,只是肤色、语言不同,对于八卦炉来说都是它生产出的产品。

image-20210610103357793

(图片取自《设计模式之禅》)

类图比较简单,AbstractHumanFactory是一个抽象类,定义了一个八卦炉具有的整体功能,HumanFactory为实现类,完成具体的任务——创建人类;Human接口是人类的总称,其三个实现类分别为三类人种;NvWa类是一个场景类,负责模拟这个场景,执行相关的任务。

我们定义的每个人种都有两个方法:getColor(获得人的皮肤颜色)和talk(交谈)。

代码示例

用代码实现上面的逻辑

目录结构

image-20210610105705758

Human

Human类是对人类的总称,定义了两个方法。

Human.java
public interface Human {

//每个人种的皮肤都有相应的颜色
void getColor();

//人类会说话
void talk();

}

具体的人种实现类

BlackHuman.java
public class BlackHuman implements Human{


@Override
public void getColor() {
System.out.println("黑色");
}

@Override
public void talk() {
System.out.println("黑色的专业团队");
}
}
WhiteHuman.java
public class WhiteHuman implements Human{


@Override
public void getColor() {
System.out.println("白色");
}

@Override
public void talk() {
System.out.println("白种人很白");
}
}

.YellowHuman.java
public class YellowHuman implements Human{

@Override
public void getColor() {
System.out.println("黄色");
}

@Override
public void talk() {
System.out.println("黄种人好看");
}
}

所有的人种定义完毕,下一步就是定义一个八卦炉,然后烧制人类。我们想象一下,女娲最可能给八卦炉下达什么样的生产命令呢?应该是“给我生产出一个黄色人种(YellowHuman类)”,而不会是“给我生产一个会走、会跑、会说话、皮肤是黄色的人种”,因为这样的命令增加了交流的成本,作为一个生产的管理者,只要知道生产什么就可以了,而不需要事物的具体信息。通过分析,我们发现八卦炉生产人类的方法输入参数类型应该是Human接口的实现类,这也解释了为什么类图上的AbstractHumanFactory抽象类中createHuman方法的参数为Class类型。

AbstractHumanFactory.java
public abstract class AbstractHumanFactory {

public abstract <T extends Human> T createHuman(Class<T> c);

}

这里采用了泛型(Generic),通过定义泛型对createHuman的输入参数产生两层限制:

● 必须是Class类型;

● 必须是Human的实现类

人类创建工厂

HumanFactory.java
public class HumanFactory extends AbstractHumanFactory {

@Override
public <T extends Human> T createHuman(Class<T> c) {
//定义一个生产的人种
Human human = null;
try {
//产生一个人种
human = (T) Class.forName(c.getName()).newInstance();
} catch (Exception e) {
System.out.println("人种生成错误!");
}
return (T) human;

}
}

女娲类

NvWa.java
public class NvWa {

public static void main(String[] args) {
AbstractHumanFactory humanFactory = new HumanFactory();
System.out.println("----造白人START----");
Human whiteHuman = humanFactory.createHuman(WhiteHuman.class);
whiteHuman.getColor();
whiteHuman.talk();
System.out.println("----造白人END----");

System.out.println("----造黄种人START----");
Human yellowHuman = humanFactory.createHuman(YellowHuman.class);
yellowHuman.getColor();
yellowHuman.talk();
System.out.println("----造黄种人END----");

System.out.println("----造黑人START----");
Human blackHuman = humanFactory.createHuman(BlackHuman.class);
blackHuman.getColor();
blackHuman.talk();
System.out.println("----造黑人END----");
}

}

运行结果:

----造白人START----
白色
白种人很白
----造白人END----
----造黄种人START----
黄色
黄种人好看
----造黄种人END----
----造黑人START----
黑色
黑色的专业团队
----造黑人END----

Process finished with exit code 0

人类的生产过程就展现出来了!这个世界就热闹起来了,黑人、白人、黄人都开始活动了,这也正是我们现在的真实世界。以上就是工厂方法模式。

工厂方法模式的定义

“Define an interface for creating an object,but let subclasses decide which class to instantiate.Factory Method lets a class defer instantiation to subclasses”

“定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。”

通用类图:

image-20210610112626434

(图片取自《设计模式之禅》)

在工厂方法模式中,抽象产品类Product负责定义产品的共性,实现对事物最抽象的定义;Creator为抽象创建类,也就是抽象工厂,具体如何创建产品类是由具体的实现工厂ConcreteCreator完成的。

代码清单

抽象产品类:

Product.java
public abstract class Product {

//产品类的公共方法
public void method1(){
//业务逻辑处理
}
//抽象方法
public abstract void method2();
}

具体产品类:

ConcreteProduct1.java
public class ConcreteProduct1 extends Product {

@Override
public void method2() {
// 业务处理逻辑
}
}
ConcreteProduct2.java
public class ConcreteProduct2 extends Product {

@Override
public void method2() {
// 业务处理逻辑
}
}

抽象工厂类:

Creator.java
public abstract class Creator {

/*
* 创建一个产品对象,其输入参数类型可以自行设置
* 通常为String、Enum、Class等,当然也可以为空
*/
public abstract <T extends Product> T createProduct(Class<T> c);


}

具体工厂类:

ConcreteCreator.java
public class ConcreteCreator extends Creator {

@Override
public <T extends Product> T createProduct(Class<T> c) {
Product product = null;
try {
product = (Product) Class.forName(c.getName()).newInstance();
} catch (Exception e) {
//异常处理
}
return (T) product;

}
}

场景类:

Client.java
public class Client {

public static void main(String[] args) {
Creator creator = new ConcreteCreator();
Product product = creator.createProduct(ConcreteProduct1.class);
/*
* 继续业务处理
*/
}

}

该通用代码是一个比较实用、易扩展的框架。

工厂方法模式应用

工厂方法模式的优点

  • 良好的封装性,代码结构清晰

    一个对象创建是有条件约束的,如一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的艰辛过程,降低模块间的耦合。

  • 工厂方法模式的扩展性非常优秀

    在增加产品类的情况下,只要适当地修改具体的工厂类或扩展一个工厂类,就可以完成“拥抱变化

  • 屏蔽产品类

    产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口,只要接口保持不变,系统中的上层模块就不要发生变化。因为产品类的实例化工作是由工厂类负责的,一个产品对象具体由哪一个产品生成是由工厂类决定的。

    在数据库开发中,大家应该能够深刻体会到工厂方法模式的好处:如果使用JDBC连接数据库,数据库从MySQL切换到Oracle,需要改动的地方就是切换一下驱动名称(前提条件是SQL语句是标准语句),其他的都不需要修改,这是工厂方法模式灵活的一个直接案例。

  • 工厂方法模式是典型的解耦框架

    高层模块值需要知道产品的抽象类,其他的实现类都不用关心,符合迪米特法则,我不需要的就不要去交流;也符合依赖倒置原则,只依赖产品类的抽象;当然也符合里氏替换原则,使用产品子类替换产品父类。

使用场景

  • 工厂方法模式是new一个对象的替代品,所以在所有需要生成对象的地方都可以使用,但是需要慎重地考虑是否要增加一个工厂类进行管理,增加代码的复杂度。
  • 需要灵活的、可扩展的框架时,可以考虑采用工厂方法模式。万物皆对象,那万物也就皆产品类。
  • 工厂方法模式可以用在异构项目中
  • 可以使用在测试驱动开发的框架下

工厂方法模式扩展

简单工厂模式

我们这样考虑一个问题:一个模块仅需要一个工厂类,没有必要把它产生出来,使用静态的方法就可以了。

相应的类图:

image-20210611174856641

(图片取自《设计模式之禅》)

代码如下:

HumanFactory2.java
public class HumanFactory2 {

public static <T extends Human> T createHuman(Class<T> c) {
//定义一个生产的人种
Human human = null;
try {
//产生一个人种
human = (T) Class.forName(c.getName()).newInstance();
} catch (Exception e) {
System.out.println("人种生成错误!");
}
return (T) human;

}
}

NvWa类,则移除了实例化工厂类的步骤。

运行结果没有发生变化,但是我们的类图变简单了,而且调用者也比较简单,该模式是工厂方法模式的弱化,因为简单,所以称为简单工厂模式(Simple Factory Pattern),也叫做静态工厂模式。在实际项目中,采用该方法的案例还是比较多的,其缺点是工厂类的扩展比较困难,不符合开闭原则,但它仍然是一个非常实用的设计模式。

升级为多个工厂类

项目比较复杂的时候,初始化对象经常需要耗费较大精力,所有产品类都需要放到一个工厂方法中进行初始化。这样会导致代码结构不清晰。

如:

一个产品类有5个具体实现,每个实现类的初始化方法都不相同(不仅仅是new,初始化包括new一个对象,并对对象设置一定的初始值),如果写到一个工厂方法中,会导致该方法变得很长。

若要使代码结构清晰,需要为每个产品定一个创造者,然后由调用者自己去选择与哪个工厂方法关联。

以女娲造人为例,类图如下:

image-20210612210134353

(图片取自《设计模式之禅》)

每个人种都对应了一个创建者,每个创建者都独立负责创建对应的产品对象,符合单一职责原则。

示例代码

AbstractHumanFactory3.java
public abstract class AbstractHumanFactory3 {

public abstract <T extends Human> T createHuman();

}

在这里抽象方法中不用再传递参数,没一个具体的工厂实现类都有具体职责:也就是创建自己负责的产品对象。

黑人的创建工厂实现类

public class BlackHumanFactory extends AbstractHumanFactory {
public Human createHuman() {
return new BlackHuman();
}
}

其他类似,不再贴代码啦。

运行结果还是相同。每一个产品类都对应了一个创建类,好处就是创建类的职责清晰,而且结构简单,但是给可扩展性和可维护性带来了一定的影响。如果要扩展一个产品类,就需要建立一个相应的工厂类,这样就增加了扩展的难度。因为工厂类和产品类的数量相同,维护时需要考虑两个对象之间的关系。

在复杂的应用中一般采用多工厂的方法,然后再增加一个协调类,避免调用者与各个子工厂交流,协调类的作用是封装子工厂类,对高层模块提供统一的访问接口。

替代单例模式

使用工厂方法模式来代替单例模式的功能。

image-20210613111342568

(图片取自《设计模式之禅》)

示例代码

Singleton.java
public class Singleton {

//不允许通过new产生一个对象
private Singleton(){
}
public void doSomething(){
//业务处理
}

}

Singleton保证不能通过正常的渠道建立一个对象,SingletonFactory利用反射的方式创建单例对象。

SingletonFactory.java
public class SingletonFactory {

private static Singleton singleton;

static {
try {
Class cl = Class.forName(Singleton.class.getName());
//获得无参构造
Constructor constructor = cl.getDeclaredConstructor();
//设置无参构造是可访问的
constructor.setAccessible(true);
//产生一个实例对象
singleton = (Singleton) constructor.newInstance();
} catch (Exception e) {
//异常处理
}
}

public static Singleton getSingleton() {
return singleton;
}

}

通过获得类构造器,然后设置访问权限,生成一个对象,然后提供外部访问,保证内存中的对象唯一。当然,其他类也可以通过反射的方式建立一个单例对象,但是一个项目或团队是有章程和规范的,何况已经提供了一个获得单例对象的方法,为什么还要重新创建一个新对象呢?

以上通过工厂方法模式创建了一个单例对象,该框架可以继续扩展,在一个项目中可以产生一个单例构造器,所有需要产生单例的类都遵循一定的规则(构造方法是private),然后通过扩展该框架,只要输入一个类型就可以获得唯一的一个实例。