设计模式第二部分 -工厂模式






4.80/5 (71投票s)
这是WebBiscuit设计模式系列的第二部分,着重探讨工厂模式。我们将通过一个示例来研究如何通过面向接口编程来应对不断变化的需求,并让工厂模式来决定具体实现。
引言
这是一个探讨设计模式并用 C# 创建实用示例的系列文章,我们将把这些模式付诸实践。本系列文章面向对面向对象知识有良好了解、对设计模式感到好奇但没有先验知识的程序员。这一次,我们将在工厂中工作并封装对象的创建。
工厂模式
在使用设计模式时,建议您面向接口编程,避免像坏掉的饼干一样使用特定实现。唉,在程序的某个时刻,您确实需要在某个地方创建一个具体对象,否则程序就无法做任何事情。这就是工厂模式的优势所在,因此它成为一个非常常见的模式,也是您需要了解的模式。
本文附带的可选源文件可以从本文顶部的链接下载。
定义
工厂模式的一个常见引用定义是:“工厂方法模式定义了一个用于创建对象的接口,但让子类决定实例化哪个类。”1 但正如您将看到的,它的范围要广得多。您将看到许多不严格符合此定义的工厂模式实现,我们将在本文中探讨两种变体。
基本上,工厂模式允许我们面向接口编程,只在需要时才插入实际实现。由于鼓励接口编程,工厂模式是一种基本的设计模式,并且是良好软件实现背后的真正主力。
实践中
在现实世界中,需求很少保持不变。那个为支持特定系统而设计的漂亮框架最终可能会支持它最初未曾设想的任务;这被称为“撬棍式编码”。在我们的代码库变得庞大、蔓延且无法使用之前,一些前瞻性的思考和设计可以极大地帮助我们。多态编码可以抵御多态需求,而工厂模式在此大放异彩。
问题
我们公司,“Awesome Software With Biscuits Unlimited Ltd.”(无限饼干的超棒软件有限公司)遇到了财务困难(部分原因是办公用品成本),现在轮到我们的老板伯尼·霍布诺布想出一个惊人的主意了。他连续三周抓耳挠腮、涂鸦着记事本,然后想出了一个不同的主意。我们要暂时停止在网站上销售饼干,转而销售印有我们标志的狗衣服。伯尼在给中国打电话时,告诉我们着手修改网站。我们必须支持销售一种全新的产品。
初次尝试
我们打开源代码,找到以下代码,它负责控制我们网站的显示并将一些产品打印到屏幕上:2
protected void Page_Load(object sender, EventArgs e)
{
lblTitle.Text = "Biscuits For Sale";
lblTitle.BackColor = Color.LightCoral;
IEnumerable<string> biscuits = new List<string>() {
"Hob Nob",
"Custard Creams",
"Chocolate Digestives" };
lstProducts.DataSource = biscuits;
lstProducts.DataBind();
IEnumerable<string> shippingLocations = new List<string>() {
"England",
"Australia",
"Kazakhstan" };
lstShipping.DataSource = shippingLocations;
lstShipping.DataBind();
}
源代码中的 Original.aspx.cs 文件。
代码看起来足够简单。它正在更改标签、标签颜色,然后填充两个列表框;一个用于产品,一个用于配送目的地。
伯尼模糊的需求表明,新产品类型将具有不同的标题、颜色和产品,因此需要在某个地方进行决策和分支。
以下是不可取之举
// Attempt 1. Don't do this!
protected void Page_Load(object sender, EventArgs e)
{
string product = "clothing";
if (product == "biscuit")
{
lblTitle.Text = "Biscuits For Sale";
lblTitle.BackColor = Color.LightCoral;
IEnumerable<string> biscuits = new List<string>() {
"Hob Nob",
"Custard Creams",
"Chocolate Digestives" };
lstProducts.DataSource = biscuits;
}
else if (product == "clothing")
{
lblTitle.Text = "Dog Clothes For Sale";
lblTitle.BackColor = Color.LightGreen;
IEnumerable<string> clothes = new List<string>() {
"Branded cap",
"Paw warmers",
"Furry pants" };
lstProducts.DataSource = clothes;
}
lstProducts.DataBind();
IEnumerable<string> shippingLocations = new List<string>() {
"England",
"Australia",
"Kazakhstan" };
lstShipping.DataSource = shippingLocations;
lstShipping.DataBind();
}
源代码中的 Attempt1.aspx.cs 文件。
虽然这段代码有效,我们可以通过更改 `string`(或 `enum`,或产品 ID 等)轻松切换两种产品类型,但看到这样的代码应该会让你感到不适,就像吃了太多饼干的感觉一样。想象一下,如果伯尼下周想尝试销售可食用餐具。`if`/`else` 分支会再长出一条肢体。然后他还会要求文本的前景色也取决于产品类型。我们必须将 `lblTitle.ForeColor` 添加到每个分支中,我们的工作变成了与可怕的 `if`/`else` 章鱼共舞的吃力不讨好的任务。快叫键盘医生!Ctrl、C 和 V 需要更换了!
一种设计模式飞驰而来,解救了危机。
工厂模式:首次尝试
“封装变化的代码”是设计模式的理想口号,这就是我们实现它的机会。在我们的问题中,哪些东西在变化?标题、背景颜色和产品。封装它。如何封装?我们此时并不关心,所以这指向了我们老朋友(却漠不关心的)——接口。
public interface IProduct
{
string Title{ get; }
Color BackgroundColour{ get; }
IEnumerable<string> Products { get; }
}
运输地点将来似乎可能会改变,但我们暂时忽略这一点。最坏的情况会怎样?
所以现在我们可以将变化的数据移到它们自己的子类中,实现接口以适应它们各自的特定需求。
public class Biscuits : IProduct
{
#region IProduct Members
public string Title
{
get { return "Biscuits For Sale"; }
}
public Color BackgroundColour
{
get { return Color.LightCoral; }
}
public IEnumerable<string> Products
{
get
{
return new List<string>() {
"Hob Nob",
"Custard Creams",
"Chocolate Digestives" };
}
}
#endregion
}
public class Clothing : IProduct
{
#region IProduct Members
public string Title
{
get { return "Dog Clothes For Sale"; }
}
public Color BackgroundColour
{
get { return Color.LightGreen; }
}
public IEnumerable<string> Products
{
get
{
return new List<string>() {
"Branded cap",
"Paw warmers",
"Furry pants" };
}
}
#endregion
}
我们的控制类现在变得更加简单,仅限于 IProduct 接口。
// Attempt 2. Much better, but not yet perfect
protected void Page_Load(object sender, EventArgs e)
{
IProduct product = GetProduct("clothing");
lblTitle.Text = product.Title;
lblTitle.BackColor = product.BackgroundColour;
IEnumerable<string> products = product.Products;
lstProducts.DataSource = products;
lstProducts.DataBind();
IEnumerable<string> shippingLocations = new List<string>() {
"England",
"Australia",
"Kazakhstan" };
lstShipping.DataSource = shippingLocations;
lstShipping.DataBind();
}
这里的关键行是 `IProduct product = GetProduct("clothing")`。我们已经抽象掉了所有具体数据,并从主代码中移除了所有对象实例化。剩下的就是工厂方法本身了。
public IProduct GetProduct(string type)
{
if (type == "biscuit")
{
return new Biscuits();
}
else if (type == "clothing")
{
return new Clothing();
}
else
{
// Returning a default
// or throw exception, or panic
return new Biscuits();
}
}
源代码中的 Attempt2.aspx.cs 文件。
这之所以有效,是因为我们利用了多态的力量,方法签名保证我们返回的是 `IProduct` 类型的对象,但没有确切说明是哪一个。在方法内部,创建的对象是此接口的特化,我们确实创建并返回具体的饼干(好吃!)、服装,或我们将来决定支持的任何其他 `IProduct`。
通常讨论到此为止。大多数人,包括撰写本文时的维基百科,都将此称为工厂设计模式。虽然它确实是一种设计模式,并且在大多数情况下相当适用,但它不符合本页顶部所述的原始定义,最糟糕的是,它违背了基本的设计模式哲学。
它为何足够?好吧,我们已将变化的程序代码移至此函数中。如果需要创建新产品,我们添加一个新的 `else if` 分支,创建一个新的子类并实例化它。这可行,但违反了创建可重用软件的一个关键目标:**代码应开放扩展但封闭修改。** 修改运行良好的代码总是伴随着代码更改后可能不再工作的风险。这段代码绝对不是封闭修改的。想象一下,我们将我们的框架出售给其他公司,但我们希望保护我们的源代码。该公司无法添加新的产品类型,因为他们无法进入我们的 `GetProduct` 函数。我们别无选择,只能放弃我们源代码的核心部分,或为他们修改它,这两种情况都不是理想的。
如果这不能说服你,那么这个可能会。澳大利亚爆发的慢性“饼干病”要求立即禁止所有进口饼干。这迫使我们改变特定产品的运输方式,导致出现这样的代码:
...
IEnumerable<string> shippingLocations = GetShippingLocations("biscuit");
...
public IEnumerable<string> GetShippingLocations(string type)
{
if (type == "biscuit")
{
return new List<string>() {
"England",
"Kazakhstan" };
}
else
{
// Returning a default
return new List<string>() {
"England",
"Australia",
"Kazakhstan" };
}
}
没错,现在我们必须饲养和维护两个这样的 `if`/`else` 怪物。
让我们看看工厂模式究竟是什么。
工厂模式:最终尝试
工厂模式实际上是在我们现有基础上增加了一个抽象层。我们创建一个 `abstract` 类或接口来定义工厂,然后实例化派生工厂,这些工厂精确地说明了如何以及创建哪些对象。
这是围绕我们问题的工厂,还包含一个辅助函数,说明我们可以将产品运往何处
public abstract class ProductFactory
{
public abstract IProduct CreateProduct();
public virtual IEnumerable<string> ShippingLocations()
{
return new List<string> {
"England",
"Australia",
"Kazakhstan" };
}
}
服装工厂的简单实现返回服装产品
public class ClothingFactory : ProductFactory
{
public override IProduct CreateProduct()
{
return new Clothing();
}
}
对于饼干,我们可以做类似的事情,但请记住,由于健康风险,有些地方已经禁止了我们的饼干。我们可以通过在工厂中重写基函数来轻松实现产品特定行为。
public class BiscuitFactory : ProductFactory
{
public override IProduct CreateProduct()
{
return new Biscuits();
}
public override IEnumerable<string> ShippingLocations()
{
return new List<string> {
"England",
"Kazakhstan" };
}
}
我们的主函数现在移除了创建器辅助函数,转而将所有产品创建的责任委托给工厂。
protected void Page_Load(object sender, EventArgs e)
{
ProductFactory factory = new BiscuitFactory();
IProduct product = factory.CreateProduct();
lblTitle.Text = product.Title;
lblTitle.BackColor = product.BackgroundColour;
IEnumerable<string> products = product.Products;
lstProducts.DataSource = products;
lstProducts.DataBind();
// Use helper custom function within factory
lstShipping.DataSource = factory.ShippingLocations();
lstShipping.DataBind();
}
源代码中的 Attempt3.aspx.cs 文件。
现在维护这个软件只需要从 `IProduct` 派生一个产品,并从 `ProductFactory` 派生一个产品工厂来返回这个产品。我们还提供了根据需要重写辅助函数的额外功能。
这有什么帮助?
敏锐的读者可能已经意识到我们回到了一个根本问题。看这一行:
ProductFactory factory = new ClothingFactory();
我们正在使用 new 关键字并实例化一个对象!我们创建工厂的初衷就是为了消除具体对象的创建!这难道是徒劳无功的吗?
不。要使软件工作,您无法回避必须在某个地方创建真实对象的事实。您的代码必须有一个入口点。想象一下,我们创建了一个我们希望锁定的框架,但又希望允许其他开发人员扩展。在我们的 API 中,我们可以公开一个名为 `SetFactory(...)` 的函数,它接受一个从 `AbstractFactory` 派生的类,这允许开发人员创建这个类的派生类并将其传递到我们封闭的框架中。然后,框架通过其已知接口使用这个未知类来创建所有对象。如果您理解了这一点,那么您就已经走上了学习开闭原则的道路——对象开放扩展但封闭修改。
使用工厂模式的其他好处
- 您可以创建一个测试工厂,例如一个创建并返回假数据而不是使用实时数据并修改实时数据库的工厂。这样您就可以模拟传统上昂贵且危险的任务——例如添加新用户或测试购物流程。在测试用户界面时换上您的模拟工厂,并在上线时换回来。
- 您可以轻松更改应用程序的外观、感觉和行为。您可以为您的应用程序编写多个前端。使用工厂模式作为您的 GUI 换肤的基础。您还可以使用该模式来针对不同的技术;例如 WinForms、WebForms 和 WPF。
- 工厂可以封装复杂的对象创建过程。如果一个类的构造函数需要许多参数,您可以使用不同的工厂以不同的方式封装同一个类,从而为您提供更简单的 API。
- 您可以将工厂类作为函数的参数传递,以在运行时改变其行为。如果这个概念对您来说很陌生,那么您一定错过了第一部分,策略模式!
替代实现
工厂模式以各种形式反复出现。如果您正在通过子类化来改变程序中的对象创建,那么您就拥有一个工厂。还有抽象工厂,它们本身就值得拥有一个设计模式。这些基本上是在工厂模式之上更深一层的抽象,创建的类具有可重写的方法,用于定义要返回的工厂。确实非常强大。
结论
工厂模式是保护源代码内部工作原理的绝佳模式。编写您的代码以与接口协同工作,然后让客户端定义负责创建符合这些接口的对象的工厂。您为应用程序提供的定制和可修改性几乎是无限的。
网站完成后不久,伯尼已将下一款产品投入生产:咖啡味回形针。工厂模式万岁!
2010年2月7日
注释和参考文献
延伸阅读
- Gamma, Helm, Johnson & Vlissides (1994)。设计模式(四人帮著作)。Addison-Wesley。ISBN 0-201-63361-2
- 维基百科关于工厂模式的文章
- 设计模式1:策略模式