在进行IoC时,我(认为我)理解了它通过构成正确的部分来获得所需的应用程序级功能的用途,以及可测试性的好处。但在微观层面上,我不太了解如何确保对象注入了可以实际使用的依赖项。我的示例是数据库的BackupMaker
。
要进行备份,需要以特定格式导出数据库,使用特定压缩算法进行压缩,然后与一些元数据打包在一起以形成最终的二进制文件。完成所有这些任务似乎并不是一个单一的责任,所以我最终得到了两个合作者:一个DatabaseExporter
和一个Compressor
。BackupMaker
并不真正在乎数据库的导出方式(例如,使用IPC到数据库软件随附的实用程序,或者通过执行正确的API调用),但是它确实非常在意结果,即它需要首先要以可移植(版本不可知)格式进行这种数据库备份,我都不知道如何包装合同。压缩器是否在内存或磁盘中进行压缩也不在乎,但必须为BZip2。
如果我为BackupMaker
提供错误类型的导出器或压缩器,它仍然会产生结果,但是会损坏-看起来像是备份,但不会具有应有的格式。感觉系统的其他任何部分都无法信任这些协作者,因为BackupMaker
不能保证自己做正确的事。 (从我的角度来看)它的工作是产生有效的备份,如果情况不正确,它将不会,更糟糕的是,它不会知道它。同时,即使在撰写本文时,在我看来我现在也在说些愚蠢的事情,因为单一职责的重点是每个人都应该做好工作,而不用担心别人的工作。如果这么简单,那就不需要合同了-J.B. Rainsberger刚刚教我了。 (仅供参考,我直接向他发送了这个问题,但我还没有得到答复,对此事会有更多意见是很好的。)
凭直觉,我最喜欢的选择是使不可能以无效的方式组合类/对象,但我不知道该怎么做。我是否应该编写极其具体的接口名称(例如IDatabaseExportInSuchAndSuchFormatProducer
和ICompressorUsingAlgorithmXAndParametersY
),并假设没有类实现它们(如果它们不这样做),然后再称呼它,因为对于完全说谎的代码无法做任何事情,请每天调用它吗?我是否应该完成解剖数据库导出和压缩算法的二进制格式这样的平凡任务,以便进行合同测试以不仅验证语法,而且还验证行为,然后确定(但如何?)仅使用经过测试的类?还是我可以以某种方式重新分配责任以使这一问题消失?是否应该有另一个班级负责组成正确的较低层元素?还是我分解得太多?
改写
我注意到,这个非常特殊的示例受到了很多关注。我的问题比这更笼统。因此,对于赏金的最后一天,我将尝试总结如下。
根据定义,在使用依赖项注入时,一个对象需要依赖于其他对象。在许多书籍示例中,指示兼容性的方法(满足需求的能力)是使用类型系统(例如,实现接口)。除此之外,尤其是在动态语言中,使用合同测试。编译器(如果有)检查语法,合同测试(程序员需要记住)验证语义。到目前为止,一切都很好。但是,有时语义仍然太简单,无法确保某个类/对象可用作对另一个类的依赖,或者太复杂而无法在合同中正确描述。
在我的示例中,依赖于数据库导出器的类将考虑实现IDatabaseExportInSuchAndSuchFormatProducer
并返回有效字节的任何内容(因为我不知道如何验证格式)。是非常具体的命名和如此艰巨的合同,还是我可以做得更好?我应该将合同测试转变为集成测试吗?也许(集成)测试这三个组成部分?我并不是在试图通用,而是试图将职责分开并保持可测试性。
最佳答案
您在问题中发现的是,您有2个彼此隐式依赖的类。因此,最实际的解决方案是使依赖性明确。
您可以通过多种方法来执行此操作。
选项1
最简单的选择是使一个服务依赖于另一服务,并使从属服务在其抽象中是显式的。
优点
很少有类型可以实现和维护。
仅通过将压缩服务排除在构造函数之外就可以跳过特定实现的压缩服务。
DI容器负责生命周期管理。
缺点
可能将非自然的依赖项强制转换为实际上不需要它的类型。
public class MySqlExporter : IExporter
{
private readonly IBZip2Compressor compressor;
public MySqlExporter(IBZip2Compressor compressor)
{
this.compressor = compressor;
}
public void Export(byte[] data)
{
byte[] compressedData = this.compressor.Compress(data);
// Export implementation
}
}
选项2
由于您要进行不直接依赖于特定压缩算法或数据库的可扩展设计,因此可以使用Aggregate Service(实现Facade Pattern)来从您的
BackupMaker
中抽象出更具体的配置。如本文所述,您有一个隐式域概念(依赖项的协调),需要将其实现为显式服务
IBackupCoordinator
。优点
DI容器负责生命周期管理。
将压缩排除在特定的实现之外,就像将数据通过该方法一样容易。
明确实现您所缺少的域概念,即协调依赖关系。
缺点
有许多类型可以构建和维护。
BackupManager
必须具有3个依存关系,而不是在DI容器中注册的2个依存关系。通用接口
public interface IBackupCoordinator
{
void Export(byte[] data);
byte[] Compress(byte[] data);
}
public interface IBackupMaker
{
void Backup();
}
public interface IDatabaseExporter
{
void Export(byte[] data);
}
public interface ICompressor
{
byte[] Compress(byte[] data);
}
专用接口
现在,要确保各部分仅以一种方式连接在一起,您需要创建特定于所使用算法和数据库的接口。您可以使用接口继承来实现此目的(如图所示),也可以只将界面差异隐藏在立面后面(
IBackupCoordinator
)。public interface IBZip2Compressor : ICompressor
{}
public interface IGZipCompressor : ICompressor
{}
public interface IMySqlDatabaseExporter : IDatabaseExporter
{}
public interface ISqlServerDatabaseExporter : IDatabaseExporter
{}
协调员实施
协调员是为您完成工作的人。实现之间的细微差别是显式调用了接口依赖关系,因此您不能在DI配置中注入错误的类型。
public class BZip2ToMySqlBackupCoordinator : IBackupCoordinator
{
private readonly IMySqlDatabaseExporter exporter;
private readonly IBZip2Compressor compressor;
public BZip2ToMySqlBackupCoordinator(
IMySqlDatabaseExporter exporter,
IBZip2Compressor compressor)
{
this.exporter = exporter;
this.compressor = compressor;
}
public void Export(byte[] data)
{
this.exporter.Export(byte[] data);
}
public byte[] Compress(byte[] data)
{
return this.compressor.Compress(data);
}
}
public class GZipToSqlServerBackupCoordinator : IBackupCoordinator
{
private readonly ISqlServerDatabaseExporter exporter;
private readonly IGZipCompressor compressor;
public BZip2ToMySqlBackupCoordinator(
ISqlServerDatabaseExporter exporter,
IGZipCompressor compressor)
{
this.exporter = exporter;
this.compressor = compressor;
}
public void Export(byte[] data)
{
this.exporter.Export(byte[] data);
}
public byte[] Compress(byte[] data)
{
return this.compressor.Compress(data);
}
}
BackupMaker实施
BackupMaker
现在可以是通用的,因为它可以接受任何类型的IBackupCoordinator
进行繁重的工作。public class BackupMaker : IBackupMaker
{
private readonly IBackupCoordinator backupCoordinator;
public BackupMaker(IBackupCoordinator backupCoordinator)
{
this.backupCoordinator = backupCoordinator;
}
public void Backup()
{
// Get the data from somewhere
byte[] data = new byte[0];
// Compress the data
byte[] compressedData = this.backupCoordinator.Compress(data);
// Backup the data
this.backupCoordinator.Export(compressedData);
}
}
请注意,即使您的服务在
BackupMaker
以外的其他地方使用,也可以将它们巧妙地包装到一个包中,然后再传递给其他服务。您不必仅因为注入了IBackupCoordinator
服务就需要使用这两种操作。可能会遇到麻烦的唯一地方是,如果在跨不同服务的DI配置中使用命名实例。选项3
与选项2相似,您可以使用Abstract Factory的特殊形式来协调具体
IDatabaseExporter
和IBackupMaker
之间的关系,这将填补依赖项协调器的作用。优点
很少需要维护的类型。
在DI容器中仅注册1个依赖项,使其更易于处理。
将生命周期管理移至
BackupMaker
服务,这使得不可能以会导致内存泄漏的方式对DI进行错误配置。明确实现您所缺少的域概念,即协调依赖关系。
缺点
将压缩排除在特定的实现之外需要您实现Null对象模式。
DI容器不负责生命周期管理,并且每个依赖项实例都是针对每个请求的,这可能不是理想的选择。
如果您的服务具有许多依赖关系,则通过
CoordinationFactory
实现的构造函数注入它们可能会变得很笨拙。介面
我将使用每种类型的
Release
方法显示工厂实现。这是遵循Register, Resolve, and Release pattern的,这使得它对于清除依赖项有效。如果第三方可以实现ICompressor
或IDatabaseExporter
类型,则这一点尤其重要,因为未知他们可能需要清除哪种依赖关系。但是请注意,在这种模式下,
Release
方法的使用完全是可选的,将它们排除在外将大大简化设计。public interface IBackupCoordinationFactory
{
ICompressor CreateCompressor();
void ReleaseCompressor(ICompressor compressor);
IDatabaseExporter CreateDatabaseExporter();
void ReleaseDatabaseExporter(IDatabaseExporter databaseExporter);
}
public interface IBackupMaker
{
void Backup();
}
public interface IDatabaseExporter
{
void Export(byte[] data);
}
public interface ICompressor
{
byte[] Compress(byte[] data);
}
BackupCoordinationFactory实现
public class BZip2ToMySqlBackupCoordinationFactory : IBackupCoordinationFactory
{
public ICompressor CreateCompressor()
{
return new BZip2Compressor();
}
public void ReleaseCompressor(ICompressor compressor)
{
IDisposable disposable = compressor as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
public IDatabaseExporter CreateDatabaseExporter()
{
return new MySqlDatabseExporter();
}
public void ReleaseDatabaseExporter(IDatabaseExporter databaseExporter)
{
IDisposable disposable = databaseExporter as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
}
public class GZipToSqlServerBackupCoordinationFactory : IBackupCoordinationFactory
{
public ICompressor CreateCompressor()
{
return new GZipCompressor();
}
public void ReleaseCompressor(ICompressor compressor)
{
IDisposable disposable = compressor as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
public IDatabaseExporter CreateDatabaseExporter()
{
return new SqlServerDatabseExporter();
}
public void ReleaseDatabaseExporter(IDatabaseExporter databaseExporter)
{
IDisposable disposable = databaseExporter as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
}
BackupMaker实施
public class BackupMaker : IBackupMaker
{
private readonly IBackupCoordinationFactory backupCoordinationFactory;
public BackupMaker(IBackupCoordinationFactory backupCoordinationFactory)
{
this.backupCoordinationFactory = backupCoordinationFactory;
}
public void Backup()
{
// Get the data from somewhere
byte[] data = new byte[0];
// Compress the data
byte[] compressedData;
ICompressor compressor = this.backupCoordinationFactory.CreateCompressor();
try
{
compressedData = compressor.Compress(data);
}
finally
{
this.backupCoordinationFactory.ReleaseCompressor(compressor);
}
// Backup the data
IDatabaseExporter exporter = this.backupCoordinationFactory.CreateDatabaseExporter();
try
{
exporter.Export(compressedData);
}
finally
{
this.backupCoordinationFactory.ReleaseDatabaseExporter(exporter);
}
}
}
选项4
在
BackupMaker
类中创建一个保护子句以防止允许不匹配的类型,并在不匹配的类型时引发异常。在C#中,您可以使用属性(将自定义元数据应用于类)来执行此操作。其他平台可能会或可能不会支持此选项。
优点
无缝-在DI中无需配置其他类型。
如果需要,用于比较类型是否匹配的逻辑可以扩展为每个类型包括多个属性。因此,例如,单个压缩器可用于多个数据库。
100%无效的DI配置将导致错误(尽管您可能希望使异常指定如何使DI配置工作)。
缺点
将压缩排除在特定的备份配置之外需要您实现Null对象模式。
用于比较类型的业务逻辑是在静态扩展方法中实现的,这使其可测试但无法与其他实现互换。
如果重构了设计,使得
ICompressor
或IDatabaseExporter
不再是同一服务的依赖项,那么它将不再起作用。自定义属性
在.NET中,属性可用于将元数据附加到类型。我们创建了一个自定义
DatabaseTypeAttribute
,可以将数据库类型名称与两种不同类型进行比较以确保它们兼容。[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public DatabaseTypeAttribute : Attribute
{
public DatabaseTypeAttribute(string databaseType)
{
this.DatabaseType = databaseType;
}
public string DatabaseType { get; set; }
}
具体的
ICompressor
和IDatabaseExporter
实现[DatabaseType("MySql")]
public class MySqlDatabaseExporter : IDatabaseExporter
{
public void Export(byte[] data)
{
// implementation
}
}
[DatabaseType("SqlServer")]
public class SqlServerDatabaseExporter : IDatabaseExporter
{
public void Export(byte[] data)
{
// implementation
}
}
[DatabaseType("MySql")]
public class BZip2Compressor : ICompressor
{
public byte[] Compress(byte[] data)
{
// implementation
}
}
[DatabaseType("SqlServer")]
public class GZipCompressor : ICompressor
{
public byte[] Compress(byte[] data)
{
// implementation
}
}
扩展方式
我们将比较逻辑放入扩展方法中,这样
IBackupMaker
的每个实现都会自动包含它。public static class BackupMakerExtensions
{
public static bool DatabaseTypeAttributesMatch(
this IBackupMaker backupMaker,
Type compressorType,
Type databaseExporterType)
{
// Use .NET Reflection to get the metadata
DatabaseTypeAttribute compressorAttribute = (DatabaseTypeAttribute)compressorType
.GetCustomAttributes(attributeType: typeof(DatabaseTypeAttribute), inherit: true)
.SingleOrDefault();
DatabaseTypeAttribute databaseExporterAttribute = (DatabaseTypeAttribute)databaseExporterType
.GetCustomAttributes(attributeType: typeof(DatabaseTypeAttribute), inherit: true)
.SingleOrDefault();
// Types with no attribute are considered invalid even if they implement
// the corresponding interface
if (compressorAttribute == null) return false;
if (databaseExporterAttribute == null) return false;
return (compressorAttribute.DatabaseType.Equals(databaseExporterAttribute.DatabaseType);
}
}
BackupMaker实施
保护子句可确保在创建类型实例之前,拒绝两个元数据不匹配的类。
public class BackupMaker : IBackupMaker
{
private readonly ICompressor compressor;
private readonly IDatabaseExporter databaseExporter;
public BackupMaker(ICompressor compressor, IDatabaseExporter databaseExporter)
{
// Guard to prevent against nulls
if (compressor == null)
throw new ArgumentNullException("compressor");
if (databaseExporter == null)
throw new ArgumentNullException("databaseExporter");
// Guard to prevent against non-matching attributes
if (!DatabaseTypeAttributesMatch(compressor.GetType(), databaseExporter.GetType()))
{
throw new ArgumentException(compressor.GetType().FullName +
" cannot be used in conjunction with " +
databaseExporter.GetType().FullName)
}
this.compressor = compressor;
this.databaseExporter = databaseExporter;
}
public void Backup()
{
// Get the data from somewhere
byte[] data = new byte[0];
// Compress the data
byte[] compressedData = this.compressor.Compress(data);
// Backup the data
this.databaseExporter.Export(compressedData);
}
}
如果您决定使用这些选项之一,请留下您对使用哪个选项的评论。在我的一个项目中,我也遇到类似的情况,我倾向于选择2。
回应您的更新
是非常具体的命名和如此艰巨的合同,还是我可以做得更好?我应该将合同测试转变为集成测试吗?也许(集成)测试这三个组成部分?我并不是在试图通用,而是试图将职责分开并保持可测试性。
创建集成测试是一个好主意,但前提是您确定要测试生产DI配置。尽管将所有组件作为一个单元进行测试以验证其是否可行也很有意义,但是如果所提供的代码配置与测试不同,则对于此用例而言,这样做没有多大用处。
你应该具体吗?我相信在这件事上我已经给了您一个选择。如果使用保护子句,则根本不需要特定。如果您选择其他选项之一,则在特定和通用之间会妥协。
我知道您说过您不是有意尝试通用,最好在某处划界线以确保解决方案不会过度设计。另一方面,如果由于接口不够通用而不得不重新设计解决方案,那也不是一件好事。扩展性始终是一项要求,无论是否事先指定,因为您永远不会真正知道将来业务需求将如何变化。因此,拥有通用的BackupMaker绝对是最好的选择。其他类可能更具体-如果将来的需求发生变化,您只需要一个接缝即可交换实现。
关于oop - 如何处理难以表达的依赖要求?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32340398/