你应该了解的工厂方法模式:优雅的代码永不过时(一)

简介: 你应该了解的工厂方法模式:优雅的代码永不过时

前言


在之前单例模式的学习中,愈发的感觉到设计模式的精妙,设计模式就像是一种思想,能够很好的嵌入到自己的代码当中,写出更优雅更高效对的代码,本文讲述学习Java设计模式过程中的第二个设计模式——工厂方法模式

一、举例说明工厂方法模式


东汉《风俗通》中记录了一则神话故事:“开天辟地,未有人民,女娲搏黄土做人”。讲述的是女娲造人的故事。女娲造人的过程是这样的:首先,女娲采集黄土捏成人的形状,然后方法八卦炉中烧制,最后放置到大地上生长,但是在制作的过程中意外随时都有可能发生:

  1. 第一次烤制泥人,感觉应该烤熟了,结果放到大地上时发现没有烤熟,于是一个白人就诞生了!
  2. 第二次烤制泥人,因为第一次没有烤熟,于是这一次多烤一会,结果放到大地上发现熟过头了,于是乎黑人就诞生了!
  3. 第三次烤制泥人,一边烧制一边查看,直到泥人表皮微黄,于是放到大地上,这一次烤制刚刚好,于是黄种人就出现了!

如何通过Java程序来实现女娲造人的过程呢?

在面向对象的思维中,万物皆对象,是对象我们就可以通过软件的设计来实现

1、程序分析


  1. 首先对造人的过程进行分析,该过程涉及到三个对象:女娲、八卦炉、三种不同肤色的人。
  2. 女娲可以使用场景类Client来表示。
  3. 八卦炉类似一个工厂,负责生产产品(即负责生产人类)。
  4. 对于三种不同肤色的人,他们都是同一接口下的不同实现类(对于八卦炉来说三种不同肤色的人都是它生产出来的产品)。

2、程序类图


image.png

类图解析:

  1. AbstractHumanFactory类是一个抽象类,定义了一个八卦炉具有的整体功能。
  2. HumanFactory类为实现类,用于完成具体的任务——创建人类
  3. Human接口是人类的总称,其三个实现类分别定义三类不同肤色的人种
  4. NvWa类是一个场景类,负责模拟这个场景,执行相关的任务

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

3、程序代码


Human接口

public interface Human {
    // 每个人种的皮肤都有相应的颜色
    public void getColor();
    // 只要是人类就都是会说话的
    public void talk();
}

该类对应上面类图中的Human接口,接口中定义了每个人种都具备的两个方法:获得人的肤色(getColor方法)和交谈(talk方法)

BlackHuman类(黑色人种)

public class BlackHuman implements Human {
    @Override
    public void getColor() {
        System.out.println("黑色人种的肤色当然是黑色的");
    }
    @Override
    public void talk() {
        System.out.println("黑种人:我们为黑人牙膏代言");
    }
}

YellowHuman类(黄色人种)

publicclassYellowHumanimplementsHuman {
@OverridepublicvoidgetColor() {
System.out.println("黄种人的肤色是黄色的");
    }
@Overridepublicvoidtalk() {
System.out.println("黄种人:古老的东方有一条龙");
    }
}

WhiteHuman类(白色人种)

publicclassWhiteHumanimplementsHuman {
@OverridepublicvoidgetColor() {
System.out.println("白种人的肤色肯定是白色的");
    }
@Overridepublicvoidtalk() {
System.out.println("白种人:有汰渍,没污渍!汰渍,专业漂白100年");
    }
}

到这里所有的人种已经定义完毕,有了所有人种的样本,下一步就是定义一个用于烧制人类的八卦炉。在这里可以畅想一下女娲给八卦炉会下什么样的命令呢 ?无非就以下两种命令:

  1. 给我生产出一个黄色人种
  2. 给我生产出一个会跑、会跳、会说话的黄色人种

