Dependency injection 依赖注入要走多远?

Dependency injection 依赖注入要走多远?,dependency-injection,solid-principles,Dependency Injection,Solid Principles,最近我发现了依赖注入,现在我正努力掌握它的使用频率和距离 例如,假设我有一个对话框,提示用户输入他们的注册详细信息——名字、姓氏、电话号码、序列号——诸如此类。应以各种方式验证数据(例如,名字和姓氏不为空,序列号为特定长度)。验证后,应将其缓存在本地计算机上,并发送到注册服务器。只有当所有这些操作成功或用户取消时,对话框才会关闭 这可能是我们在这里要实现的四件事(职责):UI、验证、本地缓存、将数据发送到非本地服务器 对话框的职责是什么?应该注入什么?显然,对话框负责UI,但验证、缓存和数据发送

最近我发现了依赖注入,现在我正努力掌握它的使用频率和距离

例如,假设我有一个对话框,提示用户输入他们的注册详细信息——名字、姓氏、电话号码、序列号——诸如此类。应以各种方式验证数据(例如,名字和姓氏不为空,序列号为特定长度)。验证后,应将其缓存在本地计算机上,并发送到注册服务器。只有当所有这些操作成功或用户取消时,对话框才会关闭

这可能是我们在这里要实现的四件事(职责):UI、验证、本地缓存、将数据发送到非本地服务器

对话框的职责是什么?应该注入什么?显然,对话框负责UI,但验证、缓存和数据发送都应该注入吗?我认为他们是这样做的,否则dialog类必须知道数据字段背后的逻辑才能进行验证,它必须知道如何和在哪里缓存数据,以及如何将数据发送到某处。如果是这样的话,这可能会导致调用端出现一些繁重的代码(假设我们通过构造函数进行注入,我认为这比setter函数更好),例如

但也许这没关系?多年来,我一直在看像对话框这样的代码,现在我觉得它确实有点陌生。但我也能看到这种情况是如何迅速升级的——如果有其他各种各样的小事情需要做怎么办——有多少事情被注入到了“太多”中


请不要试图在示例场景中挑出漏洞,我只是用它来举例说明。我更感兴趣的是DI的原理,以及在什么情况下您可能会走得太远。

您当然可以做到这一点。注入验证非常有意义,因为这样您就可以围绕验证代码编写单元测试,而无需启动任何GUI组件即可工作。注入缓存是有意义的,因为这样对话框就不必知道其接口以外的任何缓存系统。注入发送者是很有意义的,因为你的对话不必对任何事情都有模糊的概念

我有一个习惯,把事情严重地分开,因为我喜欢单一责任原则,我喜欢编写尽可能纯净的代码

问题是当你注入太大的接口时,你不再有任何合理的想法,你注入的东西实际上需要调用这些接口中的哪些部分,交互变得复杂,您的单元测试开始精确地依赖于依赖项所做的工作,因为当您知道75%的接口不会被使用时,您就不会费心去模拟整个接口

因此,一定要注入明确分开的职责,但要确保以适当的约束方式设计它们的接口。类可以同时实现多个接口,所以这并不是说你不能把接口分成小块,而是如果你想的话,用同一个对象实现它们。依赖代码永远不必知道

至于当你走得太远的时候。。。真的很难说,但我认为除非你使用一个完全没有添加任何东西的接口注入一些东西,否则你不会达到这一点。我总是希望注入一些有副作用的东西,因为这对单元测试和保持事情的合理性有很大的帮助。如果您可以将业务逻辑拆分为纯类并注入这些类,那么您将有一段非常棒的时间为其编写单元测试,所以这可能是值得做的

我使用这样的测试:

  • 它是否执行I/O,而我还没有在I/O提供类中?注射它
  • 它是否提供了我不需要知道细节的自包含处理?注射它
  • 它是否做了一些不属于我单一责任的事情?注射它

  • 您的里程数可能会有所不同。

    您偶然发现了许多人难以理解的DI中的一个令人困惑的部分。当使用构造函数注入时,有一种自然的趋势是将所有顶级服务推送到应用程序的入口点

    这种反模式称为注入上的构造函数。当一个类有3个或4个以上的依赖项时,它(在本例中是您的表单)可能会违反单一责任原则,这是一种代码味道。发生这种情况时,应该考虑创建相关的功能组合。

    尽管您的
    验证程序
    缓存程序
    发送方
    是独立的服务,但它们的功能显然是相关的。事实上,它们的功能可能在几个方法调用中重叠

    例如,在这个特定的实例中,使用一个用于<代码> Caseer-<代码>和<代码>发送器< /代码>将是有意义的,因为您将在从<代码>发件人< /代码>(我将考虑一个接收者-响应/请求)的过程中缓存数据。您可能还需要从UI直接将数据写入

    缓存
    发送方
    ,这样在将数据写入持久性存储后就不需要重新加载缓存

    public interface IDataService
    {
        IData ReadData(int id);
        void WriteData(IData data);
    }
    
    public class Sender: IDataService
    {
        public IData ReadData(int id)
        {
            // Get data from persistent store
        }
    
        public void WriteData(IData data)
        {
            // Write data to persistent store
        }
    }
    
    public class Cacher : IDataService
    {
        public readonly IDataService innerDataService;
        public readonly ICache cache;
    
        public Cacher(IDataService innerDataService, ICache cache)
        {
            if (innnerDataService == null)
                throw new ArgumentNullException("innerDataService");
            if (cache == null)
                throw new ArgumentNullException("cache");
    
            this.innerDataService = innerDataService;
            this.cache = cache;
        }
    
        public IData ReadData(int id)
        {
            IData data = this.cache.GetItem(id);
            if (data == null)
            {
                data = this.innerDataService.ReadData(id);
                this.cache.SetItem(id, data);
            }
            return data;
        }
    
        public void WriteData(IData data)
        {
            this.cache.SetItem(id, data);
            this.innerDataService.WriteData(data);
        }
    }
    
    用法
    根据需要验证的方式,让您的验证成为另一个
    IDataService

    也可能是有意义的,DI有许多优点,这实际上取决于您的项目。我以前在一个项目中工作过,每个依赖项都被注入,而在其他项目中,每个依赖项都被注入了一半。如果您正在编写单元测试,那么您将需要DI,以便在测试时可以传递模拟对象。
    public interface IDataService
    {
        IData ReadData(int id);
        void WriteData(IData data);
    }
    
    public class Sender: IDataService
    {
        public IData ReadData(int id)
        {
            // Get data from persistent store
        }
    
        public void WriteData(IData data)
        {
            // Write data to persistent store
        }
    }
    
    public class Cacher : IDataService
    {
        public readonly IDataService innerDataService;
        public readonly ICache cache;
    
        public Cacher(IDataService innerDataService, ICache cache)
        {
            if (innnerDataService == null)
                throw new ArgumentNullException("innerDataService");
            if (cache == null)
                throw new ArgumentNullException("cache");
    
            this.innerDataService = innerDataService;
            this.cache = cache;
        }
    
        public IData ReadData(int id)
        {
            IData data = this.cache.GetItem(id);
            if (data == null)
            {
                data = this.innerDataService.ReadData(id);
                this.cache.SetItem(id, data);
            }
            return data;
        }
    
        public void WriteData(IData data)
        {
            this.cache.SetItem(id, data);
            this.innerDataService.WriteData(data);
        }
    }
    
    MyDialog dlg = new MyDialog(new validator(), new cacher(new sender()));