TL;博士
维基百科以外的(Ninject)示例真的遵循抽象工厂模式吗?
在概念上,是的,像 Ninject 这样的 IoC 容器允许(本着)Abstract Factory的原始目标(以及更多) ,但在实施中,不,使用像 Ninject 这样的 IoC 容器的现代应用程序不需要无数具体的工厂类——通常除了new()
构建它们的类型的具体实例之外什么都不做——尤其是在像 JVM 和托管 .Net 这样的垃圾收集环境中使用时。
IoC 容器具有反射、工厂函数/lambda 甚至动态语言等工具来创建具体类。这包括允许额外的创建策略,例如允许对参数和调用上下文进行区分。
我建议不要专注于 GoF 模式的原始代码类实现,而是关注每个 GoF 模式的高级概念,以及每个模式旨在解决的问题。
基本原理
许多四人组模式(如Abstract Factory
)经常被现代语言和框架吸收或简化 - 即自 1990 年代中期以来的进化语言和设计改进在许多情况下意味着可以实现核心 GoF 模式概念更简洁,在某些情况下可能会使原始 GoF 书中的一些代码和 UML 类变得多余。
例如在 C# 中,
Iterator
经常被直接并入编译器 ( foreach / GetEnumerator()
)
Observer
标配多播委托和事件等。
- 使用
Singleton
,而不是使用静态实例化,我们通常会使用 IoC 来管理单例。是否使用惰性实例来管理生命周期的决定将完全是一个单独的问题。(我们有Lazy<T>
这个,包括处理GoF中没有预见到的线程安全问题)
- 我相信在许多情况下,当 IoC 容器可用时,情况也是
Abstract Factory
如此Factory Method
。
然而,所有 GoF 设计模式的概念在今天仍然和以往一样重要。
对于各种创新的 GoF 模式,在编写 Gang of Four 书时,像 Ninject 这样的 IoC 容器还没有在主流中广泛使用。此外,90 年代中期的大多数语言都没有垃圾收集 - 因此,依赖于其他的类(“依赖类”)必须管理依赖项的解析和控制生命周期,这可能有助于解释为什么显式工厂在 90 年代比今天更普遍。
这里有一些例子:
如果工厂仅用于抽象创建,和/或允许可配置的策略来解决单个依赖项,并且不需要特殊的依赖项生命周期控制,则可以完全避免使用工厂并将依赖项留给 IoC容器来建立。
例如,在 OP 提供的 Wiki 示例中,是否构建 aWinFormsButton
或 an的策略(决定)很可能OSXButton
是一个应用程序配置,该配置在应用程序进程的生命周期内是固定的。
GoF 风格示例
对于 Windows 和 OSX 实现,需要ICanvas
和ICanvasFactory
接口,以及额外的 4 个类 - OSX 和 Windows Canvasses,以及两者的 FactoryClasses。策略问题,即解决哪个 CanvasFactory 也需要解决。
public class Screen
{
private readonly ICanvas _canvas;
public Screen(ICanvasFactory canvasFactory)
{
_canvas = canvasFactory.Create();
}
public ~Screen()
{
// Potentially release _canvas resources here.
}
}
现代 IoC 时代的简单工厂方法示例
如果不需要在运行时动态确定具体类的决定,则可以完全避免工厂。依赖类可以简单地接受依赖抽象的实例。
public class Screen
{
private readonly ICanvas _canvas;
public Screen(ICanvas canvas)
{
_canvas = canvas;
}
}
然后所需要的就是在 IoC 引导中配置它:
if (config.Platform == "Windows")
// Instancing can also be controlled here, e.g. Singleton, per Request, per Thread, etc
kernel.Bind<ICanvas>().To<WindowsCanvas>();
else
kernel.Bind<ICanvas>().To<OSXCanvas>();
因此,我们只需要一个接口,加上两个具体WindowsCanvas
和OSXCanvas
类。该策略将在 IoC 引导中解决(例如 Ninject Module.Load
) Ninject 现在负责ICanvas
注入依赖类的实例的生命周期。
抽象工厂的 IoC 替换
然而,在现代 C# 中仍然存在一些情况,其中一个类仍然需要一个依赖工厂,而不仅仅是一个注入的实例,例如
- 当要创建的实例数量未知/动态确定时(例如,一个
Screen
类可能允许动态添加多个按钮)
- 当依赖类不应该有一个延长的生命周期 - 例如,释放由创建的依赖所拥有的任何资源很重要(例如依赖实现
IDisposable
)
- 当依赖实例的创建成本很高,并且实际上可能根本不需要时 - 请参阅 Lazy Initialization patterns like Lazy
即便如此,使用 IoC 容器也有一些简化,可以避免多个工厂类的扩散。
抽象工厂接口(例如GUIFactory
在 Wiki 示例中)可以简化为使用 lambda Func<discriminants, TReturn>
- 即因为工厂通常只有一个公共方法Create()
,因此不需要构建工厂接口或具体类。例如
Bind<Func<ButtonType, IButton>>()
.ToMethod(
context =>
{
return (buttonType =>
{
switch (buttonType)
{
case ButtonType.OKButton:
return new OkButton();
case ButtonType.CancelButton:
return new CancelButton();
case ButtonType.ExitButton:
return new ExitButton();
default:
throw new ArgumentException("buttonType");
}
});
});
抽象工厂可以替换为Func resolver
public class Screen
{
private readonly Func<ButtonType, IButton> _buttonResolver;
private readonly IList<IButton> _buttons;
public Screen(Func<ButtonType, IButton> buttonResolver)
{
_buttonResolver = buttonResolver;
_buttons = new List<IButton>();
}
public void AddButton(ButtonType type)
{
// Type is an abstraction assisting the resolver to determine the concrete type
var newButton = _buttonResolver(type);
_buttons.Add(newButton);
}
}
尽管在上面,我们只是简单地使用了 anenum
来抽象创建策略,但 IoC 框架允许以多种方式指定具体创建“区分”的抽象,例如通过命名抽象、通过属性(不推荐 -这污染了依赖代码),与上下文相关联,例如通过检查其他参数或依赖类类型等。
还值得注意的是,IoC 容器也可以在依赖项本身还具有需要解析的其他依赖项时提供帮助(可能再次使用抽象)。在这种情况下,new
可以避免并通过容器再次解决每个按钮类型的构建。例如,上面的引导代码也可以指定为:
case ButtonType.ExitButton:
return KernelInstance.Get<OkButton>();