Czy wzorzec użytkownika jest prawidłowy w tym scenariuszu?

9

Celem mojego zadania jest zaprojektowanie małego systemu, który może uruchamiać zaplanowane zadania cykliczne. Powtarzającym się zadaniem jest coś takiego: „wysyłaj e-maile do administratora co godzinę od 8:00 do 17:00, od poniedziałku do piątku”.

Mam klasę podstawową o nazwie 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(){

    }
}

I mam kilka klas, które są dziedziczone z RecurringTask . Jeden z nich nazywa się SendEmailTask .

public class SendEmailTask : RecurringTask{
    private Email email;

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

    public override void Run(){
        // need to send out email
    }
}

I mam EmailService, który może mi pomóc wysłać e-mail.

Ostatnia klasa to RecurringTaskScheduler , odpowiada za ładowanie zadań z pamięci podręcznej lub bazy danych i uruchamianie zadania.

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();
            }
        }
    }
}

Oto mój problem: gdzie powinienem umieścić EmailService ?

Opcja 1 : wstrzyknięcie usługi e- mail do 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);
    }
}

Trwają już dyskusje na temat tego, czy powinniśmy wstrzyknąć usługę podmiotowi, a większość ludzi zgadza się, że nie jest to dobra praktyka. Zobacz ten artykuł .

Opcja 2: Jeśli ... Else w 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
                }
            }
        }
    }
}

Powiedziano mi, że jeśli ... Inne i obsada jak wyżej nie jest OO i przyniesie więcej problemów.

Opcja 3: Zmień podpis Run i utwórz ServiceBundle .

public class ServiceBundle{
    public EmailService EmailService{get;set}
    public CleanDiskService CleanDiskService{get;set;}
    // and other services for other recurring tasks

}

Wstrzyknij tę klasę do 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 metoda SendEmailTask byłoby

public void Run(ServiceBundle serviceBundle){
    serviceBundle.EmailService.send(this.email);
}

Nie widzę większych problemów z tym podejściem.

Opcja 4 : Wzorzec gościa.
Podstawową ideą jest stworzenie gościa, który będzie zawierał usługi podobnie jak 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){
        //
    }
}

Musimy także zmienić podpis metody Run . Run metoda SendEmailTask jest

public void Run(RecurringTaskVisitor visitor){
    visitor.visit(this);
}

Jest to typowa implementacja Wzorca Odwiedzającego, a odwiedzający zostanie wstrzyknięty do RecurringTaskScheduler .

Podsumowując: Które z tych czterech podejść jest najlepsze dla mojego scenariusza? Czy istnieje jakaś duża różnica między Opcją 3 a Opcją 4 w przypadku tego problemu?

A może masz lepszy pomysł na ten problem? Dzięki!

Aktualizacja 22.05.2015 : Myślę, że odpowiedź Andy'ego bardzo dobrze podsumowuje moją intencję; jeśli nadal masz wątpliwości co do samego problemu, sugeruję najpierw przeczytać jego post.

Właśnie dowiedziałem się, że mój problem jest bardzo podobny do problemu Wysyłania wiadomości , który prowadzi do Opcji 5.

Opcja 5 : Konwertuj mój problem na wysyłanie wiadomości .
Pomiędzy moim problemem a problemem wysłania wiadomości istnieje mapowanie jeden do jednego :

Wiadomość Dyspozytor : Otrzymuj iMessage i wysyłka sub klas iMessage do odpowiadających im koparki. → RecurringTaskScheduler

WIADOMOŚĆ : Interfejs lub klasa abstrakcyjna. → Zadanie cykliczne

MessageA : Rozszerza z iMessage , mając jakieś dodatkowe informacje. → SendEmailTask

MessageB : Inna podklasa iMessage . → CleanDiskTask

MessageAHandler : Po otrzymaniu MessageA , obsłuż go → SendEmailTaskHandler, który zawiera EmailService i wyśle ​​wiadomość e-mail, gdy otrzyma SendEmailTask

