Ответ 1
Что вы обнаружили в своем вопросе, так это то, что у вас есть 2 класса, у которых неявная зависимость одна на другую. Итак, наиболее практичным решением является сделать зависимость явной.
Существует несколько способов сделать это.
Вариант 1
Самый простой вариант - заставить одну службу зависеть от другой и сделать зависимую службу явной в своей абстракции.
Pros
- Несколько типов для реализации и поддержки.
- Служба сжатия может быть пропущена для конкретной реализации, просто оставив ее вне конструктора.
- Контейнер 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
.
Pros
- Контейнер DI отвечает за управление жизненным циклом.
- Выход из сжатия из конкретной реализации так же просто, как передача данных с помощью метода.
- Явно реализует концепцию домена, которую вы пропускаете, а именно координация зависимостей.
Против
- Много типов для создания и обслуживания.
-
BackupManager
должно иметь 3 зависимостей вместо 2, зарегистрированных в контейнере DI.
Общие интерфейсы
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, вы можете использовать специализированную форму Аннотация Factory для координации отношения между бетоном IDatabaseExporter
и IBackupMaker
, который заполнит роль координатора зависимостей.
Pros
- Несколько типов для поддержки.
- Только 1 зависимость для регистрации в контейнере DI, что упрощает дело.
- Переводит управление жизненным циклом в службу
BackupMaker
, что делает невозможным неправильную настройку DI таким образом, чтобы вызвать утечку памяти. - Явно реализует концепцию домена, которую вы пропускаете, а именно координация зависимостей.
Против
- Для завершения сжатия из конкретной реализации требуется реализовать шаблон объекта Null.
- Контейнер DI не отвечает за управление жизненным циклом, а каждый экземпляр зависимостей - за запрос, что может быть не идеальным.
- Если ваши службы имеют много зависимостей, может возникнуть громоздкость для их ввода через конструктор реализаций
CoordinationFactory
.
Интерфейсы
Я показываю реализацию factory с помощью метода Release
для каждого типа. Это следует за зарегистрироваться, разрешить и удалить шаблончто делает его эффективным для очистки зависимостей. Это становится особенно важным, если третьи стороны могут реализовать типы 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
Создайте предложение guard в классе BackupMaker
, чтобы предотвратить невозможность использования несовпадающих типов, и создайте исключение в случае, если они не совпадают.
В С# вы можете сделать это с помощью атрибутов (которые применяют специальные метаданные к классу). Поддержка этой опции может или не может существовать на других платформах.
Pros
- Бесшовные - никаких дополнительных типов для настройки в 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
Предложение guard гарантирует, что 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 production. Несмотря на то, что имеет смысл протестировать все это как единицу, чтобы проверить, работает ли он, вы не очень хорошо для этого случая использования, если код, который поставляется, настроен иначе, чем тест.
Должны ли вы быть конкретными? По-моему, я уже дал вам выбор в этом вопросе. Если вы идете с оговоркой о защите, вам не обязательно быть конкретным. Если вы перейдете к одному из других вариантов, у вас есть хороший компромисс между конкретным и общим.
Я знаю, что вы заявили, что вы не намеренно пытаетесь быть родовыми, и хорошо рисовать линию где-то, чтобы гарантировать, что решение не переработано. С другой стороны, если решение нужно переделать, потому что интерфейс не был достаточно общим, и это тоже нехорошо. Расширяемость - это всегда требование, указано ли это заранее или нет, потому что вы никогда не знаете, как изменится бизнес-требования в будущем. Таким образом, наличие резервного BackupMaker, безусловно, лучший способ. Другие классы могут быть более конкретными - вам просто нужен один шов для замены программ, если будущие требования меняются.