Вопрос дизайна - как вы нарушаете зависимость между классами, используя интерфейс? - PullRequest
3 голосов
/ 02 июня 2010

Я заранее извиняюсь, но это будет длинный вопрос.

Я застрял. Я пытаюсь изучить модульное тестирование, C # и шаблоны проектирования - все сразу. (Может быть, это моя проблема.) Поэтому я читаю «Искусство модульного тестирования» (Ошерове), «Чистый код» (Мартин) и «Шаблоны проектирования первых конструкций» (О'Рейли).

Я только сейчас начинаю понимать делегатов и события (которые вы бы увидели, если бы троллили мои так недавние вопросы). Я до сих пор не совсем получил лямбды.

Для контекстуализации всего этого я дал себе учебный проект, который я называю goAlarms. У меня есть класс Alarm с членами, которых вы ожидаете (NextAlarmTime, Name, AlarmGroup, Event Trigger и т. Д.)

Я хотел, чтобы «Таймер» тревоги был расширяемым, поэтому я создал интерфейс IAlarmScheduler следующим образом ...

public interface AlarmScheduler
{
    Dictionary<string,Alarm> AlarmList { get; }
    void Startup();
    void Shutdown();
    void AddTrigger(string triggerName, string groupName, Alarm alarm);
    void RemoveTrigger(string triggerName);
    void PauseTrigger(string triggerName);
    void ResumeTrigger(string triggerName);
    void PauseTriggerGroup(string groupName);
    void ResumeTriggerGroup(string groupName);
    void SetSnoozeTrigger(string triggerName, int duration);
    void SetNextOccurrence (string triggerName, DateTime nextOccurrence);
}

Этот интерфейс IAlarmScheduler определяет компонент, который ИСПОЛЬЗУЕТ сигнал тревоги (триггер), который поднимется до моего класса тревоги и вызовет событие триггера самого сигнала тревоги. По сути, это компонент «Таймер».

Я обнаружил, что компонент Quartz.net идеально подходит для этого, поэтому я создал класс QuartzAlarmScheduler, который реализует IAlarmScheduler.

Все в порядке. Моя проблема в том, что класс Alarm является абстрактным, и я хочу создать много разных типов тревог. Например, у меня уже есть сигнал сердцебиения (срабатывает через каждые (int) интервал минут), AppointmentAlarm (срабатывает в установленные дату и время), ежедневный сигнал тревоги (срабатывает каждый день в X) и, возможно, другие.

И Quartz.NET идеально подходит для этого.

Моя проблема - проблема дизайна. Я хочу иметь возможность создавать тревогу любого рода без моего класса Alarm (или любых производных классов), ничего не знающего о Quartz. Проблема в том, что в Quartz есть отличные фабрики, которые возвращают только правильные настройки для триггеров, которые будут нужны моим классам Alarm. Так, например, я могу получить триггер Quartz, используя TriggerUtils.MakeMinutelyTrigger, чтобы создать триггер для сигнала сердцебиения, описанного выше. Или TriggerUtils.MakeDailyTrigger для ежедневной тревоги.

Полагаю, я мог бы подвести итог так. Косвенно или напрямую я хочу, чтобы мои классы сигналов тревоги могли использовать классы TriggerUtils.Make *, ничего не зная о них. Я знаю, что это противоречие, но именно поэтому я задаю вопрос.

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

Вы, ребята, потрясающие, и заранее спасибо за ваши ответы.

Сет

Ответы [ 4 ]

1 голос
/ 02 июня 2010

Возможно, у вас может быть класс AlarmFactory, который будет возвращать соответствующий ссылочный тип IAlarmScheduler. Это может быть один из способов изолировать знания о Кварце и его связи с различными подклассами.

Я не уверен, что можно будет сделать так, чтобы подклассы IAlarmScheduler полностью не знали о деталях Quartz, но вполне возможно сделать так, чтобы клиенты из IAlarmScheduler были. Вот что делает интерфейс и заводская комбинация для вас.

0 голосов
/ 04 июня 2010

Я согласен с Робом. AlarmScheduler - это не то, что требует интерфейса. ITrigger нужен интерфейс.

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

