Как справляться с трудными для выражения требований к зависимостям?

При выполнении IoC я (думаю, что я) понимаю его использование для получения желаемой функциональности уровня приложения, составляя правильные части и преимущества для проверки. Но на микроуровне я не совсем понимаю, как убедиться, что объект получает зависимости, в которые он может работать. Мой пример для этого - BackupMaker для базы данных.

Чтобы сделать резервную копию, база данных должна быть экспортирована в определенном формате, сжата с использованием специального алгоритма сжатия, а затем упакована вместе с некоторыми метаданными для формирования окончательного двоичного файла. Выполнение всех этих задач, похоже, далеки от единой ответственности, поэтому я оказался с двумя сотрудниками: a DatabaseExporter и a Compressor.

BackupMaker все равно, как экспортируется база данных (например, с помощью IPC для утилиты, поставляемой с программным обеспечением базы данных, или путем правильного вызова API), но она очень заботится о результате, т.е. он должен быть резервным копированием такого типа в первую очередь в переносимом (версия агностическом) формате, который я действительно не знаю, как заключить контракт. Также не важно, сжат ли компрессор в памяти или на диске, но он должен быть BZip2.

Если я дам BackupMaker неправильные виды экспортера или компрессора, он все равно даст результат, но он будет поврежден - он будет выглядеть как резервная копия, но у него не будет формата, который он должен иметь. Похоже, никакая другая часть системы не может быть доверена, чтобы дать ей эти соавторы, потому что BackupMaker не сможет гарантировать правильное выполнение самой вещи; его работа (с моей точки зрения) заключается в том, чтобы создать действующую резервную копию, и это не будет, если обстоятельства не правильные, и, что еще хуже, она не будет знать об этом. В то же время, даже когда я пишу это, мне кажется, что сейчас я говорю что-то глупое, потому что весь смысл отдельных обязанностей заключается в том, что каждая часть должна выполнять свою работу, а не беспокоиться о работе других. Если бы это было так просто, не было бы необходимости в контрактах - J.B. Рейнсбергер просто научил меня. (FYI, я отправил ему этот вопрос напрямую, но у меня пока нет ответа, и больше мнения по этому вопросу будут замечательными.)

Интуитивно, моим любимым вариантом было бы сделать невозможным совмещение классов/объектов недопустимым образом, но я не вижу, как это сделать. Должен ли я писать чудовищно специфические имена интерфейсов, например IDatabaseExportInSuchAndSuchFormatProducer и ICompressorUsingAlgorithmXAndParametersY, и предположить, что ни один из классов не реализует их, если они не ведут себя как таковые, а затем называть его днем, поскольку ничего не может быть сделано о прямом лежачем коде? Должен ли я дойти до мирской задачи по анализу бинарного формата моих алгоритмов экспорта и сжатия базы данных, чтобы иметь контрактные тесты для проверки не только синтаксиса, но и поведения, а затем быть уверенным (но как?) Использовать только проверенные классы? Или я могу как-то перераспределить обязанности, чтобы эта проблема исчезла? Должен ли быть еще один класс, чья ответственность состоит в том, чтобы составить правильные элементы нижнего уровня? Или я даже сильно разлагаюсь?

Перефразировать

Я замечаю, что этому особому примеру уделяется большое внимание. Однако мой вопрос более общий, чем этот. Поэтому в последний день щедрости я попытаюсь обобщить следующее:

При использовании инъекции зависимостей, по определению, объект зависит от других объектов за то, что ему нужно. Во многих книжных примерах способ указания совместимости - возможность обеспечить эту потребность - осуществляется с помощью системы типов (например, для реализации интерфейса). Помимо этого, и особенно в динамических языках, используются контрактные тесты. Компилятор (если присутствует) проверяет синтаксис, и тесты контракта (о которых программист должен запомнить) проверяют семантику. Все идет нормально. Однако иногда семантика все еще слишком проста, чтобы гарантировать, что некоторый класс/объект можно использовать в качестве зависимости от другого или слишком сложно описывать в контракте должным образом.

В моем примере мой класс с зависимостью от экспортера базы данных рассматривает все, что реализует IDatabaseExportInSuchAndSuchFormatProducer, и возвращает байты как действительные (поскольку я не знаю, как проверить формат). Является ли очень конкретным наименованием и таким очень грубым контрактом путь, или я могу сделать лучше, чем это?. Должен ли я перевести контрактный тест на интеграционный тест? Может быть (интеграция) проверить состав всех трех? Я не очень стараюсь быть родовым, но стараюсь не разделять обязанности и поддерживать тестируемость.