MessageBHandler : Taki sam jak MessageAHandler , ale zamiast tego obsłuż MessageB . → CleanDiskTaskHandler

Najtrudniej jest jak wysyłką inny rodzaj iMessage różne ładowarki. Oto przydatny link .

Naprawdę podoba mi się to podejście, nie zanieczyszcza ono mojej istoty służbą i nie ma żadnej klasy Boga .

Sher10ck
źródło
Nie oznaczyłeś języka ani platformy, ale polecam zajrzeć do crona . Twoja platforma może mieć bibliotekę, która działa podobnie (np. Jcron, który wydaje się jakby nieczynny). Planowanie zadań i zadań jest w dużej mierze rozwiązanym problemem: czy sprawdziłeś inne opcje przed wprowadzeniem własnych? Czy były powody, aby ich nie używać?
@Snowman Później możemy przejść do dojrzałej biblioteki. Wszystko zależy od mojego menedżera. Powód, dla którego zamieszczam to pytanie, jest taki, że chcę znaleźć sposób na rozwiązanie tego rodzaju problemu. Widziałem ten problem więcej niż raz i nie mogłem znaleźć eleganckiego rozwiązania. Zastanawiam się więc, czy zrobiłem coś złego.
Sher10ck
Szczerze mówiąc, zawsze staram się zalecać ponowne użycie kodu, jeśli to możliwe.
1
SendEmailTaskwydaje mi się bardziej usługą niż bytem. Wybrałbym opcję 1 bez wahania.
Bart van Ingen Schenau
3
To, czego brakuje (dla mnie) dla Odwiedzającego, to struktura klasowa, którą acceptodwiedzają. Motywacją dla Odwiedzającego jest to, że masz wiele typów klas w niektórych agregatach, które wymagają odwiedzenia, i nie jest wygodne modyfikowanie ich kodu dla każdej nowej funkcjonalności (operacji). Nadal nie widzę, czym są te obiekty agregujące, i uważam, że Odwiedzający nie jest odpowiedni. W takim przypadku powinieneś edytować swoje pytanie (które odnosi się do odwiedzającego).
Fuhrmanator

Odpowiedzi:

4

Powiedziałbym, że najlepszą opcją jest opcja 1 . Powodem, dla którego nie powinieneś zwalniać, jest to, że nieSendEmailTask jest to byt. Podmiot to obiekt związany z przechowywaniem danych i stanu. Twoja klasa ma bardzo mało tego. W rzeczywistości nie jest bytem, ​​ale posiada byt: przedmiot, który przechowujesz. Oznacza to, że nie należy korzystać z usługi ani mieć metody. Zamiast tego powinieneś mieć usługi, które przyjmują podmioty, takie jak twój . Więc już podążasz za ideą utrzymywania usług z dala od podmiotów.EmailEmail#SendEmailService

Ponieważ SendEmailTasknie jest to byt, w związku z tym doskonale jest wstrzykiwać do niego wiadomość e-mail i usługę, i należy to zrobić za pośrednictwem konstruktora. Wykonując wstrzyknięcie konstruktora, możemy być pewni, że SendEmailTaskzawsze jest gotowy do wykonania swojej pracy.

Teraz spójrzmy, dlaczego nie zrobić innych opcji (szczególnie w odniesieniu do SOLID ).

Opcja 2

Słusznie powiedziano ci, że rozgałęzienie tego typu przyniesie więcej bólów głowy na drodze. Zobaczmy dlaczego. Po pierwsze, ifmają tendencję do skupiania się i wzrostu. Dzisiaj zadaniem jest wysyłanie e-maili, jutro każdy inny typ klasy potrzebuje innej usługi lub innego zachowania. Zarządzanie tym ifstwierdzeniem staje się koszmarem. Ponieważ rozgałęziamy się na typ (iw tym przypadku typ jawny ), niszczymy system typów wbudowany w nasz język.

