工厂模式

当看到 new 就会想到 具体

是的,当使用 new 时,的确是在实例化一个具体类,所以用的的确是实现,而不是接口。我们已经知道了代码绑定具体类会导致代码更脆弱,更缺乏弹性。 当有一群相关的具体类时,通常会写出这样的代码: 当看到这样的代码,一旦有变化或扩展,就必须重新打开这段代码进行检查和修改。通常这样修改过的代码将造成部分系统更难维护和更新,也更容易犯错。

new 有什么不对劲?

在技术上,new 没有错,毕竟这是面向对象编程的基础部分。真正的凡人是我们老朋友“改变”,以及它是如何影响 new 的使用的。 针对接口编程,可以隔离掉以后系统可能发生的一大堆改变。为什么?如果代码是针对接口而写,那么通过多态,它可以与任何新类实现该接口。但是,当代码使用大量的具体类时,等于是自找麻烦,因为一旦加入新的具体类,就必须改变代码。也就是说,你的代码并非“对修改关闭”。想用新的具体类型来拓展代码,就必须重新打开它。 所以该怎么办呢?当遇到这样的问题时,就应该回到 OO 设计原则去寻找线索。别忘了,我们的第一个原则就是用来处理改变,并帮助我们“找出会变化的方面,把它们从不变的部分分离出来”。

识别变化的方面

假设我们要实现一个披萨店 J1OlDuEBp7APv5y

但是你需要更多披萨类型

我们必须增加一些代码,来“决定”适合的披萨类型,然后在“制造”这个披萨。 QFqBEyW6bpmvswD

但是压力来自远更多的披萨类型

比如我们要更新我们的披萨菜单,要新增几个披萨类型,然后移除几个已有的披萨。 CWFpPtBw1e2YmIE 很明显的,如果实例化“某些”具体类,将使 orderPizza 出现问题,而且也无法让 orderPizza 对修改关闭。但是,现在我们已经知道哪些会改变,哪些不会改变,该是时候使用封装的时候了。

封装创建对象的代码

现在最好将创建对象移动到 orderPizza 之外: xeFoUNS5QADyT6C 我们称这个新对象为“工厂” 工厂(factory)处理创建对象的细节。

建立一个简单披萨工厂

先从工厂本身开始,我们定义一个类,为所有披萨封装创建对象的代码。代码像这样...... pBtof8nCvL6s7S5 这样我们就可以改造刚才的代码了 jS5VZFEetdGMasR

定义简单工厂

简单工厂并不是一个设计模式,反而比较像是一种编程习惯。有些开发人员的确是把这个编程习惯误认为是“工厂模式”。但是不要因为简单工厂不是一个“真正的”模式,就忽略了它的用法。让我们来看看新的披萨店类图。 1qDOG6UxoCPuBm3 谢谢简单工厂来为我们暖身。接下来登场的是两个重量级的模式,它们都是工厂。 但是别担心,未来还有更多的披萨!

加盟披萨店

现在披萨店经营有成,好多地方想加盟我们的披萨店,但是由于不同的地方的披萨做的都有大大小小的差异,所以我们希望建立一个框架,把加盟店和创建披萨绑在一起的同时又保持一定的弹性。

给披萨店使用的框架

我们把 createPiza 方法放回到 PizzaStore 中,不过要把它设置成“抽象方法”,然后为每个地区创造一个 PizzaStore 的子类。 首先,看看 PizzaStore 所做的改变: gVwkZ8zvLcN4TQH 现在已有一个 PizzaStore 作为超类;让每个区域(NYPizzaStore,ChicagoPizzaStore,CaliforniaPizzaStore)都继承这个 PizzaStore,每个子类各自决定如何制造披萨。让我们看看这要如何运行。 BUkKTDYpPl9z2sF

让我们开一家加盟店吧

开加盟店有它的好处,可以从 PizzaStore 免费取得所有的功能。区域店只需要继承 PizzaStore,然后提供 createPizza() 方法实现自己的披萨风味即可。 这是纽约风味: Js1khuWpQFweIbn

工厂方法模式

我们可以看到刚才的设计,我们把原本由一个对象负责所有具体类的实例化,现在通过对 PizzaStore 做一些转变,编程由一群子类负责实例化。工厂方法模式(Factory Method Pattern)通过让子类决定该创建的对象是什么,来达到对象创建的过程封装的目的。让我们来看看这些类图,以了解由哪些组成元素: 创建者(creator)类 UfNwlJBqcLPXDQj 产品类 eW4ukNFazjEwn25

定义工厂方法模式

工厂方法模式 定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。 工厂方法让类把实例化推迟到子类。

P7boq4vLMHCIQjf

一个很依赖的披萨店

假设我们从来没有听说过工厂模式,下面是一个不使用工厂模式的披萨店。数一数,这个类所依赖的具体披萨对象有几种。如果又加了一种加州风味披萨到这个披萨店中,那么届时又会依赖几个对象?


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;
  }
}

