在众多的设计模式中,工厂模式应该算得上最常用,也是最经典的一种模式了吧。通过接口的提取实现对功能具体实现的隐藏和对扩展性的提升。最近在读《研磨设计模式》一书时决定对这个经典的模式进行一下总结和对比,加深印象,巩固一下学习成果。
工厂模式并不是一个单独的模式,而是分成了三种不同的模式,但是这三种模式又都有工厂的思想,只是因为目的不同而形式不同。这三种模式分别是:简单工厂、工厂方法和抽象工厂模式。
1. 简单工厂
所谓的工厂,就是可以成批的生产同一种的类型的产品。当然,在软件设计当中当然也希望能够使用一个工具就可以生产出我们想要的实例,而不需要我们知道这个实例的具体信息和如何通过逻辑操作获得这个实例。因此,就需要设计一种模式来满足这样的需求,而恰巧,java中接口就相当于是产品的类型信息,实现就是具体信息,我们只需要利用接口来定义好工厂即可。
下面以打印不同词语的例子进行说明:
接口:
public interface printInterface {
public void print();
}
实现:
public class printA implements printInterface {
@Override
public void print() {
System.out.println("A");
}
}
public class printB implements printInterface {
@Override
public void print() {
System.out.println("B");
}
}
工厂方法:
public class printFactory {
public static printInterface getPrint(int type)
{
if(type < 0)
{
System.out.println("error");
return null;
}
if(type == 1)
{
return new printA();
}
else if(type == 2)
{
return new printB();
}
else {
System.out.println("null");
return null;
}
}
}
测试:
public class NormalSimpleFactory {
@Test
public void test() {
printInterface testPrintInterface = printFactory.getPrint(2);
testPrintInterface.print();
}
}
从上面的代码可以看出,这个模式是一种基于选择的方式,也就是客户进行选择来进行实例的获取。
从这个类图可以看出,Client只需要知道自己想要的实例的接口和工厂,然后通过工厂去获取就可以了。这样就做到了对下城实现的隐藏,所有同一类型的产品都通过一个方法去获取。这样做就封装了所有对接口的实现,但是上面的做法还是有一个问题,当我们需要实现一个新的产品时,姑且定义为printC,这时就需要通过修改工厂类或者继承工厂类,然后重载其中静态方法,这样是可以实现的,但是需要进行代码的修改,也不是很方便。
我们希望做到可配置的方法,也就是通过配置文件来进行读取,这里就需要用到java的反射机制了。
public class printFactory {
public static printInterface getprint()
{
Properties p = new Properties();
InputStream in = null;
try {
//将文件转换成流文件对象,这里输入的路径是一个相对路径,相对于printFactory这个类的
in = printFactory.class.getResourceAsStream("FactoryTest.properties");
p.load(in);
} catch (IOException e_IO) {
System.out.println("装载工厂配置文件出错了,具体的堆栈信息如下:");
e_IO.printStackTrace();
}finally{
try {
in.close();
} catch (IOException e_IO) {
e_IO.printStackTrace();
}
}
printInterface printer = null;
try {
//用反射机制获取实例
printer = (printInterface)Class.forName(p.getProperty("ImplClass")).newInstance();
} catch (ClassNotFoundException e_CNF) {
e_CNF.printStackTrace();
}catch (IllegalAccessException e_IAE) {
e_IAE.printStackTrace();
}catch (InstantiationException e_IE) {
e_IE.printStackTrace();
}
return printer;
}
}
上述代码通过对properties文件解析获取类的名称,然后通过反射机制进行实例化,然后进行返回。这样做就增加了工厂方法的可配置性。
无论是用type进行实例化的控制还是用配置文件进行实例化的控制,都透露出一个信息,需要调用者来选择,选择通过共同的工厂来实现哪个实例。所以,简单工厂的本质就是:选择实现。
简单工厂的好处在于对实现进行封装,封装了关于实现的所有细节,也实现了耦合度的降低,因为不在需要我们自己去new一个一个的实例了。
2. 工厂方法
上述的简单工厂确实实现了对具体实现的封装,但是在业务需要拓展,需要增加新产品时,简单工厂给出的方案多少有些复杂(要么继承工厂类,要么通过配置文件进行选择)。那么我们能不能找到一个能够轻松实现新产品增加的方案呢?
通过分析简单工厂可以发现,之所以简单工厂的扩展性比较差,主要是因为工厂只有一个,所有产品都要通过这个方法来选择和产生,而且每次只能 产生一种产品。那么,我们就应该扩展工厂,将工厂扩展到产品,每个产品一个工厂,这样在扩展时就比较方便了,这样也就衍生出了工厂的接口,通过接口来控制工厂的功能。下面展示的是基于接口的工厂方法的实现:
动物接口:
public interface Animal {
public void Run();
public void Eat();
}
Cat类和Dog类实现了Animal接口:
public class Cat implements Animal {
@Override
public void Run() {
System.out.println("Cat Run....");
}
@Override
public void Eat() {
System.out.println("Cat Eat....");
}
}
public class Dog implements Animal {
@Override
public void Run() {
System.out.println("Dog Run....");
}
@Override
public void Eat() {
System.out.println("Dog Eat....");
}
}
工厂类:
public interface AnimalFactory {
public Animal createAnimal();
}
public class CatFactory implements AnimalFactory {
@Override
public Animal createAnimal() {
return new Cat();
}
}
public class DogFactory implements AnimalFactory {
@Override
public Animal createAnimal() {
return new Dog();
}
}
从上面的类图和代码可以看出基于接口的工厂方法与简单工厂方法的主要区别在于多了一个工厂的接口,所有的工厂被统一了规格。这样做的好处就在工厂可以自由扩展,每种实现对应一个工厂,方便管理和扩展。除了基于接口的实现,我们还可以进行基于抽象类的实现:
public abstract class AnimalFactory {
protected abstract Animal getAnimal();
}
在不计较逻辑关系(但实际上在考虑对象化时,逻辑关系是需要考虑的,因为好的逻辑能够帮助提高项目的可读性和可维护性),也可以这样写:
public abstract class AnimalFactory implements Animal {
@Override
public void Run() {
getAnimal().Run();
}
@Override
public void Eat() {
getAnimal().Eat();
}
protected abstract Animal getAnimal();
}
这样写的好处在于可以减少了实现的工作量,将共同的方法全都放在了一起,通过工厂便可以直接操作对象,类似于简单工厂的思想。看上去上面的抽象类的用法在功能上与用接口没有太多的差别,只是编写形式和逻辑抽象关系的不同。那让我们来看下面的代码:
public class AnimalFactoryByArgs implements Animal {
@Override
public void Run(int type) {
getAnimal(type).Run();
}
@Override
public void Eat(int type) {
getAnimal(type).Eat();
}
protected Animal getAnimal(int type)
{
if(type == 1)
{
return new Dog();
}
else if(type == 2)
{
return new Cat();
}
else {
return null;
}
}
}
从上面的代码可以看出,我们可以通过用一个普通的类作为工厂类,然后通过传入的参数来控制实际的对象。这样的方式的好处在于便于扩展:比如现在我们需要在与拿来的基础上扩展一个Pig工厂:
public class ExtendAnimalFactoryByArgs extends AnimalFactoryByArgs {
private int type;
public ExtendAnimalFactoryByArgs(int type) {
super(type);
this.type = type;
}
@Override
protected Animal getAnimal()
{
if(type == 3)
{
return new Pig();
}
else {
return super.getAnimal();
}
}
}
我们不用修改原有工厂,只是继承原来的工厂,然后做方法的重载就行。
无论是基于接口的工厂方法,还是基于抽象类的工厂方法,或者是基于普通类的工厂方法,他们的核心思想都是一样的:将对工厂的实现与选择延迟到子类进行。
那有人要问了,基于普通类的工厂方法不是在工厂方法就进行了实现了吗?是的,确实是进行了实现,但是那个方法相当于一个基础方法,也就是默认实现,主要的功能还是通过继承然后进行扩展完成真正工厂的功能。算是对简单工厂方法的一种升级,但是我更倾向于将他分类在工厂方法的行列,因为主要的实现还是在子类中进行。
3.抽象工厂
本质上,抽象方法跟上述两种方法应该不算是一种类型的,虽然都是工厂,但是生产东西的类型完全不同。上述的工厂生产的单个产品,而抽象工厂生产的则是产品簇。也就是说,抽象工厂方法生产的产品是由多个产品组成的一个组合产品。
抽象工厂实现起来并没有什么难度,也不具有太多的点,所以这里借鉴一张网上的图以示说明:
总结:
简单工厂方法:实现简单,工厂可以直接操作产品的方法,但是扩展性较差,不易增加多个产品。
工厂方法:可以实现产品的增加,工厂只能生产产品不能操作产品,具有很强的扩展性。
抽象工厂:可以实现多个产品的组装然后产生产品族,可以随意扩展产品族工厂,但是对于产品族内增加产品是无能为力的。
(文章将持续修正)