理解工厂方法和抽象工厂模式






4.33/5 (47投票s)
通过示例 C++ 代码简化解释工厂方法和抽象工厂模式。
引言
在设计模式中,最常用和最受欢迎的模式是工厂方法模式和抽象工厂模式。对于刚接触设计领域的开发人员来说,这些也是最令人困惑的模式。
本文通过清晰的示例解释了这两个“必知”模式,然后深入探讨它们之间的区别。此外,我们还将了解这两种模式的优缺点。
工厂方法模式
根据“四人组”的说法,工厂方法的意图是:
“定义一个用于创建对象的接口,但由子类决定实例化哪一个类。工厂方法允许类将实例化推迟到子类。”
现在,让我们来理解工厂方法模式的这个定义。但首先,我们应该讨论使用设计模式创建对象时存在的一个问题。
在编程过程中,我们经常会遇到这样的情况:一个类需要包含其他类或类层次结构的对象。这可以通过使用 `new` 关键字和类构造函数来轻松实现。这种方法的缺点是,它是一种非常硬编码的对象创建方式,因为它会在两个类之间产生依赖关系。
我们可以使用工厂模式来解耦这两个类。工厂方法通常实现为虚方法,因此该模式也被称为“虚拟构造函数”。这些方法创建产品或目标类的对象。
此外,代码处理的是产品接口,因此可以与任何用户定义的具体产品类一起使用。因此,代码不会与特定应用程序或类紧密绑定。
未使用工厂模式的代码示例
让我们从解释问题开始,并以此作为解释工厂方法的动机。在这里,我们打算创建一个用户界面 (UI) 框架,该框架由数据组件、显示数据的视图以及用于与视图交互的关联工具栏或大小调整栏组成。
在下面的示例代码中:
- 类 `CUIFrameWork` 是 UI 框架类。
- 类 `CDataComponent`、`CUIComponent` 和 `CToolBarComponent` 是产品。
class CUIFrameWork{
public:
CUITemplate* CreateUI()
{
CDataComponent* pData = new CDataComponent();
CUIComponent* pUI = new CUIComponent();
CToolBarComponent* pTooBar1 = new CToolBarComponent();
CToolBarComponent* pTooBar2 = new CToolBarComponent();
pUI->AddToolBar(pTooBar1);
pUI->AddToolBar(pTooBar2);
return new CUITemplate( pData, pUI );
}
};
问题
现在考虑添加一个新的 UI 组件,它需要是一个滚动视图或者说一个列表视图。可能会出现对其他工具栏或大小调整栏的类似请求。在当前方法中,即使任何一个组件发生变化,我们也需要重新编写创建对象及其绑定的完整代码。
在下面的示例代码中:
- 添加了一个新的具体产品 `CUIComponentScrolling`。
- 但现在,我们的 `CUIFrameWork` 也意识到了这个新的 UI 组件。
class CUIFrameWork
{
public:
CUITemplate* CreateUI()
{
… Same code as in above example
}
CUITemplate* CreateScrollingUI()
{
CDataComponent* pData = new CDataComponent();
CUIComponent* pUI = new CUIComponentScrolling();
CToolBarComponent* pTooBar1 = new CToolBarComponent();
CToolBarComponent* pTooBar2 = new CToolBarComponent();
pUI->AddToolBar(pTooBar1);
pUI->AddToolBar(pTooBar2);
return new CUITemplate( pData, pUI );
}
};
在这里,我们无法重用 `CreateUI()` 方法中编写的任何代码。此外,客户端代码 (`CUIFrameWork`) 需要意识到或与具体产品 `CUIComponentScrolling` 紧密耦合。
解决方案
通过应用工厂方法模式,我们现在编写虚方法来创建产品对象。因此,每当添加新的具体产品并需要在 `CUIFrameWork` 中使用时,我们需要派生一个新的框架类,并通过重写相关的工厂方法来创建具体产品。
在这里,主框架类 (`CUIFrameWork`) 只处理产品接口,并将具体产品的创建任务委托给子类。
在下面的示例代码中:
- 使用的工厂方法是 `MakeDataComp`、`MakeUIComp` 和 `MakeToolBarComp`。
class CUIFrameWork
{
public:
// Instead of hard coding we write factory methods which
// perform the task of object creation.
virtual CDataComponent* MakeDataComp()
{
return new CDataComponent();
}
virtual CUIComponent* MakeUIComp()
{
return new CUIComponent();
}
virtual CToolBarComponent* MakeToolBarComp( UINT nID )
{
return new CToolBarComponent( nID );
}
CUITemplate* CreateUI()
{
CDataComponent* pData = MakeDataComp();
CUIComponent* pUI = MakeUIComp();
CToolBarComponent* pTooBar1 = MakeToolBarComp( ID_STANDARD );
CToolBarComponent* pTooBar2 = MakeToolBarComp( ID_CUSTOM );
pTooBar2->AddDropDownButton();
pTooBar2->AddComboBox();
pUI->AddToolBar(pTooBar1);
pUI->AddToolBar(pTooBar2);
return new CUITemplate( pData, pUI );
}
};
要创建滚动 UI 组件,我们只需创建一个另一个框架类,并重写 `MakeUIComp()` 方法来创建滚动 UI 组件。
class CUIFrameWork_ScrollingUI : public CUIFrameWork
{
public:
virtual CUIComponent* MakeUIComp()
{
return new CUIComponentScrolling();
}
};
类似于更改工具栏组件,我们可以创建一个另一个框架类,并重写 `MakeToolBarComp()` 方法来创建新的工具栏组件。
class CUIFrameWork_SizingBars : public CUIFrameWork
{
public:
virtual CToolBarComponent* MakeToolBarComp( UINT nID )
{
return new CToolBarComponent_SizingBar( nID );
}
};
创建滚动 UI,请参阅下面的示例客户端代码。
// Client Code to create scrolling view.
CUIFrameWork_ScrollingUI objScrollUI;
CUITemplate* pTemplate = objScrollUI.CreateUI();
优点
- 工厂方法消除了将应用程序特定类绑定到代码中的需要。
- 代码只处理产品接口;因此,它可以与任何用户定义的具体产品类一起使用。
- 工厂方法为子类提供了创建不同具体产品的钩子。在下面的示例中,工厂方法 `MakeUISpecificCtrls` 提供了创建 UI 组件特定控件的钩子。在默认的 `CUIComponent` 中,会创建一个简单的编辑控件;但是,我们可以在派生类中更改此行为,以创建一个仅接受浮点值的编辑控件。
- 工厂方法以一种方式连接并行类层次结构,即局部化了哪些类属于一起的知识。在下面的示例中,`CUIComponent` 和 `CEditCtrl` 类层次结构可以相互连接。同时请注意工厂方法如何定义这两个类层次结构之间的连接。
在下面的示例代码中:
- 用于创建 UI 特定控件的工厂方法是 `MakeUISpecificCtrls()`。
- 这提供了一个钩子,以便我们可以派生一个子类来创建用于显示数据的不同控件。
- 此外,工厂方法以最小的耦合连接类层次结构。
class CUIComponent
{
public:
virtual void MakeUISpecificCtrls()
{
CEditCtrl* pEdit = new CEditCtrl();
}
};
class CUIComponent_NewView : public CUIComponent
{
public:
void MakeUISpecificCtrls()
{
CEditCtrl* pEdit = new CEditNumericCtrl();
}
};
缺点
- 工厂方法的一个潜在缺点是,客户端可能必须子类化创建者类才能创建特定的具体产品对象。
- 当客户端必须子类化创建者类时,子类化是可以的,但否则,客户端现在必须处理另一个演进点。
- 在工厂方法模式中,用于创建对象的工厂与客户端代码绑定,即,很难使用不同的工厂来创建对象。
抽象工厂模式
根据“四人组”的说法,抽象工厂方法的意图是:
“为创建相关或依赖对象的系列提供一个接口,而无需指定它们的具体类。”
抽象工厂是一个具有多个工厂方法的类。所以,基本上,一个抽象工厂类拥有一系列不同的工厂方法来创建不同的所需具体对象。
每个具体工厂子类都实现了特定产品族的工厂方法。抽象工厂模式可以看作是工厂方法模式的扩展。
* 抽象工厂与工厂方法模式有何不同?
Factory Method
客户端期望实现一个接口或抽象类,但不知道工厂将返回哪个具体类。
抽象工厂
这里,还有一个抽象级别。客户端甚至不知道它将使用哪个工厂。首先,它获得一个工厂,然后调用一个工厂方法。以下示例将对此进行说明。
在各种知名工具包和库中都可以找到此模式的几个示例。在深入研究并创建自己的抽象工厂之前,让我们看一个示例。
示例 1
在 COM 中,`IClassFactory` 接口用于创建 co-classes 的实例;这是抽象工厂模式的一个示例。
COM 使用类工厂遵循的步骤
- 客户端首先调用 `CoCreateInstance`,它在 COM 库中实现。`CoCreateInstance` 内部调用 `CoGetClassObject`。
- `CoGetClassObject` 在 Windows 注册表中查找组件。
- 如果它在注册表中找到组件,它将加载提供该组件的关联 DLL。
- 加载 DLL 后,`CoGetClassObject` 调用 `DllGetClassObject`。`DllGetClassObject` 在 DLL 服务器中实现。
- `DllGetClassObject` 会创建类工厂,它使用 `new` 运算符来完成。
- `DllGetClassObject` 然后查询类工厂的 `IClassFactory` 接口,该接口返回给 `CoCreateInstance`。
- `CoCreateInstance` 然后使用 `IClassFactory` 接口调用其 `CreateInstance` 函数。
- 在这里,`IClassFactory::CreateInstance` 调用 `new` 运算符来创建组件。
- 此外,它还会查询 `IUITemplate` 接口。
- 获取接口后,`CoCreateInstance` 释放类工厂并将 `IUITemplate` 接口指针返回给客户端。
- 客户端现在可以使用接口指针调用组件上的方法。
示例 2
抽象工厂模式通常使用组合将对象实例化委托给另一个对象;即 `CreateUI()` 方法获取抽象工厂类的引用。
class CFrameWorkFactory_Abs
{
public:
virtual CDataComponent* MakeDataComp() = 0;
virtual CUIComponent* MakeUIComp() = 0;
virtual CToolBarComponent* MakeToolBarComp( UINT nID ) = 0;
};
class CFrameWorkFactory : public CFrameWorkFactory_Abs
{
virtual CDataComponent* MakeDataComp()
{
return new CDataComponent();
}
virtual CUIComponent* MakeUIComp()
{
return new CUIComponent();
}
virtual CToolBarComponent* MakeToolBarComp( UINT nID )
{
return new CToolBarComponent( nID );
}
};
用于创建滚动 UI 的具体工厂类
class CFrameWorkFactory_ScrollingUI : public CFrameWorkFactory
{
public:
virtual CUIComponent* MakeUIComp()
{
return new CUIComponentScrolling();
}
};
class CUIFrame
{
public:
CUITemplate* CreateUI(CFrameWorkFactory_Abs& objFactory)
{
CDataComponent* pData = objFactory.MakeDataComp();
CUIComponent* pUI = objFactory.MakeUIComp();
CToolBarComponent* pTooBar1 = objFactory.MakeToolBarComp( ID_STANDARD );
CToolBarComponent* pTooBar2 = objFactory.MakeToolBarComp( ID_CUSTOM );
pTooBar2->AddDropDownButton();
pTooBar2->AddComboBox();
pUI->AddToolBar(pTooBar1);
pUI->AddToolBar(pTooBar2);
return new CUITemplate( pData, pUI );
}
};
因此,每当我们想要更改 UI 框架的行为时,都需要传递相关的工厂对象。请参阅下面的示例客户端代码。
创建滚动视图的示例客户端代码如下:
- 首先获取所需的工厂。
- 然后,调用工厂方法来创建对象。
- 这里的客户端函数 `CreateUI` 并不知道实际使用了哪个工厂来实例化对象。
- 使用组合而非继承。
CFrameWorkFactory_ScrollingUI objScrollFactory;
CUIFrame objUIFrame;
CUITemplate* pTemplate = objUIFrame.CreateUI(objScrollFactory);
优点
- 它将具体类与客户端隔离。
- 您使用抽象工厂来控制客户端创建的对象的类。
- 产品名称在具体工厂的实现中是隔离的,客户端通过它们的抽象接口使用实例。
- 交换产品族很容易。
- 客户端代码不会因抽象接口不经常更改而中断。
- 由于抽象工厂创建一个完整的产品族,当具体工厂更改时,整个产品族也会随之更改。
- 它促进了产品之间的一致性。
- 由具体工厂负责确保正确的产品一起使用。
缺点
- 添加新产品需要扩展抽象接口,这意味着其所有派生具体类也必须更改。需要处理以下更改:
- 添加新的抽象产品类
- 添加新的产品实现
- 扩展抽象工厂接口
- 派生的具体工厂必须实现扩展
- 客户端需要扩展才能使用新产品
如何定义可扩展工厂
为了克服抽象工厂模式的上述缺点,我们可以使用以下方法:
我们可以向创建对象的*操作*添加一个参数。此参数指定要创建的对象类型。它可以是类标识符、整数、字符串或任何其他标识产品类型的标识符。实际上,使用此方法,抽象工厂只需要一个带有*指示要创建的对象类型的参数*的“Make”操作。
这是原型模式和前面讨论的基于类的抽象工厂中使用的技术。
这被认为是一种更灵活但不太安全的设计。
结论
创建模式在您的软件对象创建方式上提供了极大的灵活性。清晰理解这些简单的入门模式及其优缺点,对于高效地扩展和维护应用程序非常有帮助。
这些模式可以用作学习其他强大但复杂的创建模式的第一步。