您在问题中发现的是,您有 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)从您的@中抽象出更具体的配置987654326@.
正如文章中所指出的,您有一个隐式域概念(依赖关系的协调),需要将其实现为显式服务,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,它可以有效地清理依赖项。如果第 3 方可以实现 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 实施
保护子句确保在创建类型实例之前拒绝具有不匹配元数据的 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);
}
}
如果您决定选择其中一个选项,如果您留下评论以说明您选择哪一个,我将不胜感激。我在我的一个项目中遇到了类似的情况,我倾向于选项 2。
响应您的更新
是非常具体的命名和如此粗略的合同,还是我可以做得更好?我应该把合同测试变成集成测试吗?也许(集成)测试所有三个的组成?我并不是真的想成为通用的,而是试图将职责分开并保持可测试性。
创建集成测试是个好主意,但前提是您确定自己正在测试生产 DI 配置。尽管将所有这些作为一个单元进行测试以验证其是否有效也很有意义,但如果配置了随附的代码,对于此用例对您没有多大帮助与测试不同。
你应该具体吗?我相信我已经在这件事上给了你一个选择。如果您使用保护条款,则根本不必具体。如果您选择其他选项之一,则可以在特定和通用之间取得很好的折衷。
我知道您说过您不是故意尝试通用的,最好在某处划清界限以确保解决方案不会过度设计。另一方面,如果由于接口不够通用而必须重新设计解决方案,那也不是一件好事。无论是否预先指定,可扩展性始终是一项要求,因为您永远不知道未来业务需求将如何变化。因此,拥有一个通用的 BackupMaker 绝对是最好的方法。其他类可以更具体 - 如果未来需求发生变化,您只需要一个接缝来交换实现。