Wariant 2 nie jest pojedynczą odpowiedzialnością (SRP), ponieważ dawniej wielokrotnego użytku RecurringTaskSchedulermusi teraz wiedzieć o wszystkich tych różnych typach zadań oraz o różnych rodzajach usług i zachowań, których mogą potrzebować. Ta klasa jest znacznie trudniejsza do ponownego wykorzystania. Nie jest również otwarty / zamknięty (OCP). Ponieważ musi wiedzieć o tym zadaniu lub o tym (lub o tym rodzaju usługi lub o tym), różne zmiany w zadaniach lub usługach mogą wymusić zmiany tutaj. Dodać nowe zadanie? Dodać nową usługę? Zmienić sposób obsługi poczty e-mail? Zmiana RecurringTaskScheduler. Ponieważ rodzaj zadania ma znaczenie, nie jest zgodny z substytucją Liskowa (LSP). Nie można po prostu wykonać zadania i wykonać. Musi zapytać o typ i na podstawie typu zrób to lub zrób to. Zamiast ujmować różnice w zadaniach, wciągamy to wszystko do RecurringTaskScheduler.

Opcja 3

Opcja 3 ma poważne problemy. Nawet w artykule, do którego prowadzi link , autor odradza:

  • Nadal możesz używać statycznego lokalizatora usług…
  • Kiedy mogę, unikam lokalizatora usług, zwłaszcza gdy lokalizator usług musi być statyczny…

Tworzysz lokalizator usług ze swoją ServiceBundleklasą. W tym przypadku nie wydaje się być statyczny, ale nadal ma wiele problemów związanych z lokalizatorem usług. Twoje zależności są teraz ukryte pod tym ServiceBundle. Jeśli dam ci następujący interfejs API mojego nowego, fajnego zadania:

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
    }
}

Z jakich usług korzystam? Jakie usługi należy wyśmiewać w teście? Co powstrzymuje mnie przed korzystaniem z każdej usługi w systemie tylko dlatego, że?

Jeśli chcę używać systemu zadań do uruchamiania niektórych zadań, jestem teraz zależny od każdej usługi w twoim systemie, nawet jeśli używam tylko kilku lub nawet wcale.

To ServiceBundlenie jest tak naprawdę SRP, ponieważ musi wiedzieć o każdej usłudze w twoim systemie. To także nie jest OCP. Dodanie nowych usług oznacza zmiany w ServiceBundle, a zmiany w ServiceBundlemogą oznaczać odmienne zmiany w zadaniach gdzie indziej. ServiceBundlenie segreguje swojego interfejsu (ISP). Ma rozbudowany interfejs wszystkich tych usług, a ponieważ jest to tylko dostawca tych usług, moglibyśmy rozważyć, aby jego interfejs obejmował również interfejsy wszystkich usług, które oferuje. Zadania nie są już zgodne z Inwersją zależności (DIP), ponieważ ich zależności są zaciemnione za ServiceBundle. To również nie jest zgodne z zasadą najmniejszej wiedzy (zwaną również prawem Demetera), ponieważ rzeczy wiedzą o wiele więcej rzeczy niż muszą.

Opcja 4

Wcześniej istniało wiele małych obiektów, które były w stanie działać niezależnie. Opcja 4 bierze wszystkie te obiekty i rozbija je razem w jeden Visitorobiekt. Ten obiekt działa jak obiekt boski we wszystkich twoich zadaniach. Redukuje twoje RecurringTaskobiekty do anemicznych cieni, które po prostu wzywają gości. Wszystkie zachowania przenoszone są do Visitor. Chcesz zmienić zachowanie? Chcesz dodać nowe zadanie? Zmiana Visitor.

