由于这里有很多问题,我将首先尝试综合阐述我对这个主题的看法,然后根据这些材料明确回答每个问题。
合成
当我写的时候the book http://affiliate.manning.com/idevaffiliate.php?id=1150_236,我首先尝试描述我在野外目睹的模式和反模式。因此,本书中的模式和反模式首先是描述性的,并且只是在较小程度上规定性的。显然,将它们分为patterns and 反模式暗示一定程度的判断:)
Bastard Injection 存在多个层面的问题:
最危险的问题与包依赖关系。这是我试图通过引入术语来使其更具可操作性的概念国外违约 versus 本地默认值。问题在于国外违约是它们拖累了硬耦合的依赖关系,这使得(去/重新)合成变得不可能 https://stackoverflow.com/a/9503612/126014。更明确地处理包管理的一个好资源是敏捷原则、模式和实践 http://amzn.to/19W4JHk.
就水平而言封装,这样的代码很难推理:
private readonly ILog log;
public MyConsumer(ILog log)
{
this.log = log ??LogManager.GetLogger("My");
}
虽然它保护了类的不变量,但问题是在这种情况下, null
是可接受的输入值。这并非总是如此。在上面的例子中,LogManager.GetLogger("My")
可能只介绍一个本地默认值。从这段代码片段中,我们无法知道这是否属实,但为了便于论证,我们现在假设这一点。如果默认ILog
确实是一个本地默认值,客户MyConsumer
可以通过null
代替ILog
。请记住,封装的目的是使客户端能够轻松地使用对象,而无需了解所有实现细节。这意味着这是客户看到的全部内容:
public MyConsumer(ILog log)
在 C#(和类似语言)中,可以通过null
代替ILog
,它将编译:
var mc = new MyConsumer(null);
通过上述实现,不仅可以编译,而且还可以在运行时运行。根据波斯特尔定律 http://en.wikipedia.org/wiki/Robustness_principle,这是一件好事,对吧?
不幸的是,事实并非如此。
考虑另一个类required依赖性;我们称其为存储库,只是因为这是一个众所周知的(尽管被过度使用)模式:
private readonly IRepository repository;
public MyOtherConsumer(IRepository repository)
{
if (repository == null)
throw new ArgumentNullException("repository");
this.repository = repository;
}
为了与封装保持一致,客户端只能看到以下内容:
public MyOtherConsumer(IRepository repository)
根据以前的经验,程序员可能倾向于编写这样的代码:
var moc = new MyOtherConsumer(null);
这仍然可以编译,但是运行时失败!
如何区分这两个构造函数?
public MyConsumer(ILog log)
public MyOtherConsumer(IRepository repository)
你不能,但目前,你的行为不一致:在一种情况下,null
是一个有效的论点,但在另一种情况下,null
将导致运行时异常。这将降低每个客户端程序员对 API 的信任。存在持续的是一个更好的前进方向。
为了让班级像MyConsumer
更容易使用,你必须保持一致。这就是接受的理由null
这是一个坏主意。更好的方法是使用构造函数链:
private readonly ILog log;
public MyConsumer() : this(LogManager.GetLogger("My")) {}
public MyConsumer(ILog log)
{
if (log == null)
throw new ArgumentNullException("log");
this.log = log;
}
客户现在看到的是这样的:
public MyConsumer()
public MyConsumer(ILog log)
这与MyOtherConsumer
因为如果你试图通过null
代替ILog
,您将收到运行时错误。
虽然这是技术上仍然是混蛋注射,我可以接受这个设计 https://stackoverflow.com/a/6739953/126014 for 本地默认值;事实上,我有时会设计这样的 API,因为它是许多语言中众所周知的习惯用法。
对于许多目的来说,这已经足够好了,但仍然违反了一个重要的设计原则:
显式优于隐式 http://www.python.org/dev/peps/pep-0020/
虽然构造函数链允许客户端使用MyConsumer
有默认的ILog
,没有简单的方法可以找出默认实例ILog
将会。有时,这也很重要。
此外,默认构造函数的存在会带来风险,即一段代码将在外部调用该默认构造函数。成分根 http://blog.ploeh.dk/2011/07/28/CompositionRoot。如果发生这种情况,则说明您过早地将对象相互耦合,并且一旦完成此操作,您就无法将它们与组合根内部解耦。
因此,使用普通构造函数注入的风险较小:
private readonly ILog log;
public MyConsumer(ILog log)
{
if (log == null)
throw new ArgumentNullException("log");
this.log = log;
}
你仍然可以作曲MyConsumer
使用默认记录器:
var mc = new MyConsumer(LogManager.GetLogger("My"));
如果你想使本地默认值更容易发现,您可以将其作为工厂公开在某个地方,例如于MyConsumer
类本身:
public static ILog CreateDefaultLog()
{
return LogManager.GetLogger("My");
}
所有这些都为回答该问题中的具体子问题奠定了基础。
1. 当依赖项的默认实现是本地默认值时,Bastard Injection 反模式是否也会发生?
是的,从技术上讲,确实如此,但后果没那么严重。混蛋注射首先是一个描述,使您能够在遇到它时轻松识别它。
请注意,书中的上述插图描述了如何重构away来自私生子注射;不是如何识别的。
2. [应该] 与本地默认值的可选依赖关系 [...] 也应该避免吗?
从包依赖关系的角度来看,您不需要避免这些;它们相对良性。
从使用的角度来看,我仍然倾向于避免它们,但这取决于我正在构建的内容。
- 如果我创建一个可重用库(例如OSS项目)很多人都会使用,我可能仍然会选择Constructor Chaining,以便更容易上手API。
- 如果我创建一个仅在特定代码库中使用的类,我倾向于完全避免可选依赖项,而是在组合根中明确地组合所有内容。
3. 他是否也在暗示,Foreign Default 会使并行开发变得更加困难,而 Local Default 则不会?
不,我不。如果有默认的话,必须先默认到位才可以使用;是否是并不重要Local or Foreign.