Избегание связи со стратегическим шаблоном

#c# #oop #design-patterns #strategy-pattern #decoupling

#c# #ооп #шаблоны проектирования #стратегия-шаблон #развязка

Вопрос:

Я пытаюсь применить шаблон стратегии к конкретной ситуации, но у меня возникла проблема с тем, как избежать связывания каждой конкретной стратегии с контекстным объектом, предоставляющим для нее данные. Ниже приведен упрощенный пример шаблона, который встречается несколькими различными способами, но должен обрабатываться аналогичным образом.

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

Итак, представьте следующие (значительно упрощенные) классы:

 class Acquisition
{
    public Int32 IntegrationTime { get; set; }
    public Double Battery { get; set; }
    public Double Signal { get; set; }
}

interface IAnalogOutputter
{
    double getVoltage(Acquisition acq);
}

class BatteryAnalogOutputter : IAnalogOutputter
{
    double getVoltage(Acquisition acq)
    {
        return acq.Battery;
    }
}
  

Теперь каждый конкретный класс стратегии должен быть связан с моим классом Acquisition, который также является одним из наиболее вероятных классов, подлежащих модификации, поскольку он является ядром нашего приложения. Это все еще улучшение по сравнению со старым дизайном, который представлял собой гигантскую инструкцию switch внутри Acquisition класса. У каждого типа данных может быть свой метод преобразования (в то время как Battery — это простой переход, другие совсем не так просты), поэтому я чувствую, что шаблон стратегии или аналогичный должен быть правильным решением.

Я также отмечу, что в окончательной реализации IAnalogOutputter вместо интерфейса был бы абстрактный класс. Эти классы будут в списке, который настраивается пользователем и сериализуется в XML-файл. Список должен быть доступен для редактирования во время выполнения и запоминаться, поэтому Serializable должен быть частью нашего окончательного решения. На случай, если это имеет значение.

Как я могу гарантировать, что каждый класс реализации получает данные, необходимые для работы, не привязывая их к одному из моих самых важных классов? Или я подхожу к такого рода проблеме совершенно неправильным образом?

Ответ №1:

Strategy Pattern инкапсулирует — обычно сложную — операцию / вычисление.

Напряжение, которое вы хотите вернуть, зависит от

  • элементы конфигурации
  • Некоторые данные сбора

Поэтому я бы поместил их в другой класс и передал его разработчикам стратегии.

Также с точки зрения сериализации вам не нужно сериализовать классы стратегий, возможно, только их имя или название типа.


Обновить

Что ж, похоже, что вашим реализациям требуется только одна часть данных сбора. Это немного необычно для шаблона стратегии — но я не думаю, что это подходит Visitor лучше, поэтому стратегия в порядке. Я бы создал класс, который имеет в качестве свойства данные сбора (возможно, наследуемые от него) в дополнение к конфигурации, которая нужна разработчикам.

Комментарии:

1. Для пояснения — данные сбора содержат около 20 фрагментов информации, все из которых будут использоваться различными устройствами вывода напряжения. Каждая реализация будет использовать только один фрагмент данных, но будет одинаковое количество (20) различных возможных реализаций вывода напряжения. Итак, как мне перейти от Acquisition к «другому классу [для] передачи его разработчикам стратегии»? Хороший момент по поводу сериализации, хотя я обязательно рассмотрю это.

Ответ №2:

Одна вещь, которую вы могли бы сделать, это использовать фабричные методы для построения ваших стратегий. Ваши индивидуальные стратегии могут использовать в своем конструкторе только отдельные элементы данных, которые им нужны, и фабричный метод — это единственное, что нужно знать, как заполнить эти данные с учетом Acquisition объекта. Что-то вроде этого:

 public class OutputterFactory
{
    public static IAnalogOutputter CreateBatteryAnalogOutputter(Acquisition acq)
    {
        return new BatteryANalogOutputter(acq.Battery);
    }



}
  

Комментарии:

1. Я думал об этом, но тогда, похоже, для каждого нового фрагмента данных я должен добавлять свойство, метод и класс. Хотя это немного больше разъединяет его, это увеличивает количество ошибок в степени 3. Но теперь мне интересно, было бы лучшим вариантом просто использовать отдельные методы для этого.

Ответ №3:

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

 interface IOutputValueProvider
{
    Double GetBattery();
    Double GetSignal();
    Int32 GetIntegrationTime();
    Double GetDictionaryValue(String key);
}

interface IAnalogOutputter
{
    double getVoltage(IOutputValueProvider provider);
}

class BatteryAnalogOutputter : IAnalogOutputter
{
    double getVoltage(IOutputValueProvider provider)
    {
        return provider.GetBattery();
    }
}

class DictionaryValueOutputter : IAnalogOutputter
{
    public String DictionaryKey { get; set; }
    public double getVoltage(IOutputValueProvider provider)
    {
        return provider.GetDictionaryValue(DictionaryKey);
    }
}
  

Итак, мне просто нужно убедиться, что Acquisition реализует интерфейс:

 class Acquisition : IOutputValueProvider
{
    public Int32 IntegrationTime { get; set; }
    public Double Battery { get; set; }
    public Double Signal { get; set; }
    public Dictionary<String, Double> DictionaryValues;

    public double GetBattery() { return Battery;}
    public double GetSignal() { return Signal; }
    public int GetIntegrationTime() { return IntegrationTime; }
    public double GetDictionaryValue(String key) 
    {
        Double d = 0.0;
        return DictionaryValues.TryGetValue(key, out d) ? d : 0.0;
    }
}
  

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