第二条命令增加了很多交流成本作为一个生产的管理者,只需要知道我们生产的是什么事物就可以了,而不需要事物的具体信息。因此,由于只需要知道生产出的是什么,不需要知道生产出的产品的具体信息,八卦炉生产人类的方法输入参数类型应该是Human接口的实现类。(这也是类图中AbstractHumanFactory抽象类中createHuman方法中的参数类型为Class类型的原因

AbstractHumanFactory类(抽象人类创建工厂)

publicabstractclassAbstractHumanFactory {
publicabstract<TextendsHuman>TcreateHuman(Class<T>c);
}

在这里我们通过定义泛型来对createHuman方法的输入参数产生两层限制

  1. 输入的参数必须是Class类型
  2. 输入的参数必须是Human的实现类

在该类中“T”表示的是只要实现了Human接口的类都可以作为参数

HumanFactory类(人类创建工厂)

publicclassHumanFactoryextendsAbstractHumanFactory {
@Overridepublic<TextendsHuman>TcreateHuman(Class<T>c) {
// 定义一个要生产的人种Humanhuman=null;
// 产生一个人种try {
human= (T)Class.forName(c.getName()).newInstance();
        } catch (Exceptione) {
System.out.println("人种生产出错了");
        }
return (T)human;
    }
}

到目前为止,人种信息、八卦炉都有了,就等女娲采集黄土然后命令八卦炉开始烧制各种肤色的人类了

NvWa类(女娲)

publicclassNvWa {
publicstaticvoidmain(String[] args) {
// 先声明一个八卦阴阳炉AbstractHumanFactoryfactory=newHumanFactory();
// 女娲开始造人,但是第一次火候不足System.out.println("<---- 造出的第一批人是白种人 ---->");
HumanwhiteHuman=factory.createHuman(WhiteHuman.class);
System.out.println("-- 获取白色人种的肤色 --");
whiteHuman.getColor();
System.out.println("-- 让我们听听白色人种想说什么 --");
whiteHuman.talk();
// 女娲第二次开始造人,烧制时间过长出现了黑人System.out.println("<---- 造出的第二批人是黑种人 ---->");
HumanblackHuman=factory.createHuman(BlackHuman.class);
System.out.println("-- 获取黑色人种的肤色 --");
blackHuman.getColor();
System.out.println("-- 让我们听听黑色人种想说什么 --");
blackHuman.talk();
// 女娲第三次造人吸取了前两次的教训,这次烧制成功了System.out.println("<---- 造出的第三批人是黄种人 ---->");
HumanyellowHuman=factory.createHuman(YellowHuman.class);
System.out.println("-- 获取黄色人种的肤色 --");
yellowHuman.getColor();
System.out.println("-- 让我们听听黄色人种想说什么 --");
yellowHuman.talk();
    }
}

4、运行结果


image.png

到此女娲造人的过程就已经被我们用Java程序实现了,以上就是工厂方法模式

二、工厂方法模式的定义


工厂方法模式:

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

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

工厂方法模式属于创造类型设计模式

1、工厂方法模式的通用类图


image.png

类图解析:

  1. Product抽象类:该类为抽象产品类,抽象产品类负责定义产品的共性,实现对事物最抽象的定义
  2. ConcreteProduct类:该类为具体产品类,具体产品类可以有多个,都继承于抽象产品类,该类中定义了该类型产品的具体内容
  3. Creator抽象类:该类为抽象创建类,也就是抽象工厂。在抽象工厂中负责定义产品对象的产生
  4. ConcreteCreator类:该类为具体的实现工厂,实现工厂类实现的是具体如何生产出一个产品对象

2、工厂方法模式通用代码


抽象产品类

publicabstractclassProduct {
// 产品类的公共方法publicvoidmethod1(){
// 具体的业务处理    }
// 抽象方法publicabstractvoidmethod2();
}

负责定义产品的共性,实现对事物最抽象的定义

具体产品类