Ответы

Ответ 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, безусловно, лучший способ. Другие классы могут быть более конкретными - вам просто нужен один шов для замены программ, если будущие требования меняются.

Ответ 2

Мое первое предположение заключалось бы в том, чтобы критически подумать, нужно ли вам быть таким общим: у вас есть конкретная проблема для решения, вы хотите создать резервную копию конкретной конкретной базы данных в определенном формате. Есть ли какая-то польза от решения проблемы для архивных баз данных и форматов? То, что вы наверняка получаете из общего решения, - это шаблонный код и повышенная сложность (люди понимают конкретные проблемы, а не общие). Если это применимо к вам, то мое предложение будет состоять в том, чтобы не позволить вашему DatabaseExporter принимать интерфейсы, а вместо этого - только конкретные реализации. Есть достаточно современных инструментов, которые также позволят вам издеваться над конкретными классами, поэтому тестируемость не является аргументом для использования здесь и здесь интерфейсов.

с другой стороны, если вам нужно сделать резервную копию нескольких баз данных с различными стратегиями, я бы, вероятно, представил что-то вроде

class BackupPlan { public DatabaseExporter exporter() {/**...*/} public Compressor compressor() {/** ... */} }

то ваш BackupMaker получит один BackupPlan, указав, какая база данных будет сжиматься с помощью какого алгоритма.

Ответ 3

В вашем вопросе подчеркивается тот факт, что состав объектов очень важен и что ответственная за такой состав (проводка) несет ответственность.

Поскольку у вас уже есть общий BackupMaker, я бы предположил, что вы сохраните его таким образом, и поставите большую ответственность за то, чтобы правильный состав объектов (для решения конкретной проблемы) выполнялся в составной корень.

Читатели исходного кода вашего приложения (вы и члены вашей команды) должны иметь одно место (корень композиции), чтобы понять, как вы создаете свои объекты для решения своей конкретной проблемы, используя общие классы (например, BackupMaker).

Иными словами, корень композиции - это то место, где вы решаете специфику. Его где вы используете общий для создания конкретного.

Чтобы ответить на комментарий:

который должен знать, что относительно этих зависимостей?

Корень композиции должен знать обо всем (все зависимости), поскольку он создает все объекты в приложении и соединяет их вместе. Корень композиции знает, что делает каждая часть головоломки, и соединяет их вместе для создания содержательного приложения.

Для BackupMaker он должен заботиться только о том, чтобы иметь возможность выполнять свою единственную ответственность. В вашем примере его единственная (простая) ответственность (как мне кажется) заключается в том, чтобы упорядочить потребление других объектов для создания резервной копии.

Пока вы используете DI, класс никогда не будет уверен, что его соавтор будет вести себя корректно, только корневой состав будет. Рассмотрим этот простой и экстремальный пример реализации IDatabaseExporter (предположим, что разработчик действительно дал этому классу это имя и что он намеренно реализовал его таким образом):

public class StupidDisastrousDatabaseExporter : IDatabaseExporter
{
    public ExportedData Export()
    {
        DoSomethingStupidThatWillDeleteSystemDataAndMakeTheEnterpriseBroke();

        ...
    }

    private void DoSomethingStupidThatWillDeleteSystemDataAndMakeTheEnterpriseBroke()
    {
        //do it
        ...
    }
}

Теперь BackupMaker никогда не узнает, что он потребляет глупый и катастрофический экспортер баз данных, но только корневой каталог. Мы никогда не можем обвинить программиста, написавшего класс BackupMaker для этой катастрофической ошибки (или программиста, который разработал контракт IDatabaseExporter). Но программист (программисты), составляющие приложение в корне композиции, обвиняются, если они вставляют экземпляр StupidDisastrousDatabaseExporter в конструктор BackupMaker.

Конечно, никто не должен был писать класс StupidDisastrousDatabaseExporter, но я привел вам крайний пример, чтобы показать вам, что контракт (интерфейс) никогда не сможет (и никогда не должен) гарантировать каждый аспект о его разработчиках. Он должен просто сказать достаточно.

