设计模式03-抽象工厂(Abstract Factory)

762 阅读9分钟

意图

为创建相关或依赖对象的家族提供一个接口,不必指明它们的具体类。

别名

Kit

动机

假设一个用户接口工具包支持多“外观和感觉”标准,比如主题和展示管理。不同的“外观和感觉”为用户定义如滚动条,窗口和按钮之类的“小部件”接口不同的外观和行为。为了方便移植到“外观和感觉”标准,应用程序不应该为特定的外观和感觉硬编码小部件。实例化“特定外观和感觉”小部件类自始至终让应用很难以后改变外观和感觉。

我们能通过定义一个抽象的小部件工厂来解决这个问题。这个小部件工厂为创建每个基础的小部件申明一个接口。抽象类也未每一个类型的小部件,具体子类实现特定外观和感觉的小部件。小部件工厂接口有一个操作,这个操作为每一个抽象小部件类返回一个新的小部件对象。使用方调用这些操作去获取小部件实例,但是使用方不用关心他们使用的具体类。因此使用方和流行的外观和感觉保持独立。

上面是一个具体的子类关于为每个外观和感觉标准的小部件工厂。每一个子类实现操作,用于创建合适的外观和感觉的小部件。每个子类实现为“外观和感觉”创建合适的小部件的操作。比如,CreateScrollBar操作在ModifWidgetFactory实例中,为展示管理返回一个Motif 滚动条。使用方仅仅通过WidgetFactory的接口创建小部件,而不用关心为具体的行为和感觉实的类和实现widget。换句话说,使用方仅仅需要通过抽象类提交一个接口定义,而不是具体类。

一个WidgetFacory也在具体widget类之间强制依赖。一个motif滚动条应该用motif按钮和motif文本编辑,这个约束是作为相应的使用一个MotifWidgetFactory自动强制的。

应用

使用抽象工厂模式的时机:

  • 一个系统应该独立于产品如何创建,组合和展示。
  • 一个系统应该可以通过使用众多产品家族的一个进行配置。
  • 一个相关产品对象的家族被设计一起使用,你需要强制这个约束。
  • 你想要提供一个产品的类库,你想要仅查找他们的接口,而不是实现。

结构

参与者

  • 抽象工厂
    • 为创建抽象产品对象操作申明一个接口
  • 具体工厂
    • 实现用来创建具体产品对象的操作
  • 抽象产品
    • 为一类产品对象申明一个接口
  • 具体产品
    • 定义一个被相应的具体工厂创建的产品对象
    • 实现抽象产品接口
  • 客户
    • 仅仅使用抽象工厂和抽象产品类的接口

协作

  • 通常一个独立的具体工厂类的实例是在运行时被创建的。具体工厂创建对象有特定的实现。要创建不同产品对象,使用方需要使用不同的具体工厂。
  • 抽象工厂延迟创建对象的过程到它的具体的工厂子类中。

影响

抽象工厂模式有以下好处和累赘:

  • 隔离了具体类。抽象工厂模式帮助你控制应用创建的对象的类。因为工厂封装了创建产品对象的职责和过程,它隔离了使用者和实现类。使用者通过抽象接口操作实例。产品类名字在具体工厂的实现中被隔离,不出现在使用方的代码中。
  • 使得很容易更换产品家族。具体工厂类只在一个应用中出现一次,就是它实例化的时候。这使得很容易改变一个应用使用的具体工厂。可以通过改变具体工厂来使用不同的产品配置。因为一个抽象工厂创建一个完整的产品家族,整个产品家族一次性改变。在我们用户接口例子中,我们可以通过简单的通过切换相应的工厂对象再重建实例的方式,从而从Motif小部件切换到PresentationManager小部件。
  • 促进产品之间的一致性。我们在一个设计好的一起工作的家族生产对象,一个应用使用特定时刻来自一个家族的对象非常重要。抽象工厂使得遵循这件事非常简单。
  • 支持新种类的产品很难。扩展抽象工厂生产新种类的产品不容易。因为抽象工厂接口固定了可以被创建的产品集合。假设新类别的产品需要扩展工厂接口,会导致改变抽象工厂类和所有的子类。我们在实现章节中讨论其中一种解决方案。

实现

这里有一些有用的技术来实现抽象工厂模式。

  1. 工厂作为单例。一个应用典型的只需要每个产品家族的一个具体工厂的实例。因此通常最好被实现成单例。
  2. 创建产品。抽象工厂仅仅为创建对象申明一个接口。一直到创建产品子类才真正的创建他们。最通用的方式的实现是为每一个产品定义一个工厂方法。一个具体的工厂将通过重写每个工厂方法指定它生产的产品。同时这个实现很简单,它需要为每一个产品家族一个新的具体工厂子类,即使这个产品家族仅有一点点不同。

如果有很多产品家族,具体工厂可以使用Prototype模式实现。具体工厂使用为每一个家族中的产品使用一个原型实例初始化,然后它通过克隆它的原型创建一个新产品。这种基于原型的方式消除了每一个新的产品家族需要新的具体工厂类的需求。

