5

我们核心产品的一部分是网站 CMS,它使用各种页面小部件。这些小部件负责显示内容、列出产品、处理事件注册等。每个小部件由派生自基本小部件类的类表示。渲染页面时,服务器从数据库中获取页面的小部件,然后创建正确类的实例。工厂方法对吗?

Private Function WidgetFactory(typeId)
    Dim oWidget
    Select Case typeId
        Case widgetType.ContentBlock
            Set oWidget = New ContentWidget
        Case widgetType.Registration
            Set oWidget = New RegistrationWidget
        Case widgetType.DocumentList
            Set oWidget = New DocumentListWidget
        Case widgetType.DocumentDisplay
    End Select
    Set WidgetFactory = oWidget
End Function

无论如何,这一切都很好,但随着时间的推移,小部件类型的数量已增加到大约 50 种,这意味着工厂方法相当长。每次我创建一种新类型的小部件时,我都会在该方法中添加另外几行代码,然后我的脑海中就会响起一个小警报,这可能不是最好的做事方式。我倾向于忽略那个警报,但它越来越响亮。

那么,我做错了吗?有没有更好的方法来处理这种情况?

4

7 回答 7

15

我认为您应该问自己的问题是:为什么我在这里使用工厂方法?

如果答案是“因为 A ”,并且A是一个很好的理由,那么继续这样做,即使这意味着一些额外的代码。如果答案是“我不知道;因为我听说你应该这样做? ”那么你应该重新考虑。

让我们回顾一下使用工厂的标准原因。以下是Wikipedia关于工厂方法模式的说明:

[...],它处理创建对象(产品)的问题,而不指定将要创建的对象的确切类别。工厂方法设计模式通过定义一个单独的方法来创建对象来处理这个问题,然后可以覆盖其子类以指定将要创建的产品的派生类型。

由于您的WidgetFactoryPrivate,这显然不是您使用此模式的原因。“工厂模式”本身呢(与您是使用工厂方法还是抽象类实现无关)?同样,维基百科说

在以下情况下使用工厂模式:

  • 对象的创建排除了重用,而无需大量重复代码。
  • 对象的创建需要访问不适合包含在组合对象中的信息或资源。
  • 创建对象的生命周期管理需要集中化以确保行为一致。

从您的示例代码来看,这看起来并不符合您的需要。因此,问题(只有您可以回答)是:(1)您将来需要为您的小部件使用集中式工厂功能的可能性有多大,以及(2)将所有东西改回如果您将来需要它,工厂方法?如果两者都很低,您可以暂时放心地放弃 Factory 方法。


编辑:在这个通用的阐述之后,让我回到你的特殊情况:通常,它是a = new XyzWidget()vs. a = WidgetFactory.Create(WidgetType.Xyz)。但是,在您的情况下,您有一些(数字?)typeId来自数据库。正如马克正确写的那样,您需要在某处typeId -> className拥有这张地图。

因此,在这种情况下,使用工厂方法的充分理由可能是:“无论如何我都需要某种巨大的ConvertWidgetTypeIdToClassNameselect-case 语句,因此使用工厂方法不需要额外的代码,而且它免费提供工厂方法的优势,如果我需要它们的话。”

作为替代方案,您可以将小部件的类名存储在数据库中(您可能已经有一些WidgetType带有主键的表typeId,对吧?)并使用反射创建类(如果您的语言允许这种类型的东西)。这有很多优点(例如,您可以使用新的小部件放入 DLL,而不必更改核心 CMS 代码)但也有缺点(例如,在编译时未检查数据库中的“魔术字符串”;可能的代码注入,取决于谁可以访问该表)。

于 2009-12-17T09:55:52.540 回答
5

WidgetFactory 方法实际上是从 typeId 枚举到具体类的映射。一般来说,最好完全避免枚举,但有时(特别是在 Web 应用程序中)您需要往返于不了解多态性的环境(例如浏览器),并且您需要此类措施。

重构很好地解释了为什么 switch/select case 语句是代码异味,但这主要解决了您有许多类似开关的情况。

如果您的 WidgetFactory 方法是您打开该特定枚举的唯一位置,我会说您不必担心。您需要在某处拥有该地图。

作为替代方案,您可以将地图定义为字典,但代码行的数量不会显着减少 - 您可以将代码行减少一半,但复杂程度将保持不变。

于 2009-12-17T09:57:08.997 回答
4

您对工厂模式的应用是正确的。您有指示创建 N 种类型中的哪一种的信息。工厂知道如何做到这一点。(作为私有方法有点奇怪。我希望它在IWidgetFactory接口上。)

但是,您的实现将实现与具体类型紧密耦合。如果您改为映射typeId -> widgetType,则可以使用Activator.CreateInstance(widgetType)使工厂了解任何小部件类型。

现在,您可以根据需要定义映射:简单的字典、发现(属性/反射)、配置文件等。您必须在某个地方知道所有类型,但您也可以选择组合多个来源。

于 2009-12-18T20:00:44.033 回答
2

实现工厂的经典方法不是使用巨型开关或 if-ladder,而是使用将对象类型名称映射到对象创建函数的映射。除此之外,这允许在运行时修改工厂。

于 2009-12-17T10:04:29.660 回答
2

不管它是否正确,我一直认为使用工厂的时间是在决定创建什么对象类型时将基于直到运行时才可用的信息。

您在后续评论中指出小部件类型存储在数据库中。由于您的代码直到运行时才知道将创建哪些对象,我认为这是对工厂模式的完全有效的使用。通过拥有工厂,您可以让您的程序推迟决定使用哪种对象类型,直到可以实际做出决定为止。

于 2009-12-18T19:39:04.313 回答
1

根据我的经验,工厂会增长,因此它们的依赖项不必增长。如果您看到此映射在其他地方重复出现,那么您有理由担心。

于 2009-12-21T22:32:13.600 回答
0

尝试分类您的小部件,可能基于它们的功能。如果它们中的几个在逻辑上相互依赖,则使用单一结构创建它们

于 2009-12-17T09:52:52.500 回答