Является ли Шаблон посетителя действительным в этом сценарии?

9

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

Мне действительно нравится этот подход, он не загрязняет мою сущность служением, и у него нет никакого класса Бога .

Sher10ck
источник
Вы не отметили язык или платформу, но я рекомендую заглянуть в cron . Ваша платформа может иметь библиотеку, которая работает аналогичным образом (например, jcron, который кажется неработающим). Планирование заданий и заданий в значительной степени решаемая проблема: рассматривали ли вы другие варианты, прежде чем предлагать свои собственные? Были ли причины не использовать их?
@ Снеговик Мы можем перейти к зрелой библиотеке позже. Все зависит от моего менеджера. Причина, по которой я задаю этот вопрос, заключается в том, что я хочу найти способ решить эту «проблему». Я видел такую ​​проблему не раз и не мог найти элегантного решения. Поэтому мне интересно, сделал ли я что-то не так.
Sher10ck
Справедливо, я всегда стараюсь рекомендовать повторное использование кода, если это возможно.
1
SendEmailTaskдля меня это больше похоже на услугу, чем на сущность. Я бы пошел на вариант 1 без колебаний.
Барт ван Инген Шенау
3
Чего не хватает (для меня) посетителю, так это структуры классов, которую acceptпосещают посетители. Мотивация для посетителя состоит в том, что в некотором агрегате есть много типов классов, которые необходимо посетить, и не удобно изменять их код для каждой новой функциональности (операции). Я до сих пор не вижу, что это за совокупные объекты, и думаю, что Visitor не подходит. Если это так, вы должны отредактировать свой вопрос (который относится к посетителю).
Фурманатор

Ответы:

4

Я бы сказал, Вариант 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 моей классной новой задачи:

class MyCoolNewTask implements RecurringTask
{
    public bool isOccuring(DateTime dateTime) {
        return true; // It's always happenin' here!
    }

    public void Run(ServiceBundle bundle) {
        // yeah, some awesome stuff here
    }
}

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

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

На 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 даст вам наименьшую боль и является наиболее правильным способом справиться с этой ситуацией.

cbojar
источник
Ваш анализ на Otion2,3,4 фантастический! Это действительно очень помогает мне. Но для Варианта 1 я бы сказал, что * SendEmailTask ​​* является сущностью. У него есть идентификатор, у него есть повторяющийся шаблон и другая полезная информация, которая должна храниться в БД. Я думаю, что Энди суммирует мое намерение хорошо. Может быть, имя типа * EMailTaskDefinitions * более уместно. я не хочу загрязнять свою сущность своим сервисным кодом. В эйфории упоминается некоторая проблема, если я внедряю сервис в сущность. Я также обновляю свой вопрос и включаю Option5, который я считаю лучшим решением на данный момент.
Sher10ck
@ Sher10ck Если вы извлекаете конфигурацию для своей SendEmailTaskбазы данных, то эта конфигурация должна быть отдельным классом конфигурации, который также должен быть введен в ваш SendEmailTask. Если вы генерируете данные из своего SendEmailTask, вы должны создать объект memento для хранения состояния и поместить его в свою базу данных.
cbojar
Мне нужно вытащить конфигурацию из базы данных, так что вы предлагаете вводить оба EMailTaskDefinitionsи EmailServiceв SendEmailTask? Затем RecurringTaskScheduler, мне нужно внедрить что-то вроде того, SendEmailTaskRepositoryчья ответственность заключается в загрузке определения и обслуживания, и внедрить их в SendEmailTask. Но я бы поспорил сейчас о RecurringTaskSchedulerнеобходимости знать репозиторий каждой задачи, вроде бы CleanDiskTaskRepository. И мне нужно менять RecurringTaskSchedulerкаждый раз, когда у меня появляется новое задание (добавить репозиторий в планировщик).
Sher10ck
@ Sher10ck RecurringTaskSchedulerСледует знать только о концепции обобщенного хранилища задач и a RecurringTask. Делая это, это может зависеть от абстракций. Репозитории задач могут быть введены в конструктор RecurringTaskScheduler. Тогда разные репозитории нужно знать только, где они RecurringTaskSchedulerсоздаются (или могут быть спрятаны на фабрике и вызваны оттуда). Потому что это зависит только от абстракций, RecurringTaskSchedulerне нужно менять с каждой новой задачей. В этом суть инверсии зависимости.
cbojar
3