publicclassConcreteProduct1extendsProduct {
@Overridepublicvoidmethod2() {
// 具体的业务处理    }
}
publicclassConcreteProduct2extendsProduct {
@Overridepublicvoidmethod2() {
// 具体业务处理    }
}

具体的产品类可以有多个,都继承于抽象产品类

抽象工厂类

publicabstractclassCreator {
/*** 创建一个产品对象,其输入参数的类型可以自行设置* 通常为String、Enum、Class等,也可以为空*/publicabstract<TextendsProduct>TcreateProduct(Class<T>c);
}

抽象工厂类负责定义产品对象的产生

具体工厂类

publicclassConcreteCreatorextendsCreator {
@Overridepublic<TextendsProduct>TcreateProduct(Class<T>c) {
// 声明生产产品的成员变量Productproduct=null;
try {
// 通过反射获取到指定类的类名并创建此Class对象所表示的类的一个新实例product= (Product)Class.forName(c.getName()).newInstance();
        } catch (Exceptione) {
// 异常处理的方法,也可以是其它的业务语句e.printStackTrace();
        }
return (T)product;
    }
}

具体如何生产出一个产品的对象,是由具体的工厂类来实现的

场景类

publicclassClient {
publicstaticvoidmain(String[] args) {
// 声明具体进行生产的工厂Creatorcreator=newConcreteCreator();
// 声明由工厂生产的具体产品1的对象Productproduct1=creator.createProduct(ConcreteProduct1.class);
// 声明由工厂生产的具体产品2的对象Productproduct2=creator.createProduct(ConcreteProduct2.class);
/*** 继续进行接下来的业务处理*/    }
}

从抽象产品类开始到场景类结束就是一个比较实用并且易扩展的工厂方法模式的通用代码

三、工厂方法模式的应用


1、工厂方法模式的优点


  1. 工厂方法模式具有良好的封装性,代码结构清晰。一个对象创建是有条件约束的,例如一个调用者需要一个具体的产品对象,只要知道这个产品的类名(或约束字符串)就可以了,不用知道创建对象的具体过程是什么极大的降低了模块间的耦合性
  2. 工厂方法模式具有良好的扩展性在增加产品类的情况下,只要适当地修改具体的工厂类或者扩展一个工厂类,就可以很好的完成新的变化。例如在上面的例子中要加入一个绿色人种,只需要增加一个GreenHuman类,工厂类不用任何修改就可以完成系统的扩展。
  3. 工厂方法模式可以屏蔽产品类(这一特点非常重要),产品类的实现如何变化,调用者都不需要关心,它只需要关心产品的接口只要接口保持不变,系统中的上层模块就不要发生变化。因为产品类的实例化工作时由工厂类负责的,一个产品对象具体由哪一个产品生成是由工厂类决定的
  4. 工厂方法模式是典型的解耦框架高层模块只需要知道生产产品的抽象类,其它的实现类都不用关心,不需要的类也不用去交流,只依赖产品类的抽象

2、工厂方法模式的使用场景


  1. 工厂方法模式是new一个对象的替代品,所以在所有需要生成对象的地方都可以使用。但是要考虑是否要增加一个工厂类进行管理,因为这样会增加代码的复杂度
  2. 需要灵活的、可扩展的框架时,可以考虑采用工厂方法模式。万物皆对象,同样也可以说万物皆产品类。例如需要设计一个连接邮件服务器的框架,有三种协议可供选择:POP3、IMAP、HTTP,这时我们就可以把这三种连接方式作为产品类,定义一个接口IConnectMail,然后定义对邮件的操作方法,用不同的方法实现三个具体的产品类(也就是三种连接方式),再定义一个工厂方法,按照不同的传入条件,选择不同的连接方式。这样设计就可以做到很好的扩展。如果某些邮件服务器提供了WebService接口时我们只需要增加一个产品类就可以了。
  3. 工厂方法模式可以用在异构的项目中,例如通过WebService与一个非Java的项目交互(虽然WebService号称可以做到异构系统的同构化,但是在实际开发过程中还是会遇到很多问题),遇到类型问题、WSDL文件的支持等问题,从WSDL中产生的对象都认为是一个产品,然后这时由一个具体的工厂类进行管理,可以减少与外围系统的耦合。
  4. 工厂方法模式可以使用在测试驱动开发的框架下。例如测试一个A类,就需要把与A类有关联关系的B类也同时生产出来,这时就可以使用工厂方法模式把B类虚拟出来,避免A类与B类的耦合。(这个使用场景目前已经弱化了,遇到这种情况时可以考虑使用JMock或EasyMock)

四、工厂方法模式的扩展


工厂方法模式有很多扩展,在本文将介绍4种扩展

1、缩小为简单工厂模式


在开发中我们有时会考虑这样一个问题:一个模块仅需要一个工厂类,因此没有必要把抽象工厂生产出来,只需要使用静态方法就可以了。基于这种要求,还是以女娲造人为例,需要将例子中的AbstractHumanFactory修改一下即可

修改后的类图为

image.png

在修改后的类图中,具体的八卦炉中创建人类的方法是静态的

两次类图的对比

image.png

程序代码:

代码没有发生改变的程序

// 简单工厂模式中的Human接口publicinterfaceHuman {
// 每个人种的皮肤都有相应的颜色publicvoidgetColor();
// 只要是人类就都是会说话的publicvoidtalk();
}
// 简单工厂模式中的黄种人类publicclassYellowHumanimplementsHuman {
@OverridepublicvoidgetColor() {
System.out.println("黄种人的肤色是黄色的");
    }
@Overridepublicvoidtalk() {
System.out.println("黄种人:古老的东方有一条龙");
    }
}
// 简单工厂模式中的黑种人类publicclassBlackHumanimplementsHuman {
@OverridepublicvoidgetColor() {
System.out.println("黑色人种的肤色当然是黑色的");
    }
