在 C# 中创建注入构造函数的接口的最佳方法是什么?

What is the best way to create Interfaces that are injected into constructor in C#?

我在一个项目中工作,其中外部数据是从不同来源获取的,例如数据库、3 个外部网络 API、网络配置。 为了避免紧耦合,在我的 类 构造函数中使用并传递了一些接口,例如:

public Dog(IDataAccess dataAccess, IConverter converter, IConfigAccess configAccess,
    ITimezoneAccess timezoneAccess)

public Cat(IDataAccess dataAccess, IConverter converter, IConfigAccess configAccess, 
    ITimezoneAccess timezoneAccess)

public Duck(IDataAccess dataAccess, IConverter converter, IConfigAccess configAccess, 
    ITimezoneAccess timezoneAccess)

它有助于我们进行单元测试,因为我们创建了这些接口的模拟实现。

在开发代码时,所有 类 之间有一些共同的功能,例如日期时间操作、固定值方法等。我决定创建一些静态 类 以将此功能划分为特定的类例如DatetimeHelper、FixedCalculationsHelper、StringHandlingHelper等

我得到的建议是避免使用这些静态 类 并将它们转换为带有接口的策略,并将它们作为其他外部数据访问接口传递到构造函数中。

  1. 当我应用这个的时候,我的类的构造函数会有很多Interface参数,比如:

    public Dog(IDataAccess 数据访问, IConverter 转换器, IConfigAccess configAccess, ITimezoneAccess timezoneAccess, IStringHandling stringHandler, IDatetimeHelper datetimeHelper ...等等...

处理这种情况最 elegant/best 的方法是什么? (不确定这里是否使用了一些技术,例如容器或类似的东西)

  1. 为什么将此静态 类 转换为 interface/implementation 策略更好(即使此方法是静态的,例如 CalculateArea(int value1, int value2))?

非常欢迎任何评论或解释。提前致谢。

使用接口的目的是编写抽象代码而不是具体化代码,从而消除依赖性。

  1. 将许多接口传递给构造函数是可以的,但是您不想传递具体的 类。如果您只是不希望构造函数具有参数,则可以使用 setter 注入而不是构造函数注入。

    public class Duck
    {
        IDataAccess DataAccess { get; set; }
        IConverter Converter { get; set; }
        IConfigAccess ConfigAccess { get; set; }
        ITimezoneAccess TimezoneAccess { get; set; }
    
        public Duck()
        {
             // parameterless contructor 
        }
    }
    
  2. 使用接口更改实现会容易得多。它使您可以更好地控制程序的结构。您希望 类 对扩展开放但对修改关闭,即 Open Closed Principle。在我看来,我会制作助手扩展方法并放弃为它们制作接口。

如果你不想要任何 DI 容器,对于助手,我建议你使用我用来调用的东西 "Abstract Intefacing" 创建空接口:

public interface IDateTimerHelper { }
public interface IFixedCalculationsHelper { }

然后在扩展中实现类


    public static class DateTimerHelperExtension
    {
        public static void HelpMeForDateTimering(this IDateTimerHelper dth/*, params*/)
        {
            //Help here;
        }
        public static void HelpMe(this IDateTimerHelper dth/*, params*/)
        {
            //Help here;
        }
    }
    public static class FixedCalculationsHelperExtension
    {
        public static void HelpMeForFixedCalculations(this IFixedCalculationsHelper fch/*, params*/)
        {
            //implement here
        }
        public static void HelpMe(this IFixedCalculationsHelper fch/*, params*/)
        {
            //implement here
        }
    }

终于这样用了


    public class Dog:IFixedCalculationsHelper,IDateTimerHelper
    {
        public Dog(/*other injections*/)
        {
            //Initialize
        }
        public void DoWork()
        {
            (this as IDateTimerHelper).HelpMe();
            (this as IFixedCalculationsHelper).HelpMe();
            this.HelpMeForDateTimering();
            this.HelpMeForFixedCalculations();
        }
    }

在您的项目中使用 StructureMap IoC 容器。让构造函数接收这些接口,并在项目中创建一个注册表,设置每个接口使用哪个class。

例如

    public class DuckProjectRegistry : Registry
    {
        public DuckProjectRegistry()
        {
            For<IDataAccess >().Use<ConcreteClassDataAccess>();
            For<IConverter>().Use<ConcreteConverterXYZ>();
            For<IConfigAccess>().Use<ConcreteConfigAccess>().Singleton();
            // etc.
        }
    }

public class Duck
{
    private readonly IDataAccess _dataAccess;
    private readonly IConverter _converter;
    private readonly IConfigAccess _configAccess;
    // etc.

    public Duck(
        IDataAccess dataAccess,
        IConverter converter,
        IConfigAccess configAccess
        // ,etc.)
    {
        _dataAccess = dataAccess;
        _converter = converter;
        _configAccess = configAccess;
        // etc.
    }

我们应用依赖注入来允许代码松散耦合。松散耦合的代码使我们的代码非常灵活。它允许我们的代码被隔离测试,允许代码独立部署,允许我们拦截或修饰 classes 而无需对整个应用程序进行彻底的更改。

但是对于 class 需要的每个依赖项,您不需要这些特征。对于简单的辅助方法,它们本身没有任何依赖性,永远不需要替换、修饰或拦截,并且不会使测试复杂化,几乎不需要将它们提升为完整组件并将它们隐藏在抽象背后.您很快就会发现您想要单独测试消耗 class,但要使用真正的辅助逻辑。现在您将无法在单元测试中连接所有这些。

我的建议是不要过度。

话虽如此,即使您不注入那些简单的帮助程序方法,您的 classes 可能仍然有很大的构造函数。具有许多依赖项的构造函数是一种代码味道。这表明这样的 class 违反了 Single Responsibility Principle (SRP),这意味着 class 有太多的责任。违反 SRP 会导致代码难以维护和测试。

修复 SRP 违规并不总是那么容易,但有几种模式和做法可以帮助您改进设计。

Refactoring to Aggregate Services 就是其中一种做法。如果 class 有很多依赖项,通常可以将 class 的逻辑的额外部分与这些依赖项放在一起,并将它们置于新的抽象之后:您的聚合服务。该聚合服务不公开其依赖项,而仅公开允许访问该提取逻辑的方法。

能够应用此重构的一个很好的迹象是,如果您有一组依赖项被注入到多个服务中。在您的情况下,您有一个清晰的组,由 IDataAccessIConverterIConfigAccessITimeZoneAccess 组成。您可以将两个、三个甚至所有四个移动到聚合服务。

让横切关注点与业务逻辑纠缠在一起是 classes 变得太大、依赖太多的另一个常见原因。您经常会看到事务处理、日志记录、审计跟踪、安全检查等与业务逻辑混在一起,并在整个代码库中重复出现。

一个有效的解决方法是将这些横切关注点从包含业务逻辑的 class 中移出,并使用拦截或修饰来应用它。如果您有一个 SOLID design. Take a look at this article 示例来了解如何应用横切关注点,而不必对整个代码库进行全面更改,则此方法效果最佳。