在 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等
我得到的建议是避免使用这些静态 类 并将它们转换为带有接口的策略,并将它们作为其他外部数据访问接口传递到构造函数中。
当我应用这个的时候,我的类的构造函数会有很多Interface参数,比如:
public Dog(IDataAccess 数据访问, IConverter 转换器, IConfigAccess configAccess,
ITimezoneAccess timezoneAccess, IStringHandling stringHandler,
IDatetimeHelper datetimeHelper ...等等...
处理这种情况最 elegant/best 的方法是什么?
(不确定这里是否使用了一些技术,例如容器或类似的东西)
- 为什么将此静态 类 转换为 interface/implementation 策略更好(即使此方法是静态的,例如 CalculateArea(int value1, int value2))?
非常欢迎任何评论或解释。提前致谢。
使用接口的目的是编写抽象代码而不是具体化代码,从而消除依赖性。
将许多接口传递给构造函数是可以的,但是您不想传递具体的 类。如果您只是不希望构造函数具有参数,则可以使用 setter 注入而不是构造函数注入。
public class Duck
{
IDataAccess DataAccess { get; set; }
IConverter Converter { get; set; }
IConfigAccess ConfigAccess { get; set; }
ITimezoneAccess TimezoneAccess { get; set; }
public Duck()
{
// parameterless contructor
}
}
使用接口更改实现会容易得多。它使您可以更好地控制程序的结构。您希望 类 对扩展开放但对修改关闭,即 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 的逻辑的额外部分与这些依赖项放在一起,并将它们置于新的抽象之后:您的聚合服务。该聚合服务不公开其依赖项,而仅公开允许访问该提取逻辑的方法。
能够应用此重构的一个很好的迹象是,如果您有一组依赖项被注入到多个服务中。在您的情况下,您有一个清晰的组,由 IDataAccess
、IConverter
、IConfigAccess
和 ITimeZoneAccess
组成。您可以将两个、三个甚至所有四个移动到聚合服务。
让横切关注点与业务逻辑纠缠在一起是 classes 变得太大、依赖太多的另一个常见原因。您经常会看到事务处理、日志记录、审计跟踪、安全检查等与业务逻辑混在一起,并在整个代码库中重复出现。
一个有效的解决方法是将这些横切关注点从包含业务逻辑的 class 中移出,并使用拦截或修饰来应用它。如果您有一个 SOLID design. Take a look at this article 示例来了解如何应用横切关注点,而不必对整个代码库进行全面更改,则此方法效果最佳。
我在一个项目中工作,其中外部数据是从不同来源获取的,例如数据库、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等
我得到的建议是避免使用这些静态 类 并将它们转换为带有接口的策略,并将它们作为其他外部数据访问接口传递到构造函数中。
当我应用这个的时候,我的类的构造函数会有很多Interface参数,比如:
public Dog(IDataAccess 数据访问, IConverter 转换器, IConfigAccess configAccess, ITimezoneAccess timezoneAccess, IStringHandling stringHandler, IDatetimeHelper datetimeHelper ...等等...
处理这种情况最 elegant/best 的方法是什么? (不确定这里是否使用了一些技术,例如容器或类似的东西)
- 为什么将此静态 类 转换为 interface/implementation 策略更好(即使此方法是静态的,例如 CalculateArea(int value1, int value2))?
非常欢迎任何评论或解释。提前致谢。
使用接口的目的是编写抽象代码而不是具体化代码,从而消除依赖性。
将许多接口传递给构造函数是可以的,但是您不想传递具体的 类。如果您只是不希望构造函数具有参数,则可以使用 setter 注入而不是构造函数注入。
public class Duck { IDataAccess DataAccess { get; set; } IConverter Converter { get; set; } IConfigAccess ConfigAccess { get; set; } ITimezoneAccess TimezoneAccess { get; set; } public Duck() { // parameterless contructor } }
使用接口更改实现会容易得多。它使您可以更好地控制程序的结构。您希望 类 对扩展开放但对修改关闭,即 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 的逻辑的额外部分与这些依赖项放在一起,并将它们置于新的抽象之后:您的聚合服务。该聚合服务不公开其依赖项,而仅公开允许访问该提取逻辑的方法。
能够应用此重构的一个很好的迹象是,如果您有一组依赖项被注入到多个服务中。在您的情况下,您有一个清晰的组,由 IDataAccess
、IConverter
、IConfigAccess
和 ITimeZoneAccess
组成。您可以将两个、三个甚至所有四个移动到聚合服务。
让横切关注点与业务逻辑纠缠在一起是 classes 变得太大、依赖太多的另一个常见原因。您经常会看到事务处理、日志记录、审计跟踪、安全检查等与业务逻辑混在一起,并在整个代码库中重复出现。
一个有效的解决方法是将这些横切关注点从包含业务逻辑的 class 中移出,并使用拦截或修饰来应用它。如果您有一个 SOLID design. Take a look at this article 示例来了解如何应用横切关注点,而不必对整个代码库进行全面更改,则此方法效果最佳。