开个新坑,复习基础知识,用typescript写写旧技术——设计模式。今天就介绍一下工厂模式,以及其他两个衍生模式工厂方法和抽象工厂。
简单工厂
工厂模式,又称简单工厂,顾名思义就是使用“工厂”(一个或一系列方法)去生产“产品”(一个或一系列的派生类实例)。UML如下所示:
上图我定义产品接口名为Vegetable,它的两个派生类为Onion和Garlic:
// Vegetable.ts
interface Vegetable {
fry(): void;
}
class Onion implements Vegetable {
public fry(): void {
console.log('Onion');
}
}
class Garlic implements Vegetable {
public fry(): void {
console.log('Garlic');
}
}
“蔬菜工厂”如下所示,通过调用不同的方法生产出不同的蔬菜实例。
p.s. 有些工厂模式的实现只有一个函数体,根据特定参数来“生产”特定的派生实例,这也是可行的。
// SimpleFactory.ts
import {Garlic, Onion, Vegetable} from './Vegetable';
class SimpleFactory {
public createOnion(): Vegetable {
return new Onion();
}
public createGarlic(): Vegetable {
return new Garlic();
}
}
最后看一下client调用:
// client.ts
import {SimpleFactory} from './SimpleFactory';
import {Vegetable} from './Vegetable';
const factory: SimpleFactory = new SimpleFactory();
let onion: Vegetable = factory.createOnion();
let garlic: Vegetable = factory.createGarlic();
onion.fry(); // Onion
garlic.fry(); // Garlic
OK,问题来了,我们使用工厂模式的意义是什么?饶了这么一大圈不就是打印了个两个菜名吗?为什么不直接new呢?
import {Onion, Garlic} from './Vegetable';
let onion: Onion = new Onion();
let garlic: Garlic = new Garlic();
直接new出两个蔬菜实力确实特别简单,但是在大型软件开发中这很危险,用专业术语来说是违反了依赖倒置原则:
高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象
有点绕,阳春白雪和者必寡,我们还是走下里巴人路线吧。通俗来讲,引入的依赖并不可靠,它一般是第三方库或是其他开发人员实现的代码;在不确定的某一天,里面的代码会被修改甚至是删除。这时候你的代码会变得很脆,不知不觉中就崩了。但现实中又不可能不引入其他依赖,所以大家就约定最小依赖引入:依赖提供者将隐藏对象的属性和实现细节,仅对外公开接口(抽象),这就是OOP三大特性之一的封装。
再回看一下client实现,只import了一个公用接口Vegetable
。运行时,我们利用多态——let onion: Vegetable = factory.createOnion()
——就可以实现派生方法的动态绑定(onion.fry()
)。这样,“派生蔬菜”(Onion或Garlic)的修改就不会影响现有代码了。哪天VegetableFactory
开发者觉得Onion子类实现太过丑陋或是性能太差,他只需在简单工厂里换一个新的“派生蔬菜”(OnionFromJapan
)即可,client代码不需要做任何改动。
class VegetableFactory {
public createOnion(): Vegetable {
return new OnionFromJapan();
}
}
工厂模式的最大优点就是屏蔽产品的具体实现,调用者只关心产品的接口。当然,它也有自己的问题,产品种类可能有成千上万,如果都是依靠同一个工厂生产,那么必然会使得工厂代码及其庞大。这就有了工厂方法的设计实现。
工厂方法
工厂方法就是针对每一种产品提供一个工厂类,通过不同派生工厂创建不同的产品。
实现很简单,为每种蔬菜提供对应的派生“蔬菜工厂”就行了。问题又来了,工厂与蔬菜一一对应有没有多此一举呢?要不直接new?嗯,这里不给出解答了,回去体会一下oop三大特性:封装、多态和继承。
import {Garlic, Onion, Vegetable} from './Vegetable';
interface VegetableFactory {
create(): Vegetable;
}
class OnionFactory implements VegetableFactory {
public create(): Vegetable {
return new Onion();
}
}
class GarlicFactory implements VegetableFactory {
public create(): Vegetable {
return new Garlic();
}
}
工厂方法减轻了工厂类的负担,新增一种“蔬菜”只需添加一个特定的“蔬菜工厂”即可,这就符合了开放闭合原则:
对扩展是开放的;对修改是关闭的
这里提一下,开放闭合原则并不是说接口一成不变,它要求的是增量变化——只增加新方法,不改动旧方法。
抽象工厂
工厂方法自然比简单工厂更加灵活,但当业务有所变更时,比如需要添加“蔬菜”的周边产品——“酒”呢?(洋葱和红酒更配哦)这时候我们就需要一个更复杂的模式——抽象工厂了。
抽象工厂是一个产品簇的概念,一个工厂可以生产多种业务相关的产品。我们在工厂方法的基础上扩充一下代码:定义一个抽象工厂接口AbstractFactory
,通过不同的方法生产出一个“抽象”产品簇(Vegetable
和Drink
)。回过头来再看工厂方法,事实上它就是抽象工厂最简单的一种场景设计——只生成一种产品。
interface AbstractFactory {
create(): Vegetable;
pick(): Drink;
}
class OnionRecipe implements AbstractFactory {
public create(): Vegetable {
return new Onion();
}
public pick(): Drink {
return new Wine();
}
}
抽象工厂的缺点很明显:成也产品簇败也产品簇,复杂度大,应用场景有限。
总结
简单工厂:调用者只需使用单例工厂就可获取同一范畴的所有产品
工厂方法:调用者并不知道它在运行时会获取何种产品,只知道某个特定的工厂能生成出满足需求的产品
抽象工厂:调用者可以在运行时从特定的工厂中获得所有信息相关的产品簇
设计模式是上个世纪九十年代初从建筑领域引入到计算机软件开发领域的概念;是对软件设计中反复出现的各种问题所提出的一套解决方案。一般来说,设计模式的教学案例都是基于OOP语言java实现的,所以有时候会有一种错觉,以为设计模式只有java开发人员才该掌握的。但事实上设计模式更多的是一种思想,是在某种情景下解决特定问题的可靠途径,它不仅仅局限于组织编码,更可以应用于架构层面的思考。
思考题
如果我们的一个服务类中出现大量类似于if(useA)
、if(useB)
这样的条件判断语句,你会怎么去重构它?
class Service {
public methodA(): void {
...
if( this.useA ){
// omit
}
...
}
public methodB(): void {
...
if( this.useA ){
// omit
}
...
if( this.useB ){
// omit
}
...
}
}