要进行备份,数据库需要以特定的格式导出,使用特定的压缩算法进行压缩,然后与一些元数据打包在一起形成最终的二进制文件.做所有这些任务似乎远远不是一个单一的责任,所以我最后有两个合作者:一个DatabaseExporter和一个压缩器.
BackupMaker不太在乎数据库的导出方式(例如,使用IPC到数据库软件附带的实用程序,或通过执行正确的API调用),但它确实关心结果,即它需要是一个这种数据库备份首先在可移植(版本不可知)格式中,其中任何一个我真的不知道如何包装合同.也不关心压缩机是否在内存或磁盘上进行压缩,但必须是BZip2.
如果我给BackupMaker提供了错误的出口商或压缩器,它仍然会产生一个结果,但它会被破坏 – 它将看起来像一个备份,但它不会有它应该具有的格式.感觉像系统的其他部分不能信任给予这些合作者,因为BackupMaker将无法保证自己做正确的事情;它的工作(从我的角度来看)是产生一个有效的备份,如果情况不正确,那就不会这样做,更糟糕的是它不会知道.同时,即使写这个,在我看来,我现在在说愚蠢的事情,因为单一责任的一个重点就是每一件事都要做,而不用担心别人的工作.如果那么简单,那就没有必要了 – J.B. Rainsberger刚刚教我了. (FYI,我直接向他发了这个问题,但我还没有答复,对此事情的意见很大.)
直观地,我最喜欢的选择是使得无法以无效的方式组合类/对象,但是我看不到如何做到这一点.我应该写出可怕的具体接口名称,如IDatabaseExportInSuchAndSuchFormatProducer和ICompressorUsingAlgorithmXAndParametersY,并假设没有类实现这些,如果它们不这样做,然后调用它一天,因为没有什么可以做完全的谎言代码?我应该去解剖数据库的导出和压缩算法的二进制格式的平凡任务,以便进行合同测试,以验证不仅语法,而且还要行为,然后确定(但是如何?)仅使用测试类?或者我可以以某种方式重新分配责任,使这个问题消失吗?应该有另一个班级负责组成正确的下级单位吗?还是我甚至分解太多?
重述
我注意到这个很具体的例子给了很多的关注.但是,我的问题比这更笼统.因此,在赏金的最后一天,我将尝试总结如下.
当使用依赖注入时,根据定义,一个对象依赖于其他对象需要什么.在许多书籍示例中,指示兼容性(提供该需求的能力)的方式是通过使用类型系统(例如实现接口).除此之外,特别是在动态语言中,使用合同测试.编译器(如果存在)检查语法,并且合同测试(程序员需要记住的)验证语义.到现在为止还挺好.然而,有时语义仍然太简单,无法确保某些类/对象可用作对另一个的依赖,或者太复杂,无法在合同中正确描述.
在我的示例中,我的类依赖于数据库导出器考虑了实现IDatabaseExportInSuchAndSuchFormatProducer的任何东西,并将字节返回为有效(因为我不知道如何验证格式).是非常具体的命名和这样一个非常粗暴的合同的方式去还是可以做得比那更好?我应该把合同测试变成一个集成测试吗?可能(整合)测试三者的组成?我不是真的想成为通用的,但我正在努力保持责任分离,并保持可测试性.
有很多方法可以做到这一点.
选项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之间的关系,这将填补依赖关系协调器的角色.
优点
>几种类型要维护.
>只有1个依赖注册在DI容器中,使其更易于处理.
>将生命周期管理移动到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类中创建一个guard子句,以防止不匹配的类型被允许,并在不匹配的情况下引发异常.
在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实现
保护子句确保在创建类型实例之前,将拒绝具有不匹配元数据的2个类.
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); } }
如果您决定其中一个选项,如果您留下了与您一起出现的评论,我将不胜感激.在我的一个项目中,我也有类似的情况,我倾向于选项二.
对您的更新的响应
Is very specific naming and such a very rough contract the way to go or can I do better than that? Should I turn the contract test into an integration test? Perhaps (integration) test the composition of all three? I’m not really trying to be generic but am trying to keep responsibilities separate and maintain testability.
创建集成测试是一个好主意,但只有当您确定您正在测试生产DI配置时.尽管将其全部测试为一个单元来验证它的工作原理也是有意义的,但是如果配置的代码与测试的配置不同,那么对于这种用例来说,它不会很好.
你应该具体吗?我相信我已经给了你一个选择.如果你带着守卫条款,你根本不必具体.如果您使用其他选项之一,则在特定和通用之间有一个很好的妥协.
我知道你说你并不是故意试图通用,而且在某个地方画线是很好的,以确保解决方案没有被过度设计.另一方面,如果解决方案必须重新设计,因为界面不够通用,这不是一件好事.无论是否在前面指定,扩展性始终是一个要求,因为您从未真正了解未来业务需求如何变化.所以拥有一个通用的BackupMaker绝对是最好的方式.其他类可以更具体 – 如果未来需求发生变化,您只需要一个接口来交换实现.