我通常会在一些情况下使用"混蛋注射法"。当我有一个"合适的"依赖注入构造函数时:
1 2 3 4 5
| public class ThingMaker {
...
public ThingMaker(IThingSource source){
_source = source;
} |
但是,对于我打算作为公共API(其他开发团队将使用的类)的类,我再也找不到比用最可能需要的依赖项编写默认的"bastard"构造函数更好的选择:
1 2 3
| public ThingMaker () : this(new DefaultThingSource ()) {}
...
} |
这里明显的缺点是,这会创建对DefaultThingSource的静态依赖关系;理想情况下,不会有这种依赖关系,并且消费者总是会注入他们想要的任何源。然而,这太难使用了;消费者想要开发一个新的Thingmaker,开始做东西,然后几个月后当需要的时候再注入其他东西。在我看来,这只剩下几个选择:
省略bastard构造函数;强制ThingMaker的使用者了解它的源代码,了解ThingMaker如何与它的源代码交互,查找或编写具体的类,然后在其构造函数调用中注入实例。
省略bastard构造函数并提供一个单独的工厂、容器或其他引导类/方法;以某种方式使消费者理解他们不需要编写自己的ithingsource;强制Thingmaker的消费者查找和理解工厂或引导器并使用它。
保留bastard构造函数,使消费者能够"新建"一个对象并使用它运行,并处理对defaultThingSource的可选静态依赖。
孩子,3当然看起来很有吸引力。还有别的更好的选择吗?#1或2似乎不值得。
- 为什么对DefaultThingSource的依赖比对它的依赖更糟糕?和3一起去。
- 因为DefaultThingSource是一个潜在的非常具体的实现,它依赖于另一个程序集中的外部系统,这很可能在几个月内就过时了;它的源在will中始终是正确的,并且永远不会绑定到特定的实现。
- 显然,我不知道源代码有多复杂。然而,我敢打赌,如果你保持简单并设计得很好,实际上你会发现你的代码不会像你担心的那么快过时——当然不会很快证明所有这些心痛是合理的。
- 这是一个很好的依赖倒置的问题——它可能更灵活,设计更好,但是对于一个新的程序员来说,它并不是那么容易使用。
- 你也可以看看这篇文章:lostechies.com/jimmybogard/2009/07/03/…
- 也要注意,这种做法也被称为"穷人的依赖注射"。
- @帕特里克:"因为违约源是……几个月后就过时了——那么对于公共API中的"缺省"来说,它也是一个非常糟糕的候选。我投票赞成第2条。
- 这就是我使用"潜在的"过时的原因——我们不知道它将如何在使用中演变。我们希望我们的消费者找到各种有价值的方式来使用我们的软件。
据我所知,这个问题涉及到如何用一些适当的默认值公开松散耦合的API。在这种情况下,您可能有一个良好的本地默认值,在这种情况下,可以将依赖项视为可选的。处理可选依赖关系的一种方法是使用属性注入而不是构造函数注入——实际上,这是一种用于属性注入的海报场景。
但是,bastard注入的真正危险是当默认值是外部默认值时,因为这意味着默认构造函数会将不需要的耦合拖到实现默认值的程序集上。不过,据我所知,预期违约将源自同一程序集,在这种情况下,我看不到任何特定的危险。
在任何情况下,您也可以考虑一个外观,如我先前的回答之一所述:依赖注入(DI)"友好"库
顺便说一句,这里使用的术语是基于我书中的模式语言。
- +1 DI友好的库响应是在这个空间中绝对必须阅读的,它将使您能够从第一原则来思考这些东西。
- 立面也是个好主意。
- 很好地考虑了本地违约和国外违约之间的区别。在这种情况下,这确实是一种本地违约,尽管消费者可能有理由否决这一违约。在这种情况下,提供两个构造函数最简单地提示"使用默认值或注入自己的"。仅使用一个带有属性注入的默认构造函数不足以公布可注入依赖项。你同意吗?
- 我同意在这种情况下,重载的构造函数是可以的。我不同意属性注入不足以宣传用户可以重写依赖项,因为它是一种众所周知的模式,但我同意重载的构造函数在"您的脸"上稍微多一点":"
- 对于DI和IOC的所有这些问题,不能充分推荐@markseeman的书
- @关于门面,我很难调和这样一个观点,我所做的就是把混蛋注入转移到另一个类中。您对在配置文件中声明默认依赖项并通过后期绑定加载它有什么看法?
- @ZOMF在库用户上强制配置文件设置不是一种特别友好的方法。它锁定客户机以使用不灵活的配置系统。如果要从数据库而不是配置文件加载配置值,该怎么办?如果您想在单元测试中改变配置值,该怎么办?如果您想通过UI配置配置值,该怎么办?…fwiw,我在这里扩展了对DI友好的库建议,包括更多的代码示例。
- 谢谢,@markseeman。这正是我想要的。
我的交易是在@brokinglass:
1)唯一的构造函数是参数化的构造函数
2)使用工厂方法创建ThingMaker并传入该默认源。
1 2 3 4 5 6 7 8 9
| public class ThingMaker {
public ThingMaker (IThingSource source ){
_source = source ;
}
public static ThingMaker CreateDefault () {
return new ThingMaker (new DefaultThingSource ());
}
} |
显然,这并不能消除您的依赖性,但它确实使我更清楚地认识到,这个对象具有依赖性,如果调用方愿意的话,可以深入研究这些依赖性。如果您愿意(CreateThingKerWithDefaultThingSource),可以使工厂方法更加明确,如果这有助于理解。我更喜欢这个方法来重写ithingsource工厂方法,因为它继续支持组合。当defaultThingSource被废弃时,您还可以添加一个新的工厂方法,并有一个清晰的方法来使用defaultThingSource查找所有代码并将其标记为要升级。
你讨论了你的问题的可能性。在其他地方为方便或在类本身中为方便而设置的工厂类。唯一没有吸引力的选择是基于反射,进一步隐藏依赖关系。
- 我仍然不明白为什么这比默认的构造函数更好——它有所有的缺点,没有额外的好处;它只是使依赖性对任何阅读代码的人都更加模糊。
- @在我看来,它使依赖关系比使用默认构造函数更显式。类的设计方式似乎表明依赖关系很重要(它被注入到构造函数中),所以我会将其显式化。默认的构造函数告诉我:"不要担心这种依赖性,我只是为测试或一次性情况创建了一个额外的构造函数。"factory方法告诉我"有一个依赖关系需要注意,但这里是最有可能的实现。"
- 这不应该是大写C的"CreateDefault"吗?
- 这个解决方案没有明确说明如何使用这个类。第一次使用类时,您首先看到的是构造函数,如果我只看到一个构造函数,那么我假设必须提供一个IThingSource实例才能使用ThingMaker。如果我是这样一个类的用户,我可能永远不会发现createDefault方法。
- 我还是不相信,史蒂夫。虽然upvotes的数目可能表明我错了,但我不认为拥有一个默认的构造函数意味着"我只是为测试或一次性案例创建了一个额外的构造函数"。相反,我认为它意味着"使用默认值或注入自己的构造函数"。您还说,您认为默认的构造函数意味着"不要担心这种依赖性"--并且tha我同意。如果不想担心另一个,他们可以使用默认的构造函数。
- @帕特里克-够公平的。如果您想更加强调依赖性,这仅仅是一种选择。在这些决定中总是有一个权衡,这真的是一个你想把重点放在哪里的问题。如果defaultThingSource是大多数调用者的主要选项,那么我建议在默认构造函数中使用它。如果你想让来电者考虑提供他们自己的信息来源,那么我认为这是一个可以接受的中间立场。如果你想强迫用户提供一个信息源,那么我开始倾向于工厂类。
另一种选择是在您的ThingMaker类中有一个工厂方法CreateThingSource(),它为您创建依赖关系。
对于测试,或者如果您确实需要另一种类型的IThingSource,则必须创建ThingMaker的子类,并重写CreateThingSource(),以返回所需的具体类型。显然,只有当您主要需要能够将依赖项注入到中进行测试时,这种方法才是值得的,但对于大多数/所有其他目的来说,不需要另一个IThingSource。
- 我想到了这个,但它有所有的缺点,混蛋建造选项,没有任何好处!对吗?
- 我同意帕特里克的评论。
- 在很大程度上同意了这一点——我确实使用默认的构造函数自己创建了"默认"依赖项,并且没有发现任何问题。但这是不同的,因为它允许您不必在任何构造函数中具有依赖性——这样您就可以避免另一个您称之为"私生子构造函数"的构造函数,从而使公共接口更容易解析。但是,只有当更改依赖关系仅与类的可测试性相关时,这才有意义。
如果你必须有一个"默认"的依赖,也称为穷人的依赖注入,那么你必须初始化依赖并"连接"到某个地方。
我将保留这两个构造函数,但只有一个工厂用于初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class ThingMaker
{
private IThingSource _source;
public ThingMaker(IThingSource source)
{
_source = source;
}
public ThingMaker() : this(ThingFactory.Current.CreateThingSource())
{
}
} |
现在在工厂中创建默认实例并允许重写该方法:
1 2 3 4 5 6 7
| public class ThingFactory
{
public virtual IThingSource CreateThingSource ()
{
return new DefaultThingSource ();
}
} |
更新:
为什么使用两个构造函数:两个构造函数清楚地显示了类的用途。无参数构造器声明:只创建一个实例,类将执行它的所有职责。现在,第二个构造函数声明类依赖于IThingSource,并提供了一种使用不同于默认实现的实现的方法。
为什么要使用工厂:1-规程:创建新实例不应该是此类职责的一部分,工厂类更合适。2-dry:假设在同一个API中,其他类也依赖于IThingsource并执行相同的操作。一旦返回IThingSource的工厂方法和API中的所有类自动开始使用新实例,则重写。
我不认为ThingMaker与IThingSource的默认实现之间存在耦合问题,只要这个实现对整个API都有意义,而且您还提供了为测试和扩展目的重写这个依赖关系的方法。
- 那么,为什么要有一个工厂呢?只需将逻辑放入thingmaker默认构造函数。
- 这是必要的,如果你必须有一个公共部门。如果你不在船上,这是个坏主意。你能在你的答案中清楚地说明这一点吗,或者我认为它应该被否决,因为它没有添加其他答案还没有添加的任何内容,而且在不消除耦合问题的情况下,它是一个非常糟糕的实现。
- @鲁本·巴特林:我已经更新了我的答案,解释了设计选择。
- "创建新的实例[本身]不应该是这个类责任的一部分。"真的吗?你不能说每个班级都是这样的吗?我认为工厂方法/类有它们的位置,但我不能总是将它们用于所有事情。
- @帕特里克:这是个人喜好的问题。我个人尝试将有意义的类的新实例的创建隔离到我的API中的一个位置,从而实现工厂使用。此外,通过使用工厂,可以从中心位置轻松修改默认的IthingSource。正如我之前所说的个人偏好。
- @JCallico:要说清楚,你的编辑并没有动摇我。我在原始注释中遗漏了一个关键字——如果您绝对需要一个公共的无参数构造函数(用于序列化或因为它是一个属性),那么就可以了。在所有其他情况下,混蛋注射器是完全坏消息。如果你说的清楚/同意,我赞成,否则+0/meh/你的回答看起来像是在提倡坏的做法,但我不会反对,因为这是一个重要的技术。这是关于删除尽可能多的静态引用。
- @ruben bartelink:为了让调用者实例化thingmaker类并使用ithingsource的默认依赖项,这是问题作者想要的功能,您需要一个无参数的构造函数,它或者是建议的不太清晰的解决方案中的任何一个,比如静态createfault方法,我不认为ER足够。感谢您的所有反馈。
- @ JCallico:好的。我已经重新阅读了这个问题,并同意这是3的一个明确的暗示。我不同意3,除了我引用的案例(属性和序列化,其中需要一个无参数的pub-paramless-ctor),所以这通常不会得到我的赞成票,但辩论和你添加的东西触摸不同的事实值得一个反对票。
我投票赞成第3条。您将使您的生活——以及其他开发人员的生活——更加轻松。
- 你能解释一下怎么做吗?第三种方法避免了哪些问题?
- #1需要为消费者做额外的工作。#2需要对消费者和Patrick进行额外的工作。1和2都不能提供足够的好处,超过依赖于违约ThingSource的明显缺点。
- @安娜那些已经提到的问题。
- 我认为@mark seemann的答案最接近于在现实生活中提供好的设计,但这个答案也很简单也很好。:)
通常与这种类型的注入相关的示例通常非常简单:"在类B的默认构造函数中,使用new A()调用一个重载的构造函数,然后上路!"
事实上,依赖关系的构造通常极其复杂。例如,如果B需要非类依赖关系(如数据库连接或应用程序设置),该怎么办?然后将类B绑定到System.Configuration名称空间,增加其复杂性和耦合,同时降低其一致性,所有这些都对细节进行编码,而这些细节可以通过省略默认的构造函数简单地进行外部化。
这种类型的注入向读者传达了这样的信息:您已经认识到了分离设计的好处,但是不愿意承诺它。我们都知道,当有人看到这个多汁的、简单的、低摩擦的默认构造函数时,不管从那时起程序有多严格,他们都会调用它。如果不读取默认构造函数的源代码,他们就无法理解程序的结构,而这在您刚分发程序集时不是一个选项。您可以记录连接字符串名称和应用程序设置键的约定,但在这一点上,代码并不独立,您将责任推给开发人员,以寻找正确的咒语。
优化代码,使编写代码的人能够在不理解他们所说的是警笛之歌的情况下通过,这是一种反模式,最终导致在解开魔法时损失的时间比在最初的努力中节省的时间更多。要么脱钩,要么不脱钩;在每个图案中保持一只脚会降低两者的焦点。
- 对不起,你还没说服我。两个建设者——一个使容易的事情容易,另一个使困难的事情可能。你想让我做的事情似乎使事情变得困难。
- @Patrick Szalapski:我的观点是:1)如果没有源代码或文档,代码的使用者就无法理解程序的结构;2)并非每个依赖项的构造函数都很简单;3)您的对象承担了更多的知识和责任,削弱了它们的关注点。如果这些观点不能说服你,那就什么都不会说服你了。以这种方式构建一个完整的系统,并为自己评估它是否使事情变得简单。
你对这种依赖的OO不洁感到不满意,但你并没有说它最终会导致什么麻烦。
- ThingMaker是否以任何方式使用了不符合源的DefaultThingSource?不。
- 是否会有一段时间强制退出无参数构造函数?因为此时您能够提供默认实现,所以不太可能。
我认为这里最大的问题是名字的选择,而不是是否使用这种技术。
- 我不喜欢静态依赖于DefaultThingSource。
- 你为什么不喜欢这种依赖?
- 我同意你的看法,大卫。只是我不会用"不纯洁"这个词,因为没有什么不纯洁的依赖关系。这只是一个判断的要求,不管是否有。我同意这不是问题。
- @大卫B:我认为它不受欢迎,因为一旦用户开始使用awesomenewthing,它仍然对defaultThingSource有一个过时的、未使用的依赖关系。
- 这种依赖性不是公共知识,可以在不干扰用户的情况下进行更改。
为了它的价值,我在Java中看到的所有标准代码都是这样的:
1 2 3 4 5 6 7 8 9 10
| public class ThingMaker {
private IThingSource iThingSource ;
public ThingMaker () {
iThingSource = createIThingSource ();
}
public virtual IThingSource createIThingSource () {
return new DefaultThingSource ();
}
} |
任何不需要DefaultThingSource对象的人都可以覆盖createIThingSource。(如果可能的话,对EDCOX1 1的调用将是在除构造函数之外的某个地方)。C ^不鼓励像Java那样重写,并且它可能不像Java中的那样明显,用户可以并且可能应该提供它们自己的IthIn源实现。(也不是很明显如何提供)我的猜测是3是可行的,但我想我会提到这一点。
- + 0:- 5:不应该在CtoR中调用虚拟(100%对于OO来说是肯定的,对于Java来说肯定是99%)。-.5:没有解决耦合问题+1:这是一个有趣的技术变化,可以帮助人们思考,即使你我不会把它作为实际的解决方案。
- @ruben bartelink:是的,应该避免构造函数中的虚拟调用:stackoverflow.com/questions/119506/…
- 我将虚拟调用放入构造函数中,以尽可能简单地保存示例。然而,在这种情况下,是我把它放在那里。"我在Java中看到的标准代码"并没有调用构造函数中的CurreIthItHealthEuthor()等价物!
我支持选项1,有一个扩展:使DefaultThingSource成为公共类。你上面的措词意味着DefaultThingSource将对API的公共消费者隐藏,但我理解你的情况,没有理由不公开违约。此外,您可以很容易地记录这样一个事实:在特殊情况下,new DefaultThingSource()始终可以传递给ThingMaker。
- 我在想象,违约源确实是公共的,而不是内部的。
- 是的,这是可行的,但这是一个更好的解决方案有什么原因吗?
对于真正的公共API,我通常使用由两部分组成的方法来处理:
在API中创建一个助手,以允许API使用者使用其选择的IOC容器注册API中的"默认"接口实现。
如果希望允许API使用者在不使用自己的IOC容器的情况下使用API,请在API中承载一个可选容器,该容器填充相同的"默认"实现。
这里真正棘手的部分是决定何时激活容器2,最佳选择方法将在很大程度上取决于您预期的API消费者。
- 天哪,听起来很复杂。
- 只是第一次疼。;)不幸的是,如果一个人认真地试图支持作为API消费者的IOC和非IOC用户的需求,我看不到更好的方法……
- 我有一种感觉,这会伤害到所有试图维护此代码的人。
有一个内部工厂(库内部),它将DefaultThingSource映射到从默认构造函数调用的IthingSource。
这允许您在不使用参数或任何有关ThingSource的知识的情况下"新建"ThingMaker类,并且不直接依赖于DefaultThingSource。
- 这听起来像是选项3,但只是分成多个类。它不能摆脱静态依赖。
- ThingMaker在使用工厂时不再静态依赖于DefaultThingSource。如果现在有了IThingSource的新实现,只需在一个地方(即工厂)更改映射。
- 是的,但是Thingmaker对工厂有一个静态依赖关系,而工厂对我们默认的事物源有一个静态依赖关系。我们希望完全摆脱静态依赖。
- 啊,那么这是理论上的练习?因为在您的案例中,我看不到完全没有静态依赖性的实际优势。
只是一个想法——也许更优雅一点,但遗憾的是,它并没有摆脱依赖:
- 移除"混蛋建造师"
- 在标准构造函数中,将源参数默认为空
- 然后检查源是否为空,如果是这种情况,则将其分配为"new defaultThingSource()",否则将使用用户注入的任何内容。
- 这对未来的开发人员隐藏了更多的默认依赖,这是不可取的。