依赖注入构造函数的惰性初始化

12

我有一个类,在其中注入了两个服务依赖项。我正在使用Unity容器。

public interface IOrganizer
{
    void Method1();
    void Method2();
    void Method3();
}

public class Organizer : IOrganizer
{    
    private IService1 _service1;
    private IService2 _service2;

    public Organizer(Iservice1 service1, IService2 service2)
    {
        _service1 = service1;
        _service2 = service2;
    }

    public void Method1()
    {
        /*makes use of _service1 and _service2 both to serve the purpose*/
    }

    public void Method2()
    {
        /*makes use of only _service1 to serve the purpose*/
    }

    public void Method3()
    {
        /*makes use of only _service2 to serve the purpose*/
    }
}

虽然它可以正常运行,但在我仅调用Method2Method3时,Unity不必要地创建了另一个不需要的服务实例,这样有点臭。 这里的代码片段只是为了说明而示例。在实际情况中,注入服务的对象图本身相当深。

有没有更好的设计和解决这种情况的方法?


2
如果service1service2可以注册为单例,它们将只被创建一次,因此在不需要特定方法的服务时,使用该服务将不会产生任何成本。也就是说,在.NET中实例化对象的开销非常低 - 我们真正进入了微观优化领域。 - NightOwl888
3
构建对象的速度快慢取决于该对象的性质。构建不执行任何操作的构造函数的对象确实非常快。但是有些对象在构建时需要完成大量工作。 - Servy
1
@Servy - 很好的观点。但是如果遵循良好的 DI 实践 注入构造函数应该简单,那么在正常情况下这不应该成为一个问题。 - NightOwl888
1
我之前读了一篇不错的文章,觉得很有意思。也许对你有些帮助,链接在这里 https://rehansaeed.com/asp-net-core-lazy-command-pattern/ - Nkosi
1
据我所记,Unity支持延迟解析,因此您应该能够将Lazy <IService1>注入到构造函数中并使用它。 - Evk
显示剩余8条评论
2个回答

15
我认为你的嗅觉很敏锐。大多数人会毫不犹豫地编写这样的代码。但是,我同意OP中所述设计存在一些代码异味。
我想指出,我使用“代码异味”这个术语,就像重构中所用的那样。它表明某些东西可能不正确,并且值得进一步调查。有时,这样的调查揭示了代码存在的良好原因,你可以继续前进。
在OP中至少有两种不同的代码异味。它们没有关联,因此我将分别处理每个问题。

内聚性

面向对象设计的一个基本但经常被忽略的概念是内聚性。将其视为关注点分离的反作用力。正如Kent Beck曾经说过的那样(确切的来源逃脱了我,所以我进行了改述),变化在一起的事物应该在一起,而独立变化的事物应该分开。
没有内聚性的“力量”,关注点分离的“力量”会将代码拆分成非常小的类,甚至简单的业务逻辑也会分散在多个文件中。
寻找内聚性或缺乏内聚性的一种方法是“计算”每个类方法使用了多少个类字段。虽然这只是一个粗略的指标,但它确实会在OP代码中触发我们的嗅觉。 Method1使用了类的所有字段,因此没有问题。另一方面,Method2Method3仅使用了类字段的一半,因此我们可以将其视为低内聚性的迹象 - 如果您愿意,这是一种代码异味。
如何解决?再次强调,异味并不一定是坏的。它只是一个需要调查的原因。
但是,如果您想解决问题,我想不到任何其他方法,除了将类分解成几个较小的类。
OP中的Organizer类实现了IOrganizer接口,因此从技术上讲,只有在您还可以拆分接口的情况下才能拆分Organizer - 尽管您可以编写Facade,然后将每个方法委派给实现该特定方法的单独类。
但是,接口的存在强调了Interface Segregation Principle的重要性。我经常看到代码库展示出这个特殊问题,因为接口太大。如果可能的话,请尽可能使接口尽可能小。我倾向于将其推向极端,并定义每个接口上仅有一个成员。

SOLID原则中的另一个依赖反转原则可以得出,接口应由使用它们的客户端定义,而不是实现它们的类。像这样设计接口通常能使它们保持小巧简洁。

还要记住,单个类可以实现多个接口。

性能

关于OP中的设计,另一个问题是性能,尽管我同意NightOwl888的评论,你可能处于微观优化领域。

一般来说,您可以自信地组合大型对象图。正如NightOwl888在上面的评论中建议的那样,如果一个依赖项具有Singleton生命周期,那么如果您注入它但最终没有使用它,也没有什么区别。

即使您无法给像_service2这样的依赖项提供Singleton生命周期,我再次同意NightOwl888的观点,即.NET中的对象创建速度非常快,几乎无法测量。正如他指出的那样,注入构造函数应该简单

即使在依赖项必须具有瞬态生命周期的罕见情况下,且由于某种原因创建实例很昂贵,您也可以始终将该依赖项隐藏在虚拟代理后面,正如我在关于对象图的文章中所描述的那样。
如何在Unity中配置所有这些内容,我已经不记得了,但如果Unity无法处理,请选择另一种组合方法,最好是纯DI

感谢您精彩地解释了需要考虑的复杂性。 - rahulaga-msft

3
只要您使用Unit 3或更高版本,就无需特别处理来解决懒加载。
您可以像往常一样注册您的类型:
container.RegisteryType<IMyInterface>()...;

然后将构造函数更改为要求懒加载:

public class MyClass
{
  public Lazy<IMyInterface> _service1;

  public MyClass(Lazy<IMyInterface> service1)
  { 
    _serivce1 = service1;
  }
}

然后调用您需要的方法:
_service1.Value.MyMethod();

4
如果 MyClass 依赖于 IMyInterface,那么将其改为依赖于 Lazy<IMyInterface> 是一种泄露的抽象。这意味着它知道有关 IMyInterface 具体实现的某些东西,而不应该知道。虚拟代理以更优雅的方式解决了这个问题,而不需要更改 MyClass - Mark Seemann

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