Bardziej wymagającą częścią jest to, że wszystkie różne zachowania są w jednej klasie, zmieniając niektóre polimorficzne ciągnięcia wzdłuż wszystkich innych zachowań. Na przykład chcemy mieć dwa różne sposoby wysyłania wiadomości e-mail (może powinny używać różnych serwerów?). Jak byśmy to zrobili? Możemy stworzyć IVisitorinterfejs i wdrożyć ten, potencjalnie powielający kod, tak jak w przypadku #Visit(ClearDiskTask)naszego oryginalnego użytkownika. Następnie, jeśli wymyślimy nowy sposób wyczyszczenia dysku, musimy go zaimplementować i powtórzyć. Następnie chcemy obu rodzajów zmian. Zaimplementuj i powiel ponownie. Te dwa różne, odmienne zachowania są ze sobą nierozerwalnie związane.

Może zamiast tego moglibyśmy po prostu podklasę Visitor? Podklasa z nowym zachowaniem poczty e-mail, podklasa z nowym zachowaniem dysku. Dotychczas brak powielania! Podklasę z obydwoma? Teraz jedno lub drugie musi zostać zduplikowane (lub jedno i drugie, jeśli taka jest twoja preferencja).

Porównajmy z opcją 1: Potrzebujemy nowego zachowania e-mail. Możemy stworzyć nowy, RecurringTaskktóry zachowuje nowe zachowanie, wstrzykuje jego zależności i dodaje go do zbioru zadań w RecurringTaskScheduler. Nie musimy nawet rozmawiać o usuwaniu dysków, ponieważ odpowiedzialność ta leży gdzie indziej. W dalszym ciągu mamy do dyspozycji pełen zestaw narzędzi OO. Możemy udekorować to zadanie na przykład logowaniem.

Opcja 1 da ci najmniejszy ból i jest najodpowiedniejszym sposobem radzenia sobie z tą sytuacją.

Cbojar
źródło
Twoja analiza na Otion2,3,4 jest fantastyczna! To naprawdę bardzo mi pomaga. Ale w przypadku opcji 1 argumentowałbym, że * SendEmailTask ​​* jest bytem. Ma identyfikator, ma powtarzający się wzorzec i inne przydatne informacje, które powinny być przechowywane w db. Myślę, że Andy dobrze podsumowuje moją intencję. Może nazwa taka jak * EMailTaskDefinitions * jest bardziej odpowiednia. nie chcę zanieczyszczać mojej jednostki moim kodem serwisowym. Euforia wspomina o pewnym problemie, jeśli wstrzyknę usługę do obiektu. Aktualizuję również moje pytanie i dołączam opcję 5, która moim zdaniem jest najlepszym rozwiązaniem do tej pory.
Sher10ck
@ Sher10ck Jeśli wyciągasz konfigurację SendEmailTaskz bazy danych, konfiguracja ta powinna być osobną klasą konfiguracji, którą również należy wstrzyknąć do twojej SendEmailTask. Jeśli generujesz dane ze swojego SendEmailTask, powinieneś utworzyć obiekt memento do przechowywania stanu i umieścić go w swojej bazie danych.
cbojar
Muszę pobrać konfigurację z bazy danych, więc sugerujesz wstrzyknięcie zarówno do, jak EMailTaskDefinitionsi EmailServicedo SendEmailTask? Potem RecurringTaskSchedulerpotrzebuję wstrzyknąć coś takiego, na kim SendEmailTaskRepositoryspoczywa ładowanie definicji i usługi, i wrzucam je SendEmailTask. Ale kłóciłbym się teraz o RecurringTaskSchedulerpotrzebę znajomości repozytorium każdego zadania CleanDiskTaskRepository. I muszę zmieniać za RecurringTaskSchedulerkażdym razem, gdy mam nowe zadanie (aby dodać repozytorium do programu planującego).
Sher10ck
@ Sher10ck RecurringTaskSchedulerPowinieneś być świadomy koncepcji uogólnionego repozytorium zadań i RecurringTask. W ten sposób może zależeć od abstrakcji. Repozytoria zadań można wstrzyknąć do konstruktora RecurringTaskScheduler. Wówczas różne repozytoria muszą być znane tylko tam, gdzie RecurringTaskSchedulersą tworzone (lub mogą być ukryte w fabryce i wywoływane stamtąd). Ponieważ zależy to tylko od abstrakcji, RecurringTaskSchedulernie musi się zmieniać przy każdym nowym zadaniu. To jest istota inwersji zależności.
cbojar
3

