为什么更喜欢模板方法而不是依赖注入?

9

我一直在阅读Gamma等人的《设计模式》。我对模板方法与依赖注入有一个问题。

使用模板方法,您可以将策略“模板化”,以提供所需操作或计算的替代方案。因此,与从几个可选项中选择一个策略并将其编码到类中不同,您允许类的用户指定要使用的替代方案。

这听起来对我来说非常合理。但是,我遇到了一个概念上的障碍。

如果您使用策略对象实例化一个类,则策略对象需要实现抽象接口。程序员可以编写不同的策略,所有这些策略都可以编译成类而不会出错,因为这些策略实现了接口。使用这些策略的类是根据策略接口而不是实现进行编码的。

如果您要为这些策略对象定义一个抽象的IPolicy,为什么不直接使用依赖注入,并在构建时传递IPolicy呢?

有人能解释一下为什么你会更喜欢模板方法而不是依赖注入吗?


1
在构造函数中传递策略就是依赖注入。我不明白你的问题。 - Björn Pollex
是的,这就是我在问题中所说的。但是,“模板方法”与DI不同,但据我所见,它需要DI(编译时)才能正常工作。因此,问题很简单,为什么更喜欢使用TM而不是DI? - Robinson
1
这里没有偏好。您只需使用 DI 实现模板方法模式即可。您不偏好其中任何一种,而是两者都使用。 - Björn Pollex
1
回答者请注意: "模板方法模式"与C ++模板完全无关。这只是名称的巧合。 - Steve Jessop
@Robinson:没错,“策略模式设计”是给一种基本上使用模板来实现策略模式版本的东西起的名字。这与模板方法模式密切相关。大多数旨在运行时多态性方面发明的设计模式都可以使用C++中的静态多态性来实现,模板方法并不比其他模式更多。 - Steve Jessop
显示剩余3条评论
3个回答

16

关于“模板方法”(而不是设计模式),下面的例子可能有助于决定该如何做出利弊权衡。这个例子是为了创建一个旨在帮助调试/开发的库的冗长模式。

使用模板

struct console_print
{
  static void print(const string& msg) {std::cout<<msg;}
};

struct dont_print
{
  static void print(const string& msg) {}
};

template<printer>
void some_function()
{
  printer::print("some_function called\n");
}

图书馆用户可以这样写:
some_function<console_print>(); //print the verbose message;
some_function<dont_print>();    //don't print any messages.

这段代码的好处在于,如果用户不想打印代码,则对 dont_print::print(msg) 的调用将从代码中完全消失(空的静态类很容易被优化掉)。这样的调试信息甚至可以输入到性能关键区域。

模板的缺点是需要在编译前决定策略。您还需要更改任何模板的函数/类签名。

没有模板

当然,以上内容也可以通过以下方式完成:

struct printer 
{
  virtual void print(const std::string& msg) = 0;
}

struct console_print : public printer
{
  void print(const std::string& msg) {std::cout<<msg;}
}
struct debug_print : public printer
{
  void print(const std::string& msg) {}
}

这样做的好处是可以将打印机类型传递给你的类和函数,并在运行时更改它们(对某些应用程序非常有用)。但是,代价是总是调用虚函数,因此空的dont_print会有一些成本。这可能或可能不可接受对于性能关键的区域。

啊,好的,我现在明白它的好处了。这确实很有用。 - Robinson

7
首先,正如phresnel所提到的,模板方法模式不是现代意义上的模板。再读一遍,它使用运行时多态性来实现STL算法使用编译时多态性(函数模板)相同的目标。
其次,所有类型的多态性在某种意义上都是依赖注入。也就是说,调用者将算法引入作用于其上的具体类型中。因此,问题通常不是你是否应该使用依赖注入而是另一种模式:相反,该模式展示了一种有用的方式来构建代码以使用依赖注入。
如果您的“注入的依赖项”是模板类型参数,则算法使用鸭子类型,您无需实现抽象接口:只需编写具有预期签名的方法即可。

好的。但是没有办法将预期的签名正式化。基本上你只能阅读文档。 - Robinson
@Robinson:对于那些选择使用鸭子类型(和C++中的泛型编程)的人来说,缺乏明确的形式接口定义被认为是一种优势;而对于不这样做的人来说,则是一种弱点。曾经尝试将模板的显式声明接口引入到C++11中(称为“概念”),但由于尚未准备好,最终放弃了该提案。 - Steve Jessop
有趣的观点,史蒂夫。我想知道为什么这会被认为是一种弱点?考虑到现在C++编译器中错误信息的晦涩性,特别是涉及模板时,我认为对于像我这样难以“解析”编译器输出的人来说,这反而是一种优势! - Robinson
@Robinson 双刃剑。晦涩的错误信息问题确实是引入概念的最重要原因之一。通常认为有一个正式的接口定义是良好的实践。显式接口定义有时会添加大量(不必要的)样板代码,因为它可以很容易地推断出来。总体而言,在模板定义方面使用显式接口通常更受青睐,这也是概念所能给我们的。... - Fabio Fracassi
第二个正交问题,引起了@Steve提到的争议,是鸭子类型,这意味着模板的用户不必显式声明其参数类型满足所需的接口。 - Fabio Fracassi
@Robinson - 你确实只需要阅读文档(或代码),但这似乎对Python有效。作为参考,请查看旧版SGI STL文档中的概念描述:虽然它无法帮助编译器诊断或其他语言支持概念所带来的便利,但它是一个清晰描述需求以便于实现的好例子。 - Useless

2
在您的情况下,“模板方法”(不要与模板方法模式混淆),或者让我们称之为“静态依赖注入”,可以避免需要虚函数。通过给编译器更多和明确的知识,您主要获得性能,并因此为其提供更好的优化机会。类变得更加静态,类型安全性得到提高。
类大小可能会缩小(无需或减少存储指针的需求)。
古话说:

不要为您不使用的东西付费。

这里适用。如果您不需要虚接口,则模板可以帮助您避免它们,而不会牺牲所有的灵活性。

好的,你不需要一个虚拟接口,但是如果没有它如何强制执行策略类的接口呢?也就是说,当注入时,如何告诉程序员策略类必须具有哪些方法和属性才能正常运行? - Robinson
@Robinson的文档。否则,他会得到编译错误。 - BЈовић
我真的很想把上面所有的选项都勾选为“答案”,但很抱歉我必须选择Tom的例子,因为它让我“恍然大悟” :p。 - Robinson
@Robinson:没问题,你应该接受对问题有最大帮助的答案 :) - Sebastian Mach
@Robinson 你可以使用 'static_assert()'(在C++11或从boost中)来强制执行接口。 - Fabio Fracassi
@Robinson:而且在将来,C++将会拥有概念(concepts),这可以帮助您确保合约。 - Sebastian Mach

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接