在smalltalk中有一个实现基于原型的工厂的方式。具体工厂存储要被克隆的原型在一个呗叫做partCatalog目录中。这个方法使得:检索原型和克隆它:

make: partName
       ^ (partCatalog at: partName) copy
The concrete factory has a method for adding parts to the catalog.
   addPart: partTemplate named: partName
       partCatalog at: partName put: partTemplate
Prototypes are added to the factory by identifying them with a symbol: aFactory addPart: aPrototype named: #ACMEWidget

基于原型的方法的一个变种就是,可能在语言中对待类作为第一类对象(如smalltalk 和 ObjectiveC)。你可以认为一个类在这些语言中是一个退化的工厂,仅仅创建一类对象。你可以存储类在一个具体的工厂中,这个具体工厂生产各种各样具体在变量中的产品,就像原型。你通过初始化一个具体工厂类的实例定义一个新的工厂,通过产品的类而不是它的子类。这个方法有语言特性的好处,然而纯基于原型的方法是和语言无关的。

就像刚刚讨论的Smalltalk中基于原型的工厂,基于类的版本将有单独的实例变量partCataglog,即一个键值是部分的名字的字典。和存储要拷贝的原型不同,partCataglog存储产品的类。方法像这样:

make: partName
^ (partCatalog at: partName) new
  1. 定义可扩展的工厂。抽象工厂通常为每一个可以生产的产品类型定义不同的操作。产品类别编码在操作签名上。添加新的产品种类需要改变抽象通常的接口以及依赖他们的类。

一个更灵活但是不那么安全的设计就是添加参数到创建对象的操作中。这个参数指定了要创建的对象的类型。可以是类表示,一个整形,一个字符串类型活人任意类型标识产品的类别。实际上这种方法,抽象工厂仅仅需要一个简单的带指定要创建对象的类型的参数的“make”操作就可以了。这是之前讨论的抽象工厂用于基于类型和基于对象的技术。

这个变种用于动态类型语言比如smalltalk比静态类型语言如C++更容易。你可以在C++上使用仅当所有对象有同样的抽象基类或当产品对象可以通过请求他们的客户端被安全的创建出来成为正确的类型。实现章节中的工厂方法展示了在C++中如何实现这些参数化的操作。

但是即使当没有强制的时候,有一个继承问题:所有的产品用同样的抽象工厂接口被返回到客户端,类型为返回值类型。客户端将不能区分或者安全假设观叶类的产品。如果客户端需要执行子类制定的操作,他们不能通过抽象工厂接口集中化。虽然客户端可以通过执行向下转化(比如C++中的动态转化),但不是可实现的或者安全的,因为向下转化可能失败。这是典型的为高灵活性和扩展性接口的交换。

示例代码

我们将应用抽象工厂模式到我们在开始章节中讨论的创建迷宫中。

class MazeFactory {
   public:
       MazeFactory();
       virtual Maze* MakeMaze() const
          { return new Maze; }
       virtual Wall* MakeWall() const
          { return new Wall; }
       virtual Room* MakeRoom(int n) const
          { return new Room(n); }
       virtual Door* MakeDoor(Room* r1, Room* r2) const
          { return new Door(r1, r2); }
};
Maze* MazeGame::CreateMaze (MazeFactory& factory) {
       Maze* aMaze = factory.MakeMaze();
       Room* r1 = factory.MakeRoom(1);
       Room* r2 = factory.MakeRoom(2);
       Door* aDoor = factory.MakeDoor(r1, r2);
       aMaze->AddRoom(r1);
       aMaze->AddRoom(r2);
       r1->SetSide(North, factory.MakeWall());
       r1->SetSide(East, aDoor);
       r1->SetSide(South, factory.MakeWall());
       r1->SetSide(West, factory.MakeWall());
       r2->SetSide(North, factory.MakeWall());
       r2->SetSide(East, factory.MakeWall());
       r2->SetSide(South, factory.MakeWall());
       r2->SetSide(West, aDoor);
       return aMaze;
   }
   
class EnchantedMazeFactory : public MazeFactory {
   public:
       EnchantedMazeFactory();
virtual Room* MakeRoom(int n) const
{ return new EnchantedRoom(n, CastSpell()); }
virtual Door* MakeDoor(Room* r1, Room* r2) const { return new DoorNeedingSpell(r1, r2); }
   protected:
       Spell* CastSpell() const;
};

已知应用

“InterViews”使用“Kit”后缀来表示抽象工厂类。定义了WidgetKit和DialogKit抽象工厂来生成“look-and-feel-specific”用户界面对象。“InterViews”也包括一个LayoutKit,用来生成依赖于布局设计的不同的组合对象。比如,一个layout是一个概念上水平的,可能需要不同组合对象的,依赖于文档方向(横屏或竖屏)。

相关模式

抽象工厂模式经常用工厂方式来实现,也可以用原型模式来实现。

一个具体的工厂通常是一个单例。