Вы смотрели на существующие библиотеки, например, весенний кварц или весеннюю партию (я не уверен, что больше всего соответствует вашим потребностям)?

На ваш вопрос:

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

Мое предлагаемое решение:

Я бы разделил выполнение и часть данных задачи, чтобы иметь, например, TaskDefinitionи a TaskRunner. TaskDefinition имеет ссылку на TaskRunner или фабрику, которая его создает (например, если требуется какая-то настройка, например, smtp-host). Фабрика специфична - она ​​может обрабатывать только EMailTaskDefinitions и возвращает только экземпляры EMailTaskRunners. Таким образом, это больше ОО и безопасное изменение - если вы вводите новый тип задачи, вы должны вводить новую конкретную фабрику (или повторно использовать), если вы этого не делаете, вы не можете скомпилировать.

Таким образом, вы получите в итоге зависимость: уровень сущности -> уровень обслуживания и обратно, потому что Бегуну нужна информация, хранящаяся в сущности, и, вероятно, он хочет обновить ее состояние в БД.

Вы могли бы разорвать этот порочный круг, используя общий завод, который берет на TaskDefinition и возвращает специфический TaskRunner, но это потребует много МФСА. Вы можете использовать рефлексию, чтобы найти бегуна, который так же называется вашим определением, но будьте осторожны, такой подход может стоить некоторой производительности и может привести к ошибкам во время выполнения.

PS Я предполагаю, что Java здесь. Я думаю, что это похоже на .net. Основной проблемой здесь является двойное связывание.

Для шаблона посетителя

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

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

PS Я не тестировал его, но думаю, что ваш вариант 4 тоже не сработает, потому что он снова имеет двойную привязку.

Энди
источник
Вы суммируете мои намерения действительно хорошо, спасибо! Я хотел бы разорвать круг. Поскольку разрешение TaskDefiniton содержит ссылку на TaskRunner или фабрику имеет ту же проблему, что и Option1. Я отношусь к фабрике или TaskRunner как к сервису. Если для TaskDefinition нужна ссылка на них, вы должны либо внедрить службу в TaskDefinition , либо использовать какой-то статический метод, которого я стараюсь избегать.
Sher10ck
1

Я полностью не согласен с этой статьей. Службы (конкретно их «API») являются важной стороной бизнес-домена и, как таковые, будут существовать в рамках доменной модели. И нет проблем с сущностями в бизнес-домене, ссылающимися на что-то еще в том же бизнес-домене.

Когда X отправляет почту Y.

Это бизнес-правило. И для этого нужен сервис, который отправляет почту. И субъект, который обрабатывает, When Xдолжен знать об этом сервисе.

Но есть некоторые проблемы с реализацией. Для пользователя объекта должно быть понятно, что объект использует службу. Поэтому добавление службы в конструктор не очень хорошая вещь. Это также проблема, когда вы десериализуете сущность из базы данных, потому что вам нужно установить как данные сущности, так и экземпляры сервисов. Лучшее решение, которое я могу придумать, - это использование инъекции свойств после создания сущности. Возможно, заставляя каждый вновь созданный экземпляр любой сущности проходить через метод «инициализации», который внедряет все сущности, в которых нуждается эта сущность.

Euphoric
источник
На какую статью вы ссылаетесь, с которой вы не согласны? Однако интересна точка зрения на модель предметной области. Вероятно, вы можете видеть это так, хотя люди обычно избегают смешивать сервисы в сущности, потому что это очень скоро создаст тесную связь.
Энди
@Andy Тот, на кого Sher10ck ссылается в своем вопросе. И я не вижу, как это могло бы создать тесную связь. Любой плохо написанный код может привести к жесткой связи.
Эйфорический
1

Это отличный вопрос и интересная проблема. Я предлагаю вам использовать комбинацию шаблонов Chain of Responsibility и Double Dispatch (примеры шаблонов здесь ).

Сначала давайте определим иерархию задач. Обратите внимание, что теперь есть несколько runметодов для реализации двойной отправки.

public abstract class RecurringTask {

    public abstract boolean isOccuring(Date date);

    public boolean run(EmailService emailService) {
        return false;
    }

    public boolean run(ExecuteService executeService) {
        return false;
    }
}

