工厂方法模式 | Java设计模式
工厂方法模式 | Java设计模式
说明
大三刚开学,正好学校里开始上设计模式这门课了,又是用Java语言设计实现,之前JavaSE也没有学得多好,自学了前端,又不想跟着学校里的前端课程再学一遍JS、BootStrap、Jquery和Vue2基础,于是选了JavaEE,想着应该不会用JSP这种类型的东西了吧?(之前前端课程隔壁计科、大数据比我们先上,这学期可以选JavaEE和前端,现在都不知道JavaEE上什么内容)自己作为软件工程学生Java和面向对象学得一坨好像不大好,于是趁着设计模式的学习补充一下自己的Java基础。
同时要说明的是,本文仅仅为本人分享,其中举例非典型,如果有问题讨论和指正
开发环境和设计模式前言
开发环境
- JDK 1.8
- Idea(+ Maven)
- 不会涉及到 Spring ,因为我本来就是一个学前端的,只是想好好掌握 Java,虽然我目前接触的 Nest 号称小 Spring(doge)
设计模式前言
⽆论多少业务逻辑就⼀个类⼏千⾏,这样的开发也可以归纳为三步:定义属性、创建⽅法、调⽤展示,Done!只不过开发⼀时爽,重构⽕葬场。
好的代码不只为了完成现有功能,也会考虑后续扩展。在结构设计上松耦合易读易扩展,在领域实现上⾼内聚不对外暴露实现细节不被外部⼲扰。⽽这就有点像家⾥三居(MVC)室、四居(DDD)室的装修,我不会允许⼏⼗万的房⼦把⾛线⽔管裸漏在外⾯,也不会允许把⻢桶放到厨房,炉灶安装到卫⽣间。
其实有⼀部分⼈并没有仔细阅读过设计模式的相关书籍和资料,但依旧可以编写出优秀的代码。这主要是由于在经过众多项⽬的锤炼和对程序设计的不断追求,从⽽在多年编程历程上提炼出来的⼼得体会。 ⽽这份经验最终会与设计模式提到的内容⼏乎⼀致,同样会要求⾼内聚、低耦合、可扩展、可复⽤。你可能也遇到类似的经历,在学习⼀些框架的源码时,发现它⾥的某些设计和你在做开发时⼀样。
我怎么学不会设计模式? 钱也花了,书也买了。代码还是⼀坨⼀坨的!设计模式是由多年的经验提炼出来开发指导思想。就像别人告诉我⾃⾏⻋怎么骑、汽⻋怎么开,但只要没跑过⼏千公⾥,能记住的只是理论,想上道依旧很慌!
⼯⼚⽅法模式介绍
- ⼯⼚⽅法模式,图⽚来⾃ refactoringguru.cn
⼯⼚模式⼜称⼯⼚⽅法模式,是⼀种创建型设计模式,其在⽗类中提供⼀个创建对象的⽅法,允许⼦类决定实例化对象的类型。
这种设计模式也是 Java 开发中最常⻅的⼀种模式,它的主要意图是定义⼀个创建对象的接⼝,让其⼦类⾃⼰决定实例化哪⼀个⼯⼚类,⼯⼚模式使其创建过程延迟到⼦类进⾏。
简单说就是为了提供代码结构的扩展性,屏蔽每⼀个功能类中的具体实现逻辑。让外部可以更加简单的只是知道调⽤即可,同时,这也是去掉众多 if-else 的⽅式。当然这可能也有⼀些缺点,⽐如需要实现的类⾮常多,如何去维护,怎样减低开发成本。但这些问题都可以在后续的设计模式结合使用中,逐步降低。
问题导入
工厂是对对象构造、实例化、初始化过程的一种封装,以提供给其他需要对象的地方去使用,以降低耦合,提高系统的扩展性,重用性。
众所周知,当我们需要把类实例化成对象的时候,需要用到关键字new,比如Plane = new Plane(),这也是我们最常用的方式了。
然而,这样做的结果就是会把这个对象的诞生过程死死捆绑在我们的代码里,宿主类与实例化过程强耦合。对于一些庞大复杂的系统来说,过多的实例化逻辑于宿主类中会给其后期的维护与扩展带来很多麻烦。
而事实是我们根本不关心到底使用哪个对象;怎样生产出它的实例;制造过程是怎样,我们只在乎谁能给我产品来完成我的任务。为了满足用户需求,解决用户的痛点,工厂粉墨登场。
相信大家都玩过打飞机游戏吧,虽然这个主题的游戏版本繁杂但大同小异,都逃不出主角强大的武器系统,以及敌众我寡的战斗形式,所以敌人的种类就得花样百出以带来丰富多样的游戏体验。那么就从这款游戏入手,开始代码。
不好的解决方案(用类图表示,如果有)
首先来定义所有敌人的总抽象,我们想想,敌人们统统都得有一对坐标用来表达位置状态,以便可以把敌人绘制到地图上。为了让子类继承坐标,这里我们使用抽象类来定义敌人。
public abstract class Enemy {
//敌人的坐标,会被子类继承。
protected int x;
protected int y;
//初始化坐标
public Enemy(int x, int y){
this.x = x;
this.y = y;
}
//抽象方法,在地图上绘制。
public abstract void show();
}
这里我们只定义一个抽象方法show,可以把敌人绘制在地图上(下一帧会擦除重绘到下一个坐标以实现动画),当然真正的游戏或许还会有move(移动)、attack(攻击)、die(死亡)等等方法我们这里保持简单就忽略掉了。接下来是具体子类,我们这里假设只有两种,敌机类和坦克类。
public class Airplane extends Enemy {
public Airplane(int x, int y) {
super(x, y); // 调用父类构造子初始化坐标
}
@Override
public void show() {
System.out.println("飞机出现坐标:" + x + "," + y);
System.out.println("飞机向玩家发起攻击");
}
}
public class Tank extends Enemy {
public Tank(int x, int y) {
super(x, y);
}
@Override
public void show() {
System.out.println("坦克出现坐标:" + x + "," + y);
System.out.println("坦克向玩家发起攻击……");
}
}
一如既往地简单,飞机和坦克分别实现不同的show()方法。接下来开始运行游戏并实例化敌人了,重点在于怎样去实例化这些敌人,毋庸置疑要使它们出现在屏幕最上方,也就是纵坐标y等于0,但对于横坐标x我们怎样去初始化呢?写个死值么?这对于游戏可玩性来说是非常糟糕的,玩家会对每次在同一位置出现的敌人烂熟于心,长期下来会觉得无聊,游戏性大打折扣。我们来看是怎样解决这个问题,看客户端代码。
public class Client {
public static void main(String[] args) {
int screenWidth = 100;//屏幕宽度
System.out.println("游戏开始");
Random random = new Random();//准备随机数
int x = random.nextInt(screenWidth);//生成敌机横坐标随机数
Enemy airplane = new Airplane(x, 0);//实例化飞机
airplane.show();//显示飞机
x = random.nextInt(screenWidth);//坦克同上
Enemy tank = new Tank(x, 0);
tank.show();
/*
* 输出结果:
* 游戏开始
* 飞机出现坐标:x1,0
* 飞机向玩家发起攻击……
* 坦克出现坐标:x2,0
* 坦克向玩家发起攻击……
* */
}
}
对,我们在第7行获取了一个从0到屏幕宽度(为了不让敌人出现在屏幕之外)的随机数,作为敌人的横坐标并初始化了敌人,这样每次出现的位置就会不一样了,游戏随机性增强,问题解决了(我们保持简单不考虑敌人自身的宽度)。我们发现从第7行和第11行是在做同样的事情,如果其他地方也需要实例化会出现重复的逻辑,尤其我们还进行了代码省略,实际的逻辑会更复杂,重复代码会更多。如此耗时费力,何不把这些实例化逻辑抽离出来作为一个工厂类?好,开始简单工厂的开发。
import java.util.Random;
public class SimpleFactory {
private final int screenWidth;
private final Random random;//随机数
public SimpleFactory(int screenWidth) {
this.screenWidth = screenWidth;
this.random = new Random();
}
public Enemy create(String type) {
int x = random.nextInt(screenWidth);//生成敌人横坐标随机数
Enemy enemy = null;
switch (type) {
case "Airplane":
enemy = new Airplane(x, 0);//实例化飞机
break;
case "Tank":
enemy = new Tank(x, 0);//实例化坦克
break;
}
return enemy;
}
}
其实这就是简单工厂了,为客户端省去了很多烦扰,于是我们的代码变得异常简单。
public class Client {
public static void main(String[] args) {
System.out.println("游戏开始");
SimpleFactory factory = new SimpleFactory(100);
factory.create("Airplane").show();
factory.create("Tank").show();
}
}
好的解决方案(本模式的解决方案,重点讲解)
然而,这个简单工厂并不是一种设计模式,它只是对实例化逻辑进行了一层简单包裹而已,客户端依然是要告诉工厂我要的是哪个产品,虽然没有出现对产品实例化的关键字new,但这依然无疑是另一种形式的耦合。虽然我们在简单工厂中巧妙利用了坐标随机化来丰富游戏性,但又一个问题出现了,虽然坐标随机变化,但敌人的种类总是不变,游戏又开始变得无聊起来,于是随机生产敌人的工厂迫在眉睫。
我们开始思考,需要再在简单工厂里加个方法createRandomEnemy()?然后也许还需要其他生产方式再继续添加方法?随着之后版本升级,敌人种类的增多,我们再回来继续修改这个工厂类?于是这个工厂会越来越大,变得难以维护,简单工厂不简单,这显然违反了设计模式原则。
从另一方面来讲,用户的需求是多变的,我们要满足各种复杂情况,其实有些时候客户端目的很明确单纯,就是简单的需要工厂生产一个坦克而已,那么我们还有必要加载实例化这么臃肿一个简单工厂类么?问题显而易见了,简单工厂应对简单情况,而针对我们的场景,简单工厂需要多态化,我们应该对生产方式(工厂方法)进行抽象化。首先,定义一个工厂接口。
public interface Factory {
Enemy create(int screenWidth);
}
这个工厂接口就是工厂方法的核心了,它具备这么一个功能(第2行),可以在屏宽之内来产出一个敌人,这就是我们抽象出来的工厂方法。然后我们来定义这个工厂方法的子类实现,随机工厂。
import java.util.Random;
public class RandomFactory implements Factory {
private final Random random = new Random();
@Override
public Enemy create(int screenWidth) {
Enemy enemy = null;
if (random.nextBoolean()) {
enemy = new Airplane(random.nextInt(screenWidth), 0);//实例化飞机
} else {
enemy = new Tank(random.nextInt(screenWidth), 0);//实例化坦克
}
return enemy;
}
}
代码非常简洁明了,这个随机工厂无疑具备生产实力,也就是在第7行实现的工厂方法,但其产出方式是随机产出,拒不退换,我只管努力制造,但出来的是飞机还是坦克,这个由天定。
这也许有点霸权主义,我们也许需要加一些其他工厂,比如某局出现了太多的坦克,一个飞机都没有,这是工厂的随机机制造成的,于是我们可以增加一个平衡工厂让飞机和坦克交替生成,这就好比大型网站上的负载均衡的平衡策略一样,让服务器轮流接受请求。
除了以上工厂,我们或许可以为每关做一个脚本工厂,根据主角关卡进度生产该出现的敌人,又或许更具体点为每个产品做一个工厂,总之,我们可以灵活地根据自己的具体需求去实现不同的工厂,每个工厂的生产策略和方式是不同的,最终是由客户端去决定用哪个工厂生产产品。比如,玩家抵达关底,boss要出现了。
public class Boss extends Enemy {
public Boss(int x, int y) {
super(x, y);
}
@Override
public void show() {
System.out.println("Boss出现坐标:" + x + "," + y);
System.out.println("Boss向玩家发起攻击……");
}
}
接着来实现Boss的工厂方法,此处要注意Boss出现坐标是在屏幕中央,在第6行处设置横坐标为屏幕的一半。
public class BossFactory implements Factory {
@Override
public Enemy create(int screenWidth) {
// boss应该出现在屏幕中央
return new Boss(screenWidth / 2, 0);
}
}
完美,万事俱备,只欠东风,开始运行游戏。
public class Client {
public static void main(String[] args) {
int screenWidth = 100;
System.out.println("游戏开始");
Factory factory = new RandomFactory();
for (int i = 0; i < 10; i++) {
factory.create(screenWidth).show();
}
System.out.println("抵达关底");
factory = new BossFactory();
factory.create(screenWidth).show();
}
}
此处我们于第7行循环10次调用随机工厂生成随机敌人,有时出飞机,有时出坦克,玩家永远猜不透。抵达关底后于第11行换成Boss工厂,并生成Boss,如此一来,我们有产品需要就直接问工厂索要便是,至此客户端与敌人的实例化解耦脱钩。
相比简单工厂,工厂方法可以被看做是一个升级为设计模式的变种,其工厂方法的抽象化带来了极大好处,与其把所有生产方式堆积在一个简单工厂类中,不如把生产方式被推迟到具体的子类工厂中实现,工厂本身也是需要分类的,这样后期的代码维护以及对新产品的扩展都会更加方便直观,而不是对单一工厂类翻来覆去地不停改动。
说明
工厂不是万能的,方便面工厂不能生产汽车,手机工厂更不能生产辣条,这本身就看起来很荒诞,妄想吞噬兼备所有产品的工厂不是好的专业工厂。
工厂模式和简单工厂的图解
通过以下图解再来体会两种(实际上是一种,GOF在《设计模式》一书中工厂模式分为两类:工厂方法模式 Factory Method 与抽象工厂模式 Abstract Factory 。将简单工厂模式 Simple Factory 看为工厂方法模式的一种特例,两者归为一类。)设计模式的不同与相似
工厂方法模式
简单工厂模式
代码编写及运行
完全可以正常运行的,可以自己写出来试试,并通过不断改造来体会
模式的适用环境分析
工厂方法模式适用于以下场景:
- 需要创建的对象较多
当需要创建的对象较多时,通过使用工厂方法模式,可以将所有对象的创建过程统一起来,使得代码更加简洁清晰。
- 需要动态地创建对象
当需要动态地创建对象时,即在运行时根据某些条件来决定创建哪一个具体对象时,可以使用工厂方法模式。
- 需要将对象的创建过程与具体业务逻辑分离开来
当需要将对象的创建过程与具体业务逻辑分离开来,使得代码的可维护性和可读性更高时,可以使用工厂方法模式。
例如,在JDBC中,使用的就是工厂方法模式。在程序中,我们需要创建不同类型的数据库连接,但是对于不同的数据库连接,它们的创建过程是不同的。因此,在JDBC中,我们使用了工厂方法模式将创建不同类型的数据库连接的方法抽象出来,由各个具体工厂类来实现。
应用场景如下
- JDK中Calendar的getInstance方法
- JDBC中Connection对象的获取
- Spring中IOC容器创建管理bean对象
- 反射中Class对象的newInstance方法
模式的优缺点分析
优点
- 工厂方法模式将对象的创建过程封装在工厂类中,使得客户端无需关心具体对象的创建过程,从而降低了客户端与具体产品类的耦合度。
- 工厂方法模式具有良好的扩展性,由于具体工厂类是通过抽象工厂类来定义的,因此在需要添加新的产品类时,只需要实现抽象产品对应的具体产品类和抽象工厂对应的具体工厂类即可。
- 工厂方法模式可以控制对象的创建过程,以便根据需求来动态地创建具体的对象。
缺点
- 工厂方法模式的编写比较复杂,需要定义抽象产品类、具体产品类、抽象工厂类和具体工厂类等多个类。
- 工厂方法模式可以过度使用,当系统只需要少量的产品对象时,使用工厂方法模式会增加系统的复杂度,不利于简化系统架构。
总结
工厂方法模式是一种常用的设计模式,它将对象的创建过程抽象出来,由子类来实现具体的创建过程。通过使用工厂方法模式,可以将对象的创建和具体业务逻辑分离开来,提高代码的可维护性和可读性。在实际应用中,工厂方法模式通常用于需求较多、需要动态创建对象或需要将对象的创建过程与具体业务逻辑分离的场景中。
在使用工厂方法模式时,需要定义抽象产品类、具体产品类、抽象工厂类和具体工厂类等多个类,这会增加系统的复杂度,不利于简化系统架构。因此,在使用工厂方法模式时需要注意权衡其优缺点,避免过度使用。
除了工厂方法模式之外,还有简单工厂模式、抽象工厂模式等其他创建型设计模式,它们各自适用于不同的场景。因此,在选择使用哪种模式时,需要根据具体的需求和情况进行选择。
无论是简单工厂模式,工厂方法模式,还是抽象工厂模式,他们都属于工厂模式,在形式和特点上也是极为相似的,他们的最终目的都是为了解耦。
(1)简单工厂模式是由一个具体的类去创建其他类的实例,父类是相同的,父类是具体的。 (2)工厂方法模式是有一个抽象的父类定义公共接口,子类负责生成具体的对象,这样做的目的是将类的实例化操作延迟到子类中完成。 (3)抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无须指定他们具体的类。它针对的是有多个产品的等级结构。而工厂方法模式针对的是一个产品的等级结构。
在使用工厂模式时,只需要关心降低耦合度的目的是否达到了。
对比简单工厂和工厂方法
- 简单工厂模式(静态工厂模式)
- 虽然某种程度上不符合设计原则,但实际使用最多!
- 工厂方法模式
- 不修改已有类的前提下,通过增加新的工厂类实现拓展
对比简单工厂和工厂方法
结构复杂度:simple>method 代码复杂度:simple>method 编程复杂度:simple>method 管理复杂度:simple>method
根据设计原则,使用工厂方法模式,根据实际业务,使用简单工厂模式
如果根据设计原则反而把一个软件设计得极其复杂,也是很不好的
拓展思路
通过上面的例子,如果大家对模板模式有所了解的话,应该可以看出工厂模式实际上与模板模式极其类似,都是通过父类定义标准和固定的处理流程,而具体的实现方法则交给子类实现,这样的模式可以极大的增加代码的灵活度和拓展性,希望大家能够掌握其中的精髓。
当然,这两个模式也有自己的缺点,其中之一就是如果开发者没有理解在类中定义的固定流程(通常在实际开发中,业务逻辑会比上文例子中复杂数十倍),这就会导致开发者在开发其具体子类时遇到极大困难。因此我们在使用这些模式时,一定要向维护这些类的开发者准确传达这些设计模式的目的。否则将会适得其反。
转载自:https://juejin.cn/post/7278239421706354724