Есть ли способ выразить IDatabaseExporter таким образом, чтобы гарантировать, что разработчики такого интерфейса не будут совершать глупые или катастрофические действия? Нет.

Обратите внимание, что в то время как BackupMaker имеет дело с контрактами (без гарантии 100%), корень состава фактически имеет дело с конкретными классами реализации. Это дает ему великую силу (и, следовательно, большую ответственность) гарантировать состав правильного объектного графа.

как я уверен, что я сочиняю разумным способом?

Вы должны создать автоматические сквозные тесты для графа объектов, созданного корнем композиции. Здесь вы убедитесь, что корневой состав выполнил большую ответственность за правильное составление объектов. Здесь вы можете проверить точные данные, которые вы хотели (например, результат резервного копирования был в определенном формате/деталях).

Посмотрите эту статью для подхода к автоматическому тестированию с использованием корня композиции.

Ответ 4

Я считаю, что это может быть проблемой, возникающей при слишком большой фокусировке на объектных моделях, исключая функциональные композиции. Рассмотрим первый шаг в наивном разложении функции (функция как в f: a → b):

exporter: data -> (format, memory), or exception

compressor: memory -> memory, or exception

writer: memory -> side-effect, or exception 

backup-maker: (data, exporter, compressor, writer) -> backup-result

Таким образом, резервная копия, последняя функция, может быть параметризована с этими тремя функциями, предполагая, что я правильно рассмотрел ваш прецедент, и если три параметра имеют одинаковые типы ввода и вывода, например, формат и память, несмотря на их реализацию.

Теперь "кишки" или возможное разложение (чтение справа налево) создателя резервной копии со всеми связанными функциями, взятие данных в качестве аргумента и использование оператора композиции ".:

backup-maker: промежуточное вычисление. писатель. промежуточное вычисление. компрессор. промежуточное вычисление. экспортер

Я особенно хочу отметить, что эта модель архитектуры может быть выражена позже как объектные интерфейсы или как первоклассные функции, например. С++ std:: function.

Изменить: он также может быть уточнен для терминов generics, где память является аргументом общего типа, чтобы обеспечить безопасность типа, когда это требуется. Например.

backup-maker<type M>: (data, exporter<M>, compressor<M>, writer<M>) -> ..

Более подробную информацию о технике и преимуществах разложения функций можно найти здесь: http://jfeltz.com/posts/2015-08-30-cost-decreasing-software-architecture.html

Ответ 5

Ваши требования кажутся противоречивыми:

  • Вы хотите быть конкретным (разрешая только подмножество (или только одно?) комбинаций)
  • Но вы также хотите быть общим с использованием интерфейсов, DI и т.д.

Мой совет заключается в том, чтобы держать вещи простыми (в вашем случае это означает, что вы не пытаетесь быть универсальными), пока ваш код не изменится.

Только, когда ваш код будет развиваться, рефакторинг будет более общим. Код ниже показывает компромисс между общим/конкретным:

public interface ICompressor {
    public byte[] compress(byte[] source); //Note: the return type and type argument may not be revelant, just for demonstration purpose
}

public interface IExporter {
    public File export(String connectionString); //Note: the return type and type argument may not be revelant, just for demonstration purpose
}

public final class Bzip2 implements ICompressor {
    @Override
    public final byte[] compress(byte[] source) {
        //TODO
    }
}

public final class MySQL implements IExporter {
    @Override
    public final File export(String connnectionString) {
        //TODO
    }
}


public abstract class ABackupStrategy {
    private final ICompressor compressor;
    private final IExporter exporter;

    public ABackupStrategy(final ICompressor compressor, final IExporter exporter) {
        this.compressor = compressor;
        this.exporter = exporter;
    }

    public final void makeBackup() {
        //TODO: compose with exporter and compressor to make your backup
    }
}

public final class MyFirstBackupStrategy extends ABackupStrategy {
    public MyFirstBackupStrategy(final Bzip2 compressor, final MySQL exporter) {
        super(compressor, exporter);
    }
}

С ICompressor и IExporter вы можете легко добавить другой алгоритм сжатия, другую базу данных, из которой можно экспортировать.

С помощью ABackupStrategy вы можете легко определить новую разрешенную комбинацию конкретного компрессора/экспортера, наследуя ее.

Недостаток: Мне пришлось сделать ABackupStrategy abstract без объявления какого-либо метода abstract, что противоречит принципам ООП.