public class SendEmailTask extends RecurringTask {

    private String email;

    public SendEmailTask(String email) {
        this.email = email;
    }

    @Override
    public boolean isOccuring(Date date) {
        return true;
    }

    @Override
    public boolean run(EmailService emailService) {
        emailService.runTask(this);
        return true;
    }

    public String getEmail() {
        return email;
    }
}

public class ExecuteTask extends RecurringTask {

    private String program;

    public ExecuteTask(String program) {
        this.program = program;
    }

    @Override
    public boolean isOccuring(Date date) {
        return true;
    }

    public String getName() {
        return program;
    }

    @Override
    public boolean run(ExecuteService executeService) {
        executeService.runTask(this);
        return true;
    }
}

Далее давайте определим Serviceиерархию. Мы будем использовать Services для формирования Цепочки Ответственности.

public abstract class Service {

    private Service next;

    public Service(Service next) {
        this.next = next;
    }

    public void handleRecurringTask(RecurringTask req) {
        if (next != null) {
            next.handleRecurringTask(req);
        }
    }
}

public class ExecuteService extends Service {

    public ExecuteService(Service next) {
        super(next);
    }

    void runTask(ExecuteTask task) {
        System.out.println(String.format("%s running %s with content '%s'", this.getClass().getSimpleName(),
                task.getClass().getSimpleName(), task.getName()));
    }

    public void handleRecurringTask(RecurringTask req) {
        if (!req.run(this)) {
            super.handleRecurringTask(req);
        }
    }
}

public class EmailService extends Service {

    public EmailService(Service next) {
        super(next);
    }

    public void runTask(SendEmailTask task) {
        System.out.println(String.format("%s running %s with content '%s'", this.getClass().getSimpleName(),
                task.getClass().getSimpleName(), task.getEmail()));
    }

    public void handleRecurringTask(RecurringTask req) {
        if (!req.run(this)) {
            super.handleRecurringTask(req);
        }
    }
}

Последний элемент - это RecurringTaskSchedulerпроцесс загрузки и запуска.

public class RecurringTaskScheduler{

    private List<RecurringTask> tasks = new ArrayList<>();

    private Service chain;

    public RecurringTaskScheduler() {
        chain = new EmailService(new ExecuteService(null));
    }

    public void loadTasks() {
        tasks.add(new SendEmailTask("here comes the first email"));
        tasks.add(new SendEmailTask("here is the second email"));
        tasks.add(new ExecuteTask("/root/python"));
        tasks.add(new ExecuteTask("/bin/cat"));
        tasks.add(new SendEmailTask("here is the third email"));
        tasks.add(new ExecuteTask("/bin/grep"));
    }

    public void runTasks(){
        for (RecurringTask task : tasks) {
            if (task.isOccuring(new Date())) {
                chain.handleRecurringTask(task);
            }
        }
    }
}

Теперь вот пример приложения, демонстрирующего систему.

public class App {

    public static void main(String[] args) {
        RecurringTaskScheduler scheduler = new RecurringTaskScheduler();
        scheduler.loadTasks();
        scheduler.runTasks();
    }
}

Запуск выходных данных приложения:

EmailService с запущенным SendEmailTask ​​с контентом «здесь приходит первое электронное письмо»
EmailService с запущенным SendEmailTask ​​с контентом »здесь второе электронное письмо«
ExecuteService с запущенным ExecuteTask с контентом »/ root / python«
ExecuteService с запущенным ExecuteTask с контентом »/ bin / cat«
EmailService с запущенным SendEmailTask ​​с content 'вот третье электронное письмо'
ExecuteService, на котором выполняется ExecuteTask с контентом '/ bin / grep'

iluwatar
источник
У меня может быть много задач . Каждый раз, когда я добавляю новую задачу , мне нужно изменить RecurringTask, а также мне нужно изменить все его подклассы, потому что мне нужно добавить новую функцию, такую ​​как public abstract boolean run (OtherService otherService) . Я думаю, что Option4, шаблон посетителя, который также реализует двойную рассылку, имеет ту же проблему.
Sher10ck
Хорошая точка зрения. Я отредактировал свой ответ так, чтобы методы run (service) были определены в RecurringTask и возвращали false по умолчанию. Таким образом, когда вам нужно добавить другой класс задач, вам не нужно трогать родственные задачи.
iluwatar