Цель моей задачи - спроектировать небольшую систему, которая может выполнять запланированные повторяющиеся задачи. Повторяющаяся задача - это что-то вроде «отправлять электронное письмо администратору каждый час с 8:00 до 17:00 с понедельника по пятницу».
У меня есть базовый класс с именем RecurringTask .
public abstract class RecurringTask{
// I've already figured out this part
public bool isOccuring(DateTime dateTime){
// implementation
}
// run the task
public abstract void Run(){
}
}
И у меня есть несколько классов, которые унаследованы от RecurringTask . Один из них называется SendEmailTask .
public class SendEmailTask : RecurringTask{
private Email email;
public SendEmailTask(Email email){
this.email = email;
}
public override void Run(){
// need to send out email
}
}
И у меня есть EmailService, который может помочь мне отправить электронное письмо.
Последний класс - RecurringTaskScheduler , он отвечает за загрузку задач из кэша или базы данных и запуск задачи.
public class RecurringTaskScheduler{
public void RunTasks(){
// Every minute, load all tasks from cache or database
foreach(RecuringTask task : tasks){
if(task.isOccuring(Datetime.UtcNow)){
task.run();
}
}
}
}
Вот моя проблема: где я должен поставить EmailService ?
Вариант 1 : внедрить EmailService в SendEmailTask
public class SendEmailTask : RecurringTask{
private Email email;
public EmailService EmailService{ get; set;}
public SendEmailTask (Email email, EmailService emailService){
this.email = email;
this.EmailService = emailService;
}
public override void Run(){
this.EmailService.send(this.email);
}
}
Уже есть некоторые дискуссии о том, следует ли нам внедрять сервис в сущность, и большинство людей согласны с тем, что это не очень хорошая практика. Смотрите эту статью .
Вариант 2: Если ... Остальное в RecurringTaskScheduler
public class RecurringTaskScheduler{
public EmailService EmailService{get;set;}
public class RecurringTaskScheduler(EmailService emailService){
this.EmailService = emailService;
}
public void RunTasks(){
// load all tasks from cache or database
foreach(RecuringTask task : tasks){
if(task.isOccuring(Datetime.UtcNow)){
if(task is SendEmailTask){
EmailService.send(task.email); // also need to make email public in SendEmailTask
}
}
}
}
}
Мне сказали, что если ... иначе и приведение, как указано выше, не является ОО, и принесет больше проблем.
Вариант 3: измените подпись Run и создайте ServiceBundle .
public class ServiceBundle{
public EmailService EmailService{get;set}
public CleanDiskService CleanDiskService{get;set;}
// and other services for other recurring tasks
}
Вставьте этот класс в RecurringTaskScheduler
public class RecurringTaskScheduler{
public ServiceBundle ServiceBundle{get;set;}
public class RecurringTaskScheduler(ServiceBundle serviceBundle){
this.ServiceBundle = ServiceBundle;
}
public void RunTasks(){
// load all tasks from cache or database
foreach(RecuringTask task : tasks){
if(task.isOccuring(Datetime.UtcNow)){
task.run(serviceBundle);
}
}
}
}
Run метод SendEmailTask будет
public void Run(ServiceBundle serviceBundle){
serviceBundle.EmailService.send(this.email);
}
Я не вижу больших проблем с этим подходом.
Вариант 4 : Шаблон посетителя.
Основная идея - создать посетителя, который будет инкапсулировать сервисы, как ServiceBundle .
public class RunTaskVisitor : RecurringTaskVisitor{
public EmailService EmailService{get;set;}
public CleanDiskService CleanDiskService{get;set;}
public void Visit(SendEmailTask task){
EmailService.send(task.email);
}
public void Visit(ClearDiskTask task){
//
}
}
И нам также нужно изменить сигнатуру метода Run . Метод Run метода SendEmailTask :
public void Run(RecurringTaskVisitor visitor){
visitor.visit(this);
}
Это типичная реализация шаблона посетителя, и посетитель будет вставлен в RecurringTaskScheduler .
Резюмируя: какой из этих четырех подходов является лучшим для моего сценария? И есть ли большая разница между Option3 и Option4 для этой проблемы?
Или у вас есть лучшее представление об этой проблеме? Спасибо!
Обновление 22.05.2015 : Я думаю, что ответ Энди очень хорошо отражает мое намерение; если вы все еще не уверены в самой проблеме, советую сначала прочитать его пост.
Я только что узнал, что моя проблема очень похожа на проблему отправки сообщений , которая приводит к Option5.
Вариант 5 : преобразовать мою проблему в рассылку сообщений .
Между моей проблемой и проблемой отправки сообщений существует взаимно-однозначное соответствие :
Диспетчер сообщений : получение IMessage и отправка подклассов IMessage их соответствующим обработчикам. → RecurringTaskScheduler
IMessage : интерфейс или абстрактный класс. → RecurringTask
MessageA : расширяется от IMessage , имея некоторую дополнительную информацию. → SendEmailTask
MessageB : еще один подкласс IMessage . → CleanDiskTask
MessageAHandler : при получении MessageA обработайте его → SendEmailTaskHandler, который содержит EmailService, и отправит электронное письмо при получении SendEmailTask.
MessageBHandler : то же, что MessageAHandler , но вместо этого обрабатывается MessageB . → CleanDiskTaskHandler
Самое сложное - как отправить разные сообщения IM различным обработчикам. Вот полезная ссылка .
Мне действительно нравится этот подход, он не загрязняет мою сущность служением, и у него нет никакого класса Бога .
SendEmailTask
для меня это больше похоже на услугу, чем на сущность. Я бы пошел на вариант 1 без колебаний.accept
посещают посетители. Мотивация для посетителя состоит в том, что в некотором агрегате есть много типов классов, которые необходимо посетить, и не удобно изменять их код для каждой новой функциональности (операции). Я до сих пор не вижу, что это за совокупные объекты, и думаю, что Visitor не подходит. Если это так, вы должны отредактировать свой вопрос (который относится к посетителю).Ответы:
Я бы сказал, Вариант 1 - лучший маршрут. Причина, по которой вы не должны отклонять это, заключается в том, что
SendEmailTask
это не сущность. Сущность - это объект, связанный с хранением данных и состояния. В вашем классе этого очень мало. Фактически, это не сущность, но она содержит сущность:Email
объект, который вы храните. Это означает, чтоEmail
не следует брать услугу или иметь#Send
метод. Вместо этого у вас должны быть службы, которые принимают организации, такие как вашаEmailService
. Таким образом, вы уже придерживаетесь идеи не предоставлять услуги лицам.Поскольку
SendEmailTask
это не сущность, поэтому совершенно нормально внедрить в нее электронную почту и сервис, и это следует делать через конструктор. Выполняя инъекцию конструктора, мы можем быть уверены, чтоSendEmailTask
всегда готовы выполнить свою работу.Теперь давайте посмотрим, почему бы не сделать другие варианты (особенно в отношении SOLID ).
Вариант 2
Тебе справедливо сказали, что подобные ветвления приведут к дальнейшим головным болям. Давайте посмотрим, почему. Во-первых, они
if
имеют тенденцию группироваться и расти. Сегодня задача отправлять электронные письма, завтра каждому классу требуется другой сервис или другое поведение. Управление этимif
заявлением становится кошмаром. Поскольку мы разветвляемся на тип (и в данном случае явный тип ), мы подрываем систему типов, встроенную в наш язык.Вариант 2 не является Single Responsibility (SRP), потому что ранее многократно используемый
RecurringTaskScheduler
теперь должен знать обо всех этих различных типах задач, а также обо всех различных видах услуг и поведений, которые могут им понадобиться. Этот класс гораздо сложнее использовать повторно. Он также не открыт / закрыт (OCP). Поскольку ему необходимо знать об этой задаче или о той (или о той или иной услуге), несопоставимые изменения в задачах или службах могут вызвать изменения здесь. Добавить новое задание? Добавить новый сервис? Изменить способ обработки электронной почты? ИзменитьRecurringTaskScheduler
. Поскольку тип задачи имеет значение, он не соответствует замене Лискова (LSP). Это не может просто получить задачу и быть выполненным. Он должен запросить тип и в зависимости от типа сделать это или сделать это. Вместо того, чтобы заключать различия в задачи, мы включаем все это вRecurringTaskScheduler
.Вариант 3
Вариант 3 имеет некоторые большие проблемы. Даже в статье, на которую вы ссылаетесь , автор не рекомендует делать это:
Вы создаете сервисный локатор со своим
ServiceBundle
классом. В этом случае он не выглядит статичным, но он все еще имеет много проблем, присущих локатору службы. Ваши зависимости теперь скрыты под этимServiceBundle
. Если я дам вам следующий API моей классной новой задачи:Какие услуги я использую? Какие сервисы нужно макетировать в тесте? Что мешает мне использовать каждый сервис в системе, просто потому что?
Если я хочу использовать вашу систему задач для выполнения некоторых задач, я теперь зависим от каждого сервиса в вашей системе, даже если я использую только несколько или вообще не использую их.
На
ServiceBundle
самом деле это не SRP, потому что он должен знать о каждой службе в вашей системе. Это также не OCP. Добавление новых служб означает изменения вServiceBundle
, а измененияServiceBundle
могут означать несопоставимые изменения в задачах в других местах.ServiceBundle
не разделяет свой интерфейс (ISP). Он имеет обширный интерфейс всех этих сервисов, и, поскольку он является просто поставщиком этих сервисов, мы могли бы рассмотреть его интерфейс, чтобы охватить интерфейсы всех сервисов, которые он предоставляет. Задачи больше не придерживаются Deverdency Inversion (DIP), потому что их зависимости скрыты заServiceBundle
. Это также не соответствует принципу наименьшего знания (так называемый закон Деметры), потому что вещи знают о гораздо большем, чем должны.Вариант 4
Раньше у вас было много небольших объектов, которые могли работать независимо друг от друга. Вариант 4 берет все эти объекты и объединяет их в один
Visitor
объект. Этот объект действует как объект бога над всеми вашими задачами. Это уменьшает вашиRecurringTask
объекты до анемичных теней, которые просто вызывают посетителя. Все поведение переходит кVisitor
. Нужно изменить поведение? Нужно добавить новое задание? ИзменитьVisitor
.Более сложная часть состоит в том, что, поскольку все различные виды поведения находятся в одном классе, изменение некоторых полиморфно затягивает вместе с другим поведением. Например, мы хотим иметь два разных способа отправки электронной почты (возможно, они должны использовать разные серверы?). Как бы мы это сделали? Мы могли бы создать
IVisitor
интерфейс и реализовать его, потенциально дублируя код, как#Visit(ClearDiskTask)
у нашего первоначального посетителя. Тогда, если мы придумаем новый способ очистки диска, нам придется внедрить и повторить снова. Тогда мы хотим оба вида изменений. Внедрить и повторить снова. Эти два разных, несопоставимых поведения неразрывно связаны.Может быть, вместо этого мы могли бы просто подкласс
Visitor
? Подкласс с новым поведением электронной почты, подкласс с новым поведением диска. Пока без дублирования! Подкласс с обоими? Теперь один или другой должен быть дублирован (или оба, если вы предпочитаете).Давайте сравним с вариантом 1: нам нужно новое поведение электронной почты. Мы можем создать новый,
RecurringTask
который выполняет новое поведение, внедрить его в зависимости и добавить его в коллекцию задач вRecurringTaskScheduler
. Нам даже не нужно говорить об очистке дисков, потому что эта ответственность лежит где-то еще. У нас также все еще есть полный набор инструментов OO. Мы могли бы украсить эту задачу, например, регистрацией.Вариант 1 даст вам наименьшую боль и является наиболее правильным способом справиться с этой ситуацией.
источник
SendEmailTask
базы данных, то эта конфигурация должна быть отдельным классом конфигурации, который также должен быть введен в вашSendEmailTask
. Если вы генерируете данные из своегоSendEmailTask
, вы должны создать объект memento для хранения состояния и поместить его в свою базу данных.EMailTaskDefinitions
иEmailService
вSendEmailTask
? ЗатемRecurringTaskScheduler
, мне нужно внедрить что-то вроде того,SendEmailTaskRepository
чья ответственность заключается в загрузке определения и обслуживания, и внедрить их вSendEmailTask
. Но я бы поспорил сейчас оRecurringTaskScheduler
необходимости знать репозиторий каждой задачи, вроде быCleanDiskTaskRepository
. И мне нужно менятьRecurringTaskScheduler
каждый раз, когда у меня появляется новое задание (добавить репозиторий в планировщик).RecurringTaskScheduler
Следует знать только о концепции обобщенного хранилища задач и aRecurringTask
. Делая это, это может зависеть от абстракций. Репозитории задач могут быть введены в конструкторRecurringTaskScheduler
. Тогда разные репозитории нужно знать только, где ониRecurringTaskScheduler
создаются (или могут быть спрятаны на фабрике и вызваны оттуда). Потому что это зависит только от абстракций,RecurringTaskScheduler
не нужно менять с каждой новой задачей. В этом суть инверсии зависимости.Вы смотрели на существующие библиотеки, например, весенний кварц или весеннюю партию (я не уверен, что больше всего соответствует вашим потребностям)?
На ваш вопрос:
Я предполагаю, что проблема заключается в том, что вы хотите полиморфно сохранить некоторые метаданные в задаче, поэтому задаче электронной почты назначены адреса электронной почты, задаче журнала - уровень журнала и так далее. Вы можете сохранить список тех, кто находится в памяти или в вашей базе данных, но для разделения проблем вы не хотите, чтобы объект был загрязнен служебным кодом.
Мое предлагаемое решение:
Я бы разделил выполнение и часть данных задачи, чтобы иметь, например,
TaskDefinition
и aTaskRunner
. TaskDefinition имеет ссылку на TaskRunner или фабрику, которая его создает (например, если требуется какая-то настройка, например, smtp-host). Фабрика специфична - она может обрабатывать толькоEMailTaskDefinition
s и возвращает только экземплярыEMailTaskRunner
s. Таким образом, это больше ОО и безопасное изменение - если вы вводите новый тип задачи, вы должны вводить новую конкретную фабрику (или повторно использовать), если вы этого не делаете, вы не можете скомпилировать.Таким образом, вы получите в итоге зависимость: уровень сущности -> уровень обслуживания и обратно, потому что Бегуну нужна информация, хранящаяся в сущности, и, вероятно, он хочет обновить ее состояние в БД.
Вы могли бы разорвать этот порочный круг, используя общий завод, который берет на TaskDefinition и возвращает специфический TaskRunner, но это потребует много МФСА. Вы можете использовать рефлексию, чтобы найти бегуна, который так же называется вашим определением, но будьте осторожны, такой подход может стоить некоторой производительности и может привести к ошибкам во время выполнения.
PS Я предполагаю, что Java здесь. Я думаю, что это похоже на .net. Основной проблемой здесь является двойное связывание.
Для шаблона посетителя
Я думаю, что он был скорее предназначен для обмена алгоритмом для различных типов объектов данных во время выполнения, чем для целей чистого двойного связывания. Например, если у вас есть разные виды страхования и разные виды их расчета, например, потому что это требуется в разных странах. Затем вы выбираете конкретный метод расчета и применяете его для нескольких страховок.
В вашем случае вы бы выбрали конкретную стратегию задач (например, электронную почту) и применили ее ко всем своим задачам, что неправильно, потому что не все они являются задачами электронной почты.
PS Я не тестировал его, но думаю, что ваш вариант 4 тоже не сработает, потому что он снова имеет двойную привязку.
источник
Я полностью не согласен с этой статьей. Службы (конкретно их «API») являются важной стороной бизнес-домена и, как таковые, будут существовать в рамках доменной модели. И нет проблем с сущностями в бизнес-домене, ссылающимися на что-то еще в том же бизнес-домене.
Это бизнес-правило. И для этого нужен сервис, который отправляет почту. И субъект, который обрабатывает,
When X
должен знать об этом сервисе.Но есть некоторые проблемы с реализацией. Для пользователя объекта должно быть понятно, что объект использует службу. Поэтому добавление службы в конструктор не очень хорошая вещь. Это также проблема, когда вы десериализуете сущность из базы данных, потому что вам нужно установить как данные сущности, так и экземпляры сервисов. Лучшее решение, которое я могу придумать, - это использование инъекции свойств после создания сущности. Возможно, заставляя каждый вновь созданный экземпляр любой сущности проходить через метод «инициализации», который внедряет все сущности, в которых нуждается эта сущность.
источник
Это отличный вопрос и интересная проблема. Я предлагаю вам использовать комбинацию шаблонов Chain of Responsibility и Double Dispatch (примеры шаблонов здесь ).
Сначала давайте определим иерархию задач. Обратите внимание, что теперь есть несколько
run
методов для реализации двойной отправки.Далее давайте определим
Service
иерархию. Мы будем использоватьService
s для формирования Цепочки Ответственности.Последний элемент - это
RecurringTaskScheduler
процесс загрузки и запуска.Теперь вот пример приложения, демонстрирующего систему.
Запуск выходных данных приложения:
EmailService с запущенным SendEmailTask с контентом «здесь приходит первое электронное письмо»
EmailService с запущенным SendEmailTask с контентом »здесь второе электронное письмо«
ExecuteService с запущенным ExecuteTask с контентом »/ root / python«
ExecuteService с запущенным ExecuteTask с контентом »/ bin / cat«
EmailService с запущенным SendEmailTask с content 'вот третье электронное письмо'
ExecuteService, на котором выполняется ExecuteTask с контентом '/ bin / grep'
источник