Czy przeglądałeś już istniejące biblioteki, np. Kwarc wiosenny lub wiosenny pakiet (nie jestem pewien, co najbardziej odpowiada Twoim potrzebom)?

Na twoje pytanie:

Zakładam, że problem polega na tym, że chcesz utrwalić niektóre metadane zadania w sposób polimorficzny, więc zadanie e-mail ma przypisane adresy e-mail, zadanie dziennika na poziomie dziennika i tak dalej. Możesz przechowywać listę tych w pamięci lub w bazie danych, ale aby rozdzielić obawy, nie chcesz, aby jednostka została zanieczyszczona kodem serwisowym.

Moje proponowane rozwiązanie:

Chciałbym oddzielić docierania a dane-część zadania, aby mieć na przykład TaskDefinitiona TaskRunner. TaskDefinition ma odniesienie do TaskRunner lub fabryki, która je tworzy (np. Jeśli wymagana jest konfiguracja, np. Host smtp). Fabryka jest specyficzna - może obsługiwać tylko EMailTaskDefinitions i zwraca tylko wystąpienia EMailTaskRunners. W ten sposób jest więcej OO i bezpieczna zmiana - jeśli wprowadzisz nowy typ zadania, musisz wprowadzić nową konkretną fabrykę (lub użyć ponownie), jeśli nie możesz kompilować.

W ten sposób uzyskasz zależność: warstwa encji -> warstwa usługi iz powrotem, ponieważ Runner potrzebuje informacji przechowywanych w encji i prawdopodobnie chce zaktualizować swój stan w DB.

Można przerwać krąg przy użyciu rodzajowe fabryki, która zajmuje się TaskDefinition i zwraca specyficzne TaskRunner, ale to będzie wymagało dużo IFS. Ty mógł użyć refleksji znaleźć biegacza, który podobnie jak o nazwie definicji, ale być ostrożnym podejście to może kosztować trochę wydajność i może prowadzić do błędów runtime.

PS Zakładam, że tutaj Java. Myślę, że podobnie jest w .net. Głównym problemem tutaj jest podwójne wiązanie.

Do wzoru gościa

Myślę, że miał on raczej służyć do wymiany algorytmu dla różnego rodzaju obiektów danych w czasie wykonywania, niż do czystego podwójnego wiązania. Na przykład, jeśli masz różne rodzaje ubezpieczeń i różne rodzaje ich obliczania, np. Ponieważ wymagają tego różne kraje. Następnie wybierz konkretną metodę obliczania i zastosuj ją do kilku ubezpieczeń.

W twoim przypadku wybrałbyś konkretną strategię zadań (np. E-mail) i zastosowałeś ją do wszystkich swoich zadań, co jest złe, ponieważ nie wszystkie z nich są zadaniami e-mail.

PS Nie testowałem tego, ale myślę, że twoja Opcja 4 też nie będzie działać, ponieważ znów jest podwójnie wiążąca.

Andy
źródło
Bardzo dobrze podsumowujesz moją intencję, dzięki! Chciałbym przerwać krąg. Ponieważ zezwolenie TaskDefiniton zawiera odniesienie do TaskRunner lub fabryki ma ten sam problem co Option1. Traktuję fabrykę lub TaskRunner jako usługę. Jeśli funkcja TaskDefinition zawiera odwołanie do nich, albo wstrzykujesz usługę do TaskDefinition , albo używasz metody statycznej, której próbuję uniknąć.
Sher10ck
1

