aoi学院

Aisaka's Blog, School of Aoi, Aisaka University

设计模式-20软工第2周安排-创建型模式-工厂模式三兄弟

师说

这两周我们讲解的3个工厂相关的设计模式,可以结合一个例子“惠普、戴尔等工厂生产鼠标、键盘、耳麦等产品”总结如下:

简单工厂模式的方式

有一个专门生产某个产品的类,比如下图种的鼠标工厂专业生产鼠标。给参数0,生产戴尔鼠标;给参数1,生产惠普鼠标。


工厂方法模式

工厂方法模式也就是鼠标工厂是个父类(接口),有生产鼠标这个方法,戴尔鼠标工厂(类)和惠普鼠标工厂(类)都继承鼠标工厂,可以分别生产戴尔鼠标,惠普鼠标。

生产哪种鼠标不再由像简单工厂模式那样由参数决定,而是由对应的鼠标生产厂工厂创建,比如说戴尔鼠标工厂,就会生产戴尔鼠标。


抽象工厂模式

抽象工厂模式和前面两种模式不同,前面两种模式只能生产一类产品(鼠标)。这里的抽象工厂模式不仅生产鼠标,同时也能生产键盘。

抽象出一个父工厂,PC厂商接口,由生产鼠标,生产键盘两个方法,其中,戴尔工厂,惠普工厂继承了它。戴尔工厂可以生产戴尔鼠标+戴尔键盘;惠普工厂可以生产惠普鼠标+惠普键盘。


众所周知,设计模式是针对一些特定场景的、比较通用的解决方案。实际上这个定义已经指明了学习设计模式的两个重点:场景和方案。也就是说,学习设计模式的重点在于搞清楚这个模式的使用场景、以及它解决了什么问题。再深入一点的话,可以思考一下如果不使用设计模式还有什么样的方案、以及各种方案之间的优劣对比。

使用设计模式时也是一样:优先考虑使用场景以及要解决的问题,其次考虑各种解决方案之间的优劣对比,最后——其实压根也不用去考虑——到底要使用模式A还是模式B。总的来说,“使用-对比-概念,就是这个顺序”。之所以强调这一点,是因为无论是学习还是使用设计模式,我都见过很多人把时间精力花费在分辨模式A与模式B之间的概念上。最后呢?大多数人都对把自己绕晕,不但没搞清楚两者的区别,还忘了它们要怎么用、什么时候用。说到底,“黑猫白猫,会捉老鼠就是好猫”。我们学习和使用设计模式时,也不应该把重点放在“这是黄色的母马还是黑色的公马”上,而应该是这马适合长途负重、还是短距离冲刺。找准了使用场景,设计模式其实又容易学、又容易用。


也可以看我大三以前的上这门课时的博客:

设计模式-创建型模式-简单工厂模式
设计模式-创建型模式-工厂方法模式
设计模式-创建型模式-抽象工厂模式

工厂模式个人认为其实比较难理解的,如果有接触过|听过|见过该模式的同学很可能就会想:我自己new一个对象出来就好了,简单快捷。用得着你这个工厂模式吗?搞一个工厂出来还要写一大堆的代码呢?

网上的很多资料都是在阐述着:工厂模式的好处就是解耦。相信大家对解耦这个词也不陌生,那解耦究竟有什么好处呢?

本文章试图去解释为什么要用工厂模式,用了工厂模式的好处是什么,以及工厂模式衍生出的三种形式究竟有什么区别。


工厂模式概述

在《设计模式之禅》这本书中分了两章节讲工厂模式:

  • 工厂方法模式(ps:其中里面讲到了简单工厂模式)
  • 抽象工厂模式

网上的大部分资料都是将工厂模式分成三种:

  • 简单/静态工厂模式
  • 工厂方法模式
  • 抽象工厂模式

为什么要用工厂模式?

想想我们为什么要用工厂模式?下面我就简单举例子:

文件IO的操作我们会经常用得到吧,所以BufferedReader对象经常要创建的:

1
2
// 创建一个BufferedReader对象
BufferedReader bf = new BufferedReader(new FileReader(new File("aa.txt")));

你说麻烦吗?其实也不麻烦,就一行代码,哪里麻烦了。如果不太熟悉IO流的同学就没有那么机灵了,创建一个BufferedReader可能就是以下的代码了:

1
2
3
File file = new File("aa.txt");
FileReader fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader);