看看对象依赖

当你直接实例化一个对象时,就是在依赖它的具体类。看看上面这个依赖性很高的披萨店的例子,它由披萨店类来创建所有的披萨对象,而不是委托给工厂。 wxtcHviIsCVnA6p

依赖倒置原则

很清楚的,代码里减少对于具体类的依赖是件好事。事实上,还一个 OO 设计原则就正式阐明了这一点,这个原则设置还有一个响亮又正式的名字:依赖倒置原则(Dependency Inversion Principle)。

设计原则 要依赖抽象,不要依赖具体类。

即:不能让高层组件依赖低层组件,而且,不管高层或低层组件,两者都应该依赖于抽象。

原则的应用

非常依赖披萨店的主要问题在于:它依赖每个披萨的类型。因为它是在自己的 orderPizza() 方法中,实例化这些具体类的。虽然我们已经创建了一个抽象,也就是 Pizza,但是我们仍在代码中,实际地创建了具体的 Pizza,所以,这个抽象没什么影响力。 而应用工厂方法之后,依赖关系就变成了这样: miN9aMxbRQrS7uY

依赖倒置原则,究竟倒置在了哪里?

在依赖倒置原则中的倒置指的是和一般 OO 设计的思考方式完全相反。看上面的两个图,你会注意到低层组件现在竟然依赖高层组件的抽象。同样的,高层组件现在也依赖相同的抽象。之前第一张图的依赖图是自上而下的,现在却倒置了,而且高层与低层代码现在都依赖这个抽象。

开始把控披萨店的原料

披萨店开的很成功,为了进一步把控质量,我们现在需要控制每家披萨店的原料。现在唯一的问题就是加盟店坐落在不同的区域,不同区域的相同披萨对应的原料是不一致的,那么我们该怎么解决这个问题?

搭建原料工厂

现在,我们要建造一个工厂来生产原料,这个工厂负责创建所有区域店披萨的每一种原料。也就是说,工厂将需要生产面团,酱料,芝士等。等会儿,你就知道如何处理各个区域的差异来。 开始先为工厂定义一个接口,这个接口负责创建所有的原料:

export interface PizzaIngredientFactory {
  createDough(): Dough;
  createSauce(): Sauce;
  createCheese(): Cheese;
  createVeggies(): Veggies[];
  createPepperoni(): Pepperoni;
  createClam(): Clams;
}

要做的事情是:

  1. 为每个区域建造一个工厂。你需要创建一个继承自 PizzaIngredientFactory 的子类来实现每一个创建方法。
  2. 实现一组原料类供工厂使用,例如 ReggianoCheese,RedPeppers,ThickCrustDough。这些类可以在合适的区域内使用。
  3. 然后你仍然需要将这一切组合起来,将新的原料工厂整合进旧的 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();
  }
}

再修改披萨店的实现

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;
  }
}
  • 纽约店会用到纽约披萨原料工厂,由该工厂负责生产所有纽约风味披萨所需的原料
  • 把工厂传递给每一个披萨,以便于披萨能从工厂中获取原料

我们做了什么?

一连串的代码改变,我们到底做了什么?
我们引入新的类型的工厂,也就是所谓的抽象工厂,来创建披萨原料
通过抽象工厂所提供的接口,可以创建原料,利用这个接口书写代码,我的的代码从实际工厂中解耦,以便在不同的上下文中实现各式各样的工厂,制造出各种不同的产品。
因为代码从实际的产品中解耦了,所以我们可以替换不同的工厂来取得不同的行为。

定义抽象工厂模式

我们又在模式家族里新增了另一种工厂模式,这个模式可以创建产品的家族。看看这个模式的正式定义: >抽象工厂模式
提个一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类。

抽象工厂允许客户使用抽象的接口来创造一组相关的产品,而不需要知道实际产出的具体产品是什么。这样一来,客户就从具体的产品中被解耦。 Ix9viQkcYo1mDR4

比较工厂方法和抽象工厂

工厂方法

4jsEfydSFgILvNq

抽象工厂

RJ5w4jLOWTYvikV

要点

  • 所有的工厂都是用来封装对象的创建
  • 简单工厂,虽然不是真正的设计模式,但仍不失为一个简单的方法,可以将客户程序从具体类中解耦
  • 工厂方法使用继承:把对象的创建委托给子类,子类实现工厂方法来创建对象
  • 抽象工厂使用对象组合:对象的创建被实现在工厂接口所暴露出来的方法中
  • 所有工厂模式都通过减少应用程序和具体类之间的依赖促进松耦合
  • 工厂方法允许类将实例化延迟到子类进行
  • 抽象工厂创建相关的对象家族,而不需要依赖它们的具体类
  • 依赖倒置原则,指导我们避免依赖具体类,而要尽量依赖抽象
  • 工厂是很有威力的技巧,帮助我们针对抽象编程,而不要针对具体类编程