@Overridepublicvoidtalk() {
System.out.println("黑种人:我们为黑人牙膏代言");
    }
}
// 简单工厂模式中的白种人类publicclassWhiteHumanimplementsHuman {
@OverridepublicvoidgetColor() {
System.out.println("白种人的肤色肯定是白色的");
    }
@Overridepublicvoidtalk() {
System.out.println("白种人:有汰渍,没污渍!汰渍,专业漂白100年");
    }
}

代码发生改变的程序

// 简单工厂模式中的工厂类publicclassHumanFactory {
publicstatic<TextendsHuman>TcreateHuman(Class<T>c) {
// 定义一个生产出的人种Humanhuman=null;
try {
//产生一个人种human= (Human) Class.forName(c.getName()).newInstance();
        } catch (Exceptione) {
e.printStackTrace();
        }
return (T) human;
    }
}

HumanFactory类仅有两个地方发生了变化:去掉继承抽象类,并在createHuman前增加static关键字;工厂类发生了变化的同时也引起了调用者NvWa的变化

// 简单工厂模式中的NvWa类publicclassNvWa {
publicstaticvoidmain(String[] args) {
// 女娲第一次造人,火候不足,于是白色人种产生了System.out.println("<---- 造出的第一批人是白种人 ---->");
// 发生变化的代码HumanwhiteHuman=HumanFactory.createHuman(WhiteHuman.class);
System.out.println("-- 获取白色人种的肤色 --");
whiteHuman.getColor();
System.out.println("-- 让我们听听白色人种想说什么 --");
whiteHuman.talk();
// 女娲第二次开始造人,烧制时间过长出现了黑人System.out.println("<---- 造出的第二批人是黑种人 ---->");
// 发生变化的代码HumanblackHuman=HumanFactory.createHuman(BlackHuman.class);
System.out.println("-- 获取黑色人种的肤色 --");
blackHuman.getColor();
System.out.println("-- 让我们听听黑色人种想说什么 --");
blackHuman.talk();
// 女娲第三次造人吸取了前两次的教训,这次烧制成功了System.out.println("<---- 造出的第三批人是黄种人 ---->");
// 发色变化的代码HumanyellowHuman=HumanFactory.createHuman(YellowHuman.class);
System.out.println("-- 获取黄色人种的肤色 --");
yellowHuman.getColor();
System.out.println("-- 让我们听听黄色人种想说什么 --");
yellowHuman.talk();
    }
}