你说麻烦吗?其实也不麻烦,不就是三行代码,哪里麻烦了。但如果这个应用很多的类上都用到了BufferedReader对象的话,那每个类都写上这三行代码了。那你说麻烦吗?那肯定麻烦!

可以看出来,创建一个BufferReader对象里面需要一个FileReader对象,而FileReader对象又要File对象。那创建这个BufferReader对象还是比较麻烦的(代码上看不麻烦,从构造上看还是挺麻烦的)

虽然比较麻烦,但我们还能用,能用就行!于是乎,我们就去写代码了,现在有三个类都要进行文件的读写操作,于是他们就有这样的代码:

1
2
3
4
5
6
7
8
9
10
public class FileOperateA {

public static void main(String[] args) throws FileNotFoundException {
File file = new File("aa.txt");
FileReader fileReader = new FileReader(file);
BufferedReader bufferedReader = new BufferedReader(fileReader);

// 读写文件....
}
}

此时:上头说,我要换成LineNumberReader来读写,有这个需求!那我们作为一个写代码的,能怎么办?很绝望也需要去完成呀。

  • 不熟悉IDE的小伙子就一个一个将BufferedReader改成LineNumberReader,现在就3个类用到了BufferedReader,也就改6次而已。(ps:那如果很多地方都用到了呢?)
  • 熟悉IDE的小伙子就全局替换重构

那有没有一种方法能够让创建对象变得简单而且修改对象时能很方便呢?工厂模式就行了!

再说从面向对象的角度来看:我一个操作文件的类还要我会创建BufferReader是不是有点过分了?(职责没有分工好)交给工厂来创建对象这就很面向对象了!


体验工厂模式

何为工厂?将我们的产品都交由工厂来生产!我现在用的iphone5s,从哪来?从富士康组装而来,富士康是工厂。我用得着知道iphone5s在富士康是怎么组装起来的吗?不需要。

我们来改造一下上面的例子。首先我们创建一个工厂类,它可以生产Reader对象

1
2
3
4
5
6
7
8
9
// 创建Reader对象的工厂
public class ReaderFactory {
public static Reader getReader() throws FileNotFoundException {
File file = new File("aa.txt");
FileReader fileReader = new FileReader(file);
BufferedReader reader = new BufferedReader(fileReader);
return reader;
}
}

那么我们要得到BufferReader对象就简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FileOperateA {

public static void main(String[] args) throws FileNotFoundException {

//-------我有工厂了,还用自己搞吗?不用了!
//File file = new File("aa.txt");
//FileReader fileReader = new FileReader(file);
//BufferedReader bufferedReader = new BufferedReader(fileReader);
//-------我有工厂了,还用自己搞吗?不用了!

// 用工厂来创建出对象
Reader reader = ReaderFactory.getReader();

// 读写文件....
}
}

工厂将我们创建的对象过程给屏蔽了

此时我要改成LineNumberReader怎么玩?在工厂上改一下就好了:

1
2
3
4
5
6
7
8
9
// 创建Reader对象的工厂
public class ReaderFactory {
public static Reader getReader() throws FileNotFoundException {
File file = new File("aa.txt");
FileReader fileReader = new FileReader(file);
LineNumberReader reader = new LineNumberReader(fileReader);
return reader;
}
}

我们的调用方FileOperateA|FileOperateB|FileOperateC这些类完全就不用变!


使用工厂方法的好处

从上面的工厂模式体验我们就可以看到:

  • 我们修改了具体的实现类,对客户端(调用方)而言是完全不用修改的
  • 如果我们使用new的方式来创建对象的话,那么我们就说:new出来的这个对象和当前客户端(调用方)耦合了!
  • 也就是,当前客户端(调用方)依赖着这个new出来的对象!

这就是解耦的好处!

再放下之前练习的时候写过的代码吧:

有一个DaoFactory,逻辑很简单就是专门创建Dao对象

那么在Service层就可以使用工厂将想要的Dao对象初始化

此时我们的Service与Dao的对象低耦合的(大家可能看不出有什么好处,还弄了一大堆的字符串啥的)

在Service与Controller层我也弄了一个ServiceFactory,根据当时业务的需要(添加权限),我创建Service时就非常灵活了:


如何使用工厂模式

工厂模式可以分成三类:

  • 简单/静态工厂模式
  • 工厂方法模式
  • 抽象工厂模式

下面就逐一来介绍一下每一种工厂模式有什么不一样,三种模式都以买宠物的例子来讲解:


工厂方法模式

