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

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

前言


在之前单例模式的学习中,愈发的感觉到设计模式的精妙,设计模式就像是一种思想,能够很好的嵌入到自己的代码当中,写出更优雅更高效对的代码,本文讲述学习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

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

相关文章
|
人工智能 自然语言处理 Linux
AI谱曲 | 基于RWKV的最佳开源AI作曲模型魔搭推理实践
AI谱曲 | 基于RWKV的最佳开源AI作曲模型魔搭推理实践
|
12月前
|
人工智能
歌词结构的艺术:写歌词的技巧和方法深度剖析,妙笔生词AI智能写歌词软件
歌词是音乐的灵魂伴侣,其结构蕴含独特艺术魅力。掌握歌词结构技巧是创作者成功的关键。开头需迅速吸引听众,主体部分通过叙事、抒情或对话形式展开,结尾则点睛收尾。创作时可借助《妙笔生词智能写歌词软件》,利用 AI 功能优化歌词,提供丰富模板和案例,助力灵感涌现,轻松掌握歌词结构艺术。
|
监控 Java
线程池中线程异常后:销毁还是复用?技术深度剖析
在并发编程中,线程池作为一种高效利用系统资源的工具,被广泛用于处理大量并发任务。然而,当线程池中的线程在执行任务时遇到异常,如何妥善处理这些异常线程成为了一个值得深入探讨的话题。本文将围绕“线程池中线程异常后:销毁还是复用?”这一主题,分享一些实践经验和理论思考。
338 3
|
XML Java 关系型数据库
Spring6 JdbcTemplate和事务
Spring6 JdbcTemplate和事务
|
SQL 存储 数据库
使用NineData OnlineDML:轻松处理大规模数据变更
在线DML,无锁变更数据,保障业务运行。NineData助你解决大批量数据变更难题。
210 0
|
算法 Java Go
ETCD(六)ETCD和Zookeeper
ETCD(六)ETCD和Zookeeper
309 0
|
测试技术
软件设计原则-里氏替换原则讲解以及代码示例
里氏替换原则(Liskov Substitution Principle,LSP)是面向对象设计中的一条重要原则,它由Barbara Liskov在1987年提出。 里氏替换原则的核心思想是:父类的对象可以被子类的对象替换,而程序的行为不会发生变化。也就是说,如果一个类型A是另一个类型B的子类型,那么在任何使用B的地方都可以使用A,而不会引起错误或异常。
1127 0
|
微服务 Spring Java
spring cloud心跳检测自我保护(EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE. Eureka server和client之间每隔30秒会进行一次心跳通信,告诉server,client还活着 在某一些时候注册在Eureka的服务已经挂掉了,但是服务却还留在Eureka的服务列表的情况。
8485 0
|
Java
在 Java 中如何比较日期?
在 Java 中有多种方法可以比较日期,日期在计算机内部表示为(long型)时间点——自1970年1月1日以来经过的毫秒数。在Java中,Date是一个对象,包含多个用于比较的方法,任何比较两个日期的方法本质上都会比较日期的时间。
1789 0