运行结果

image.png

可以看出运行结果没有发生变化,但是我们的类图变得更简单了,而且调用者也比较简单。这个就是工厂方法模式的弱化,因为相较于工厂方法模式来说比较简单,所以被称为简单工厂模式(也叫做静态工厂模式),在实际项目中采用的还是比较多的,但是缺点是工厂类的扩展比较困难,不符合开闭原则

相关文章
|
4月前
|
设计模式
学会了这个设计模式,再也不是只会写if/else了
本文详细介绍了责任链设计模式(Chain of Responsibility Pattern),这是一种行为型设计模式,用于创建一个接收者对象的链,通过解耦请求的发送者和接收者,允许沿着链传递请求,直到某个接收者能够处理它。
学会了这个设计模式,再也不是只会写if/else了
|
6月前
|
Java
揭秘Java多态:为何同一消息,对象们却各有“心思”?
【6月更文挑战第17天】Java中的多态性让不同对象对同一方法有独特响应。以动物园为例,抽象类`Animal`定义`makeSound()`,子类如`Tiger`, `Lion`, `Monkey`继承并重写该方法。通过`Animal`引用调用,实际执行子类实现,展示动态绑定的威力。多态提升代码灵活性,支持扩展而无需改动原有代码,体现面向对象的核心思想。
31 2
|
7月前
|
设计模式
二十三种设计模式全面解析-当你的对象需要知道其他对象的状态变化时,观察者模式是你的救星!
二十三种设计模式全面解析-当你的对象需要知道其他对象的状态变化时,观察者模式是你的救星!
|
7月前
|
设计模式 存储
二十三种设计模式全面解析-深入探究备忘录模式:保留过去,预见未来
二十三种设计模式全面解析-深入探究备忘录模式:保留过去,预见未来
|
设计模式 网络协议 Java
经典永不过时!重温设计模式
在软工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。这个术语是由埃里希·伽玛(Erich Gamma)等人在1990年代从建筑设计领域引入到计算机科学的,设计模式是针对软件设计中常见问题的工具箱,其中的工具就是各种经过实践验证的解决方案。即使你从未遇到过这些问题,了解模式仍然非常件有用,因为它能指导你如何使用面向对象的设计原则来解决各种问题。
201 1
经典永不过时!重温设计模式
|
安全 Java
创建对象的相关知识补充
创建对象的相关知识补充
67 0
|
存储 算法 安全
万字 HashMap 详解,基础(优雅)永不过时
在上一篇文章里,我们聊到了散列表的整体设计思想,在后续几篇文章里,我们将以 Java 语言为例,分析标准库中实现的散列表实现,包括 HashMap、ThreadLocalMap、LinkedHashMap 和 ConcurrentHashMap。今天,我们来讨论 Java 标准库中非常典型的散列表结构,也是 “面试八股
153 0
万字 HashMap 详解,基础(优雅)永不过时
|
设计模式 缓存 Java
你应该了解的工厂方法模式:优雅的代码永不过时(二)
你应该了解的工厂方法模式:优雅的代码永不过时
你应该了解的工厂方法模式:优雅的代码永不过时(二)
|
JavaScript 前端开发
如何确保你的构造函数只能被new调用,而不能被普通调用?| 踩坑日记
如何确保你的构造函数只能被new调用,而不能被普通调用?| 踩坑日记
638 0
如何确保你的构造函数只能被new调用,而不能被普通调用?| 踩坑日记
|
设计模式 监控 Java
彻底理解静态代理模式——时间都去哪儿了
陀螺和招财的程序喵故事,带你彻底理解静态代理模式
彻底理解静态代理模式——时间都去哪儿了