很多博客都是以简单/静态工厂模式,工厂方法模式,抽象工厂模式这个顺序来讲解工厂模式的。我认为按书上的顺序比较好理解,因为简单/静态工厂模式是在工厂方法模式上缩减,抽象工厂模式是在工厂方法模式上再增强。

作为一间宠物店,号称什么宠物都有!于是乎,店主宣传的时候就说:我的宠物店什么宠物都有

于是构建宠物的工厂就诞生了

1
2
3
4
5
6
// 号称什么宠物都有
public interface AnimalFactory {

// 可以获取任何的宠物
Animal createAnimal();
}

当然了,主流的宠物得进货一些先放在店里充充门面,一些特殊的宠物就告诉顾客要时间进货。所以,我们就有了构建猫和狗的工厂(继承着所有宠物的工厂)

猫工厂:

1
2
3
4
5
6
7
8
// 继承着宠物工厂
public class CatFactory implements AnimalFactory {
@Override
// 创建猫
public Animal createAnimal() {
return new Cat();
}
}

狗工厂也是一样的:

1
2
3
4
5
6
7
8
// 继承着宠物工厂
public class DogFactory implements AnimalFactory {
// 创建狗
@Override
public Animal createAnimal() {
return new Dog();
}
}

还有我们的实体类:猫、狗、动物(多态:猫和狗都是动物,可以直接用动物来表示了)

动物实体类:

1
2
3
4
public abstract class Animal {
// 所有的动物都会吃东西
public abstract void eat();
}

猫实体类:

1
2
3
4
5
6
7
public class Cat extends Animal {
// 猫喜欢吃鱼
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}

狗实体类:

1
2
3
4
5
6
7
public class Dog extends Animal {
// 狗喜欢吃肉
@Override
public void eat() {
System.out.println("狗吃肉");
}
}

那么现在想要一只狗,跟了宠物店老板说,宠物店老板就去找狗回来了:

1
2
3
4
5
6
// 去找狗工厂拿一只狗过来
AnimalFactory f = new DogFactory();

// 店主就拿到了一只狗给Java3y
Animal a = f.createAnimal();
a.eat();

那么现在想要一只猫,跟了宠物店老板说,宠物店老板就去找猫回来了:

1
2
3
4
5
6
// 去找猫工厂拿一只猫过来
AnimalFactory ff = new CatFactory();

// 店主就拿到了一只猫给Java3y
Animal aa = ff.createAnimal();
aa.eat();

如果这个时候说想要一只蜥蜴怎么办啊?没问题啊,店主搞个蜥蜴工厂就好了

1
2
3
4
// 要买蜥蜴..
AnimalFactory fff = new LizardFactory();
Animal aaa = fff.createAnimal();
aaa.eat();

优点:

  1. 客户端不需要在负责对象的创建,明确了各个类的职责
  2. 如果有新的对象增加,只需要增加一个具体的类和具体的工厂类即可
  3. 不会影响已有的代码,后期维护容易,增强系统的扩展性

缺点:

  1. 需要额外的编写代码,增加了工作量

工厂方法类图:


简单/静态工厂模式

现在宠物店生意不好做啊,号称“什么宠物都有”,这吹过头了。于是店主只卖两种常见的宠物了。

既然就只有两种宠物的话,那就没必要有”猫厂“、”狗厂“了,一个猫狗厂就行了!

所以我们的工厂是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AnimalFactory {
public static Dog createDog() {
return new Dog();
}

public static Cat createCat() {
return new Cat();
}


// 外界想要猫要狗,这里创建就好了
public static Animal createAnimal(String type) {
if ("dog".equals(type)) {
return new Dog();
} else if ("cat".equals(type)) {
return new Cat();
} else {
return null;
}
}
}

三个实体还是没变(动物、猫、狗)

那么去宠物店买猫狗的时候,告诉老板我要猫、我要狗:

1
2
3
4
5
6
7
// 拿到狗
Animal A = AnimalFactory.createAnimal("dog");
A.eat();

// 拿到猫
Animal C = AnimalFactory.createAnimal("cat");
C.eat();

现在问题来了:

  1. 我想要一个猪,可是我的工厂类没有猪
  2. 我就去代码,写可以创建猪对象的
  3. 接着,我又要其他的动物
  4. 我还是得代码
  5. ………………
  6. 这就是简单工厂类的缺点:当需求改变了,我就要改代码。

简单工厂类的优点也很明显:我就一个具体的工厂来创建对象,代码量少。