Całkowicie nie zgadzam się z tym artykułem. Usługi (konkretnie ich „API”) są ważną stroną Domeny Biznesowej i jako takie będą istnieć w Modelu Domeny. I nie ma problemu z podmiotami w domenie biznesowej odwołującymi się do czegoś innego w tej samej domenie biznesowej.

Kiedy X wyśle ​​pocztę do Y.

To reguła biznesowa. Aby to zrobić, potrzebna jest usługa wysyłająca pocztę. Podmiot obsługujący When Xpowinien wiedzieć o tej usłudze.

Ale są pewne problemy z implementacją. Użytkownik jednostki powinien być przejrzysty, że jednostka korzysta z usługi. Zatem dodanie usługi w konstruktorze nie jest dobrą rzeczą. Jest to również problem, gdy deserializujesz encję z bazy danych, ponieważ musisz ustawić zarówno dane encji, jak i instancje usług. Najlepszym rozwiązaniem, jakie mogę wymyślić, jest zastrzyk nieruchomości po utworzeniu encji. Być może zmuszanie każdej nowo utworzonej instancji dowolnej encji do przejścia przez metodę „inicjalizacji”, która wstrzykuje wszystkie encje, których potrzebuje encja.

Euforyk
źródło
Jaki artykuł masz na myśli, z którym się nie zgadzasz? Jednak ciekawy punkt widzenia na model domeny. Prawdopodobnie widać to w ten sposób, ludzie zwykle unikają mieszania usług w podmioty, ponieważ wkrótce stworzy to ścisłe powiązanie.
Andy
@Andy Ten, do którego Sher10ck powołał się w swoim pytaniu. I nie rozumiem, jak stworzyłoby to ścisłe połączenie. Każdy źle napisany kod może wprowadzić ścisłe sprzężenie.
Euforyczny
1

To świetne pytanie i interesujący problem. Proponuję zastosować kombinację wzorców łańcucha odpowiedzialności i podwójnej wysyłki ( tutaj przykłady wzorów ).

Najpierw zdefiniujmy hierarchię zadań. Zauważ, że istnieje wiele runmetod implementacji Double Dispatch.

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;
    }
}

Następnie pozwala zdefiniować Servicehierarchię. Użyjemy Services, aby stworzyć Łańcuch Odpowiedzialności.

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);
        }
    }
}

Ostatni kawałek jest tym, RecurringTaskSchedulerktóry koordynuje proces ładowania i uruchamiania.

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);
            }
        }
    }
}

Oto przykładowa aplikacja demonstrująca system.

public class App {

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

Uruchamianie danych wyjściowych aplikacji:

EmailService działa SendEmailTask z treścią „tu pojawia się pierwszy email”
EmailService działa SendEmailTask z treścią „tu jest drugi email”
ExecuteService działa ExecuteTask z treścią „/ root / python”
ExecuteService działa ExecuteTask z zawartością „/ bin / cat”
EmailService działa SendEmailTask z content ”tutaj jest trzeci e-mail„
ExecuteService z uruchomionym ExecuteTask z zawartością ”/ bin / grep”

iluwatar
źródło
Mogę mieć dużo zadań . Za każdym razem, gdy dodam nowe zadanie , muszę zmienić RecurringTask, a także muszę zmienić wszystkie jego podklasy, ponieważ muszę dodać nową funkcję, taką jak publiczne abstrakcyjne uruchomienie wartości logicznej (OtherService otherService) . Myślę, że Option4, wzorzec odwiedzających, który również implementuje podwójną wysyłkę, ma ten sam problem.
Sher10ck
Słuszna uwaga. Zredagowałem swoją odpowiedź, aby metody uruchamiania (usługi) były zdefiniowane w RecurringTask i domyślnie zwracały wartość false. W ten sposób, gdy trzeba dodać kolejną klasę zadań, nie trzeba dotykać zadań rodzeństwa.
iluwatar