Отсюда у вас есть выбор, как включить ITriggers в Планировщик.

  1. Создайте загрузчик, чтобы вручную создавать его экземпляры при запуске.
  2. Используйте каркас Dependency Injection, чтобы сделать то же самое.
  3. Создать класс AlarmFactory с такими методами, как:

ITrigger CreateMyCustomTriggerType1(param1, param2, param3)
и
ITrigger CreateMyCustomTriggerType2(param1)

И пока мы находимся на этой теме - нет ничего плохого в использовании Enums для определения типа. Использование enum диктует какой-то оператор Switch, чтобы решить, какой тип создать. С точки зрения дизайна, нет проблем с этим, пока оператор switch не повторяется. Если вы изолируете оператор Switch за методом FACTORY, вы проявили должную осмотрительность в том, что касается хорошего дизайна ООП.

0 голосов
/ 02 июня 2010

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

Прежде всего, на данный момент, просто с целью изучения правильного ОВД, забудьте о делегатах, событиях и лямбдах. Есть причина, по которой я это говорю, вы поймете это позже. А пока сосредоточимся на интерфейсах и их реализациях. То, что вы называете «событием», также может быть реализовано как интерфейс (например, IHaveAMethodThatYouShouldCall с методом, являющимся TheMethodToCall).

Теперь ваш Alarm будет реализовывать интерфейс IAmAnAlarmAndWhenAlarmSchedulerThinkItIsTheTimeHeWillLetMeKnow, при этом единственным методом является ItIsTheTime.

Ваш AlarmScheduler реализует интерфейс IAmAlarmSchedulerThatWillNotifyAlarmWhenItIsTheTime с единственным методом RegisterAlarm (IAmAnAlarmAndWhenAlarmSchedulerThinkItIsTheTimeHeWillLetMeKnow alarm).

Кроме того, ваш Alarm примет в своем конструкторе объект типа IAmAlarmSchedulerThatWillNotifyAlarmWhenItIsTheTime и вызовет свой RegisterAlarm, передав ему свой собственный «я». В этот момент фактический AlarmScheduler, переданный внутрь, сохранит указатель на Alarm в своей закрытой переменной и при необходимости вызовет метод ItIsTheTime.

Всякий раз, когда вы создаете Alarm, вы сначала должны создать AlarmScheduler и передать его экземпляр конструктору Alarm.

Надеюсь, это имеет смысл.

0 голосов
/ 02 июня 2010

Это довольно распространенная проблема.Вы не управляете базовым классом фреймворка и хотите кодировать интерфейс.Эта же проблема возникает в ASP.NET с классом HttpContext.Если вы заглянете в HttpContextWrapper ASP.NET MVC, вы найдете множество полезных статей, в которых обсуждается, как Microsoft разорвала зависимость с пространством имен System.Web.Abstractions.

Вот краткое содержание.Это трудное решение, но это хорошо понятный шаблон.

Вы определяете AlarmSchedulerWrapper, который принимает IAlarmScheduler в качестве параметра конструктора.AlarmSchedulerWrapper перенаправляет все вызовы составленному IAlarmScheduler.Теперь вы можете передать свой QuartzAlarmScheduler, который реализует IAlarmScheduler.Это единственное место, где вам понадобится зависимость.

Для тестирования вы можете либо смоделировать AlarmScheduler, либо создать двойное значение (AlarmSchedulerDouble).AlarmSchedulerDouble может быть написано для имитации событий тревоги и записи вызовов против него (делая его «шпионом»).

Если вы собираетесь реализовать множество конкретных экземпляров AlarmScheduler, вы, как правило, также создадите AlarmSchedulerBase,AlarmSchedulerWrapper и AlarmSchedulerDouble будут использовать его в качестве своего базового класса.

Редактировать: Прочитайте ваш вопрос более подробно, и я вижу, что это триггеры, которые вы должны абстрагировать.Аналогичный ответ.Определите IAlarmTriggers, оболочку, которая реализует их путем пересылки в Quartz, и двойник для тестирования.Обратите внимание, что триггерные утилиты, вероятно, статичны.Тем не менее, просто составьте их из экземпляра класса.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...