抽象工厂模式

抽象工厂模式就比较复杂了,一般的应用都写不到。首先来简述一下需求吧:

现在非常流行在猫狗届也吹起了一股“性别风”

  • 有的喜欢公的
  • 有的喜欢母的

那我们的猫和狗都是有性别的,不是公的就是母的

  • 我们之前在工厂方法模式下是每个动物都开一个工厂,如果动物过多的话,那么就有很多的工厂
  • 那现在我们可以抽取出来:每个动物不是公的就是母的
  • 所以我们有两个工厂就足够了!

具体的代码是这样的:

我们的最大工厂还是定义了创建什么动物

1
2
3
4
public interface AnimalFactory {
Animal createDog();
Animal createCat();
}

创建母猫和母狗的工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FemaleAnimalFactory implements AnimalFactory {

// 生产母狗和母猫
@Override
public Animal createDog() {
return new FemaleDog();
}

@Override
public Animal createCat() {
return new FemaleCat();
}

}

创建公猫和公狗的工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MaleAnimalFactory implements AnimalFactory {

// 生产公狗和公猫

@Override
public Animal createDog() {
return new MaleDog();
}

@Override
public Animal createCat() {
return new MaleCat();
}

}

这是所有动物都拥有的普遍行为

1
2
3
4
5
6
7
8
public abstract class Animal {

// 所有的动物都会吃东西
public abstract void eat();

// 所有的动物都有性别
public abstract void gender();
}

这是猫都拥有的普遍行为:

1
2
3
4
5
6
7
public abstract class Cat extends Animal {
// 猫喜欢吃鱼
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}

这是狗都拥有的普遍行为:

1
2
3
4
5
6
7
8
9
public abstract class Dog extends Animal {

// 狗喜欢吃肉
@Override
public void eat() {
System.out.println("狗吃肉");
}

}

猫分为公猫、母猫。狗分为公狗和母狗:

1
2
3
4
5
6
7
public class FemaleCat extends Cat {

public void gender() {
System.out.println("I am a female Cat");
}

}

…………

简单来说:工厂方法模式的工厂是创建出一种产品,而抽象工厂是创建出一类产品。

  • 一类的产品我们称之为产品族
    • 猫是一类的,狗也是一类的。所以AnimalFactory定义了两类产品—>Animal createDog();和Animal createCat();
  • 产品的继承结构称之为产品等级
    • 所有的狗都是会吃肉的,所以Dog实现了**eat()**方法
      • 狗又分成了公狗和母狗,所以定义了两个类FemaleDog和MaleDog继承了Dog,实现了**gender()**方法
    • 所有的猫都是会吃鱼的,所以Cat实现了**eat()**方法
      • 猫又分成了公猫和母猫,所以定义了两个类FemaleCat和MaleCat继承了Cat,实现了**gender()**方法
  • 具体的工厂是面向多个产品等级结构进行生产
    • 所以FemaleAnimalFactory定义了**createDog()createCat()**生产母狗和母猫
    • 所以MaleAnimalFactory定义了**createDog()createCat()**生产公狗和共猫
  • 找到母工厂就可以创建母猫和母狗,找到公工厂就可以创建公猫和公狗
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {

// 需要性别为母的就去找母工厂
AnimalFactory af = new FemaleAnimalFactory();

// 需要一只母猫
af.createCat().gender();

// 需要一只母狗
af.createDog().gender();

// 需要性别为公的就去找公工厂
AnimalFactory aff = new MaleAnimalFactory();

// 需要一只公狗
aff.createDog().gender();

// 需要一只公猫
aff.createCat().gender();
}

这是抽象工厂模式的类图:

抽象工厂模式说到底就是多了一层抽象,减少了工厂的数量

抽象工厂缺点也很明显:

  • 难以扩展产品族:如果我再要宠物猪的话
    • 那我要修改AnimalFactory、FemaleAnimalFactory、MaleAnimalFactory这些类了

总结

总的来说我们用简单工厂模式比较多,工厂方式模式的话代码量会比较大,抽象工厂模式的话需要业务比较大的情况下才会用到。ps:工厂模式配合反射来使用也是极好的


参考资料

20软工第2周安排
工厂模式理解了没有?
《设计模式之禅》
【原创】设计模式系列(九)——抽象工厂模式
工厂设计模式有什么用?
Java设计模式——工厂模式
详解设计模式之工厂模式(简单工厂+工厂方法+抽象工厂)
工厂模式