当看到 new 就会想到 具体
是的,当使用 new 时,的确是在实例化一个具体类,所以用的的确是实现,而不是接口。我们已经知道了代码绑定具体类会导致代码更脆弱,更缺乏弹性。 当有一群相关的具体类时,通常会写出这样的代码: 当看到这样的代码,一旦有变化或扩展,就必须重新打开这段代码进行检查和修改。通常这样修改过的代码将造成部分系统更难维护和更新,也更容易犯错。
new 有什么不对劲?
在技术上,new 没有错,毕竟这是面向对象编程的基础部分。真正的凡人是我们老朋友“改变”,以及它是如何影响 new 的使用的。 针对接口编程,可以隔离掉以后系统可能发生的一大堆改变。为什么?如果代码是针对接口而写,那么通过多态,它可以与任何新类实现该接口。但是,当代码使用大量的具体类时,等于是自找麻烦,因为一旦加入新的具体类,就必须改变代码。也就是说,你的代码并非“对修改关闭”。想用新的具体类型来拓展代码,就必须重新打开它。 所以该怎么办呢?当遇到这样的问题时,就应该回到 OO 设计原则去寻找线索。别忘了,我们的第一个原则就是用来处理改变,并帮助我们“找出会变化的方面,把它们从不变的部分分离出来”。
识别变化的方面
假设我们要实现一个披萨店
但是你需要更多披萨类型
我们必须增加一些代码,来“决定”适合的披萨类型,然后在“制造”这个披萨。
但是压力来自远更多的披萨类型
比如我们要更新我们的披萨菜单,要新增几个披萨类型,然后移除几个已有的披萨。 很明显的,如果实例化“某些”具体类,将使 orderPizza 出现问题,而且也无法让 orderPizza 对修改关闭。但是,现在我们已经知道哪些会改变,哪些不会改变,该是时候使用封装的时候了。
封装创建对象的代码
现在最好将创建对象移动到 orderPizza 之外: 我们称这个新对象为“工厂” 工厂(factory)处理创建对象的细节。
建立一个简单披萨工厂
先从工厂本身开始,我们定义一个类,为所有披萨封装创建对象的代码。代码像这样...... 这样我们就可以改造刚才的代码了
定义简单工厂
简单工厂并不是一个设计模式,反而比较像是一种编程习惯。有些开发人员的确是把这个编程习惯误认为是“工厂模式”。但是不要因为简单工厂不是一个“真正的”模式,就忽略了它的用法。让我们来看看新的披萨店类图。 谢谢简单工厂来为我们暖身。接下来登场的是两个重量级的模式,它们都是工厂。 但是别担心,未来还有更多的披萨!
加盟披萨店
现在披萨店经营有成,好多地方想加盟我们的披萨店,但是由于不同的地方的披萨做的都有大大小小的差异,所以我们希望建立一个框架,把加盟店和创建披萨绑在一起的同时又保持一定的弹性。
给披萨店使用的框架
我们把 createPiza 方法放回到 PizzaStore 中,不过要把它设置成“抽象方法”,然后为每个地区创造一个 PizzaStore 的子类。 首先,看看 PizzaStore 所做的改变: 现在已有一个 PizzaStore 作为超类;让每个区域(NYPizzaStore,ChicagoPizzaStore,CaliforniaPizzaStore)都继承这个 PizzaStore,每个子类各自决定如何制造披萨。让我们看看这要如何运行。
让我们开一家加盟店吧
开加盟店有它的好处,可以从 PizzaStore 免费取得所有的功能。区域店只需要继承 PizzaStore,然后提供 createPizza() 方法实现自己的披萨风味即可。 这是纽约风味:
工厂方法模式
我们可以看到刚才的设计,我们把原本由一个对象负责所有具体类的实例化,现在通过对 PizzaStore 做一些转变,编程由一群子类负责实例化。工厂方法模式(Factory Method Pattern)通过让子类决定该创建的对象是什么,来达到对象创建的过程封装的目的。让我们来看看这些类图,以了解由哪些组成元素: 创建者(creator)类 产品类
定义工厂方法模式
工厂方法模式 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。 工厂方法让类把实例化推迟到子类。
一个很依赖的披萨店
假设我们从来没有听说过工厂模式,下面是一个不使用工厂模式的披萨店。数一数,这个类所依赖的具体披萨对象有几种。如果又加了一种加州风味披萨到这个披萨店中,那么届时又会依赖几个对象?
export class DependentPizzaStore {
public createPizza(style: string, type: string) {
let pizza: Pizza = null;
if (style === 'NY') {
if (type === 'cheese') {
pizza = new NYCheesePizza();
} else if (type === 'veggie') {
pizza = new NYVeggiePizza();
} else if (type === 'clam') {
pizza = new NYClamPizza();
} else if (type === 'pepperoni') {
pizza = new NYPepperoniPizza();
}
} else if (pizza === 'Chicago') {
if (type === 'cheese') {
pizza = new ChicagoCheesePizza();
} else if (type === 'veggie') {
pizza = new ChicagoVeggiePizza();
} else if (type === 'clam') {
pizza = new ChicagoClamPizza();
} else if (type === 'pepperoni') {
pizza = new ChicagoPepperoniPizza();
}
}
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
return pizza;
}
}
看看对象依赖
当你直接实例化一个对象时,就是在依赖它的具体类。看看上面这个依赖性很高的披萨店的例子,它由披萨店类来创建所有的披萨对象,而不是委托给工厂。
依赖倒置原则
很清楚的,代码里减少对于具体类的依赖是件好事。事实上,还一个 OO 设计原则就正式阐明了这一点,这个原则设置还有一个响亮又正式的名字:依赖倒置原则(Dependency Inversion Principle)。
设计原则 要依赖抽象,不要依赖具体类。
即:不能让高层组件依赖低层组件,而且,不管高层或低层组件,两者都应该依赖于抽象。
原则的应用
非常依赖披萨店的主要问题在于:它依赖每个披萨的类型。因为它是在自己的 orderPizza() 方法中,实例化这些具体类的。虽然我们已经创建了一个抽象,也就是 Pizza,但是我们仍在代码中,实际地创建了具体的 Pizza,所以,这个抽象没什么影响力。 而应用工厂方法之后,依赖关系就变成了这样:
依赖倒置原则,究竟倒置在了哪里?
在依赖倒置原则中的倒置指的是和一般 OO 设计的思考方式完全相反。看上面的两个图,你会注意到低层组件现在竟然依赖高层组件的抽象。同样的,高层组件现在也依赖相同的抽象。之前第一张图的依赖图是自上而下的,现在却倒置了,而且高层与低层代码现在都依赖这个抽象。
开始把控披萨店的原料
披萨店开的很成功,为了进一步把控质量,我们现在需要控制每家披萨店的原料。现在唯一的问题就是加盟店坐落在不同的区域,不同区域的相同披萨对应的原料是不一致的,那么我们该怎么解决这个问题?
搭建原料工厂
现在,我们要建造一个工厂来生产原料,这个工厂负责创建所有区域店披萨的每一种原料。也就是说,工厂将需要生产面团,酱料,芝士等。等会儿,你就知道如何处理各个区域的差异来。 开始先为工厂定义一个接口,这个接口负责创建所有的原料:
export interface PizzaIngredientFactory {
createDough(): Dough;
createSauce(): Sauce;
createCheese(): Cheese;
createVeggies(): Veggies[];
createPepperoni(): Pepperoni;
createClam(): Clams;
}
要做的事情是:
- 为每个区域建造一个工厂。你需要创建一个继承自 PizzaIngredientFactory 的子类来实现每一个创建方法。
- 实现一组原料类供工厂使用,例如 ReggianoCheese,RedPeppers,ThickCrustDough。这些类可以在合适的区域内使用。
- 然后你仍然需要将这一切组合起来,将新的原料工厂整合进旧的 PizzaStore 代码中。
创建纽约原料工厂
export class NYPizzaIngredientFactory implements PizzaIngredientFactory {
public createDough(): Dough {
return new ThinCrustDough();
}
public createSauce(): Sauce {
return new MarinaraSauce();
}
public createCheese(): Cheese {
return new ReggianoCheese();
}
public createVeggies(): Veggies[] {
return [new Garlic(), new Onion(), new Mushroom(), new RedPepper()];
}
public createPepperoni(): Pepperoni {
return new SlicedPepperoni();
}
public createClam(): Clams {
return new FreshClams();
}
}
重做披萨
工厂已经一切就绪,准备生产高质量原料了,现在我们只需要重做披萨,好让它们只使用工厂生产出来的原料,我们先从抽象的 Pizza 类开始:
export abstract class Pizza {
public name: string;
public dough: Dough;
public sauce: Sauce;
public veggies: Veggies[];
public cheese: Cheese;
public pepperoni: Pepperoni;
public clam: Clams;
public abstract prepare(): void;
public bake() {
console.log('Bake for 25 minutes at 350');
}
public cut() {
console.log('Cutting the pizza into diagonal slices');
}
public box() {
console.log('Place pizza in official PizzaStore box');
}
public getName() {
return this.name;
}
}
- 每个披萨都持有一组在准备时会用到的原料。
- 现在把 prepare() 方法声明成抽象。在这个方法中,我们需要收集披萨所需的原料,而这些原料当然来自于原料工厂。
- 其他的方法保持不变,只要 prepare() 方法需要改变。
具体的 Pizza 则是这样:
export class CheesePizza extends Pizza {
public ingredientFactory: PizzaIngredientFactory;
public constructor(ingredientFactory: PizzaIngredientFactory) {
super();
this.ingredientFactory = ingredientFactory;
}
public prepare() {
console.log(`Preparing ${this.name}`);
this.dough = this.ingredientFactory.createDough();
this.sauce = this.ingredientFactory.createSauce();
this.cheese = this.ingredientFactory.createCheese();
}
}
- 要制作披萨,需要工厂提供原料。所以每个披萨类都需要从构造函数参数中得到一个工厂,并把这个工厂存在一个实例变量中。
- prepare() 方法一步一步地创建芝士披萨,每当需要原料时,就跟工厂要。
同样可以在实现一个蛤蜊披萨
export class ClamPizza extends Pizza {
public ingredientFactory: PizzaIngredientFactory;
public constructor(ingredientFactory: PizzaIngredientFactory) {
super();
this.ingredientFactory = ingredientFactory;
}
public prepare() {
console.log(`Preparing ${this.name}`);
this.dough = this.ingredientFactory.createDough();
this.sauce = this.ingredientFactory.createSauce();
this.cheese = this.ingredientFactory.createCheese();
this.clam = this.ingredientFactory.createClam();
}
}
<a name="PLSzk"></a>
再修改披萨店的实现
class NYPizzaStore extends PizzaStore {
public createPizza(item: string): Pizza {
let pizza: Pizza = null;
const ingredientFactory = new NYPizzaIngredientFactory();
if (item === 'cheese') {
pizza = new NYCheesePizza(ingredientFactory);
pizza.setName('New York Style Cheese Pizza');
} else if (item === 'veggie') {
pizza = new NYVeggiePizza(ingredientFactory);
pizza.setName('New York Style Veggie Pizza');
} else if (item === 'clam') {
pizza = new NYClamPizza(ingredientFactory);
pizza.setName('New York Style Clam Pizza');
} else if (item === 'pepperoni') {
pizza = new NYPepperoniPizza(ingredientFactory);
pizza.setName('New York Style Pepperoni Pizza');
}
return pizza;
}
}
- 纽约店会用到纽约披萨原料工厂,由该工厂负责生产所有纽约风味披萨所需的原料
- 把工厂传递给每一个披萨,以便于披萨能从工厂中获取原料
我们做了什么?
一连串的代码改变,我们到底做了什么?<br />我们引入新的类型的工厂,也就是所谓的抽象工厂,来创建披萨原料<br />通过抽象工厂所提供的接口,可以创建原料,利用这个接口书写代码,我的的代码从实际工厂中解耦,以便在不同的上下文中实现各式各样的工厂,制造出各种不同的产品。<br />因为代码从实际的产品中解耦了,所以我们可以替换不同的工厂来取得不同的行为。
定义抽象工厂模式
我们又在模式家族里新增了另一种工厂模式,这个模式可以创建产品的家族。看看这个模式的正式定义:
抽象工厂模式<br />提个一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。
抽象工厂允许客户使用抽象的接口来创造一组相关的产品,而不需要知道实际产出的具体产品是什么。这样一来,客户就从具体的产品中被解耦。
比较工厂方法和抽象工厂
工厂方法
抽象工厂
要点
- 所有的工厂都是用来封装对象的创建
- 简单工厂,虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户程序从具体类中解耦
- 工厂方法使用继承:把对象的创建委托给子类,子类实现工厂方法来创建对象
- 抽象工厂使用对象组合:对象的创建被实现在工厂接口所暴露出来的方法中
- 所有工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合
- 工厂方法允许类将实例化延迟到子类进行
- 抽象工厂创建相关的对象家族,而不需要依赖它们的具体类
- 依赖倒置原则,指导我们避免依赖具体类,而要尽量依赖抽象
- 工厂是很有威力的技巧,帮助我们针对抽象编程,而不要针对具体类编程