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 .
SendEmailTask
wydaje mi się bardziej usługą niż bytem. Wybrałbym opcję 1 bez wahania.accept
odwiedzają. 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).Odpowiedzi:
Powiedziałbym, że najlepszą opcją jest opcja 1 . Powodem, dla którego nie powinieneś zwalniać, jest to, że nie
SendEmailTask
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.Email
Email
#Send
EmailService
Ponieważ
SendEmailTask
nie 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, żeSendEmailTask
zawsze 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,
if
mają 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 tymif
stwierdzeniem 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
RecurringTaskScheduler
musi 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? ZmianaRecurringTaskScheduler
. 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 doRecurringTaskScheduler
.Opcja 3
Opcja 3 ma poważne problemy. Nawet w artykule, do którego prowadzi link , autor odradza:
Tworzysz lokalizator usług ze swoją
ServiceBundle
klasą. 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 tymServiceBundle
. Jeśli dam ci następujący interfejs API mojego nowego, fajnego zadania: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
ServiceBundle
nie 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 wServiceBundle
, a zmiany wServiceBundle
mogą oznaczać odmienne zmiany w zadaniach gdzie indziej.ServiceBundle
nie 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 zaServiceBundle
. 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
Visitor
obiekt. Ten obiekt działa jak obiekt boski we wszystkich twoich zadaniach. Redukuje twojeRecurringTask
obiekty do anemicznych cieni, które po prostu wzywają gości. Wszystkie zachowania przenoszone są doVisitor
. Chcesz zmienić zachowanie? Chcesz dodać nowe zadanie? ZmianaVisitor
.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ć
IVisitor
interfejs 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,
RecurringTask
który zachowuje nowe zachowanie, wstrzykuje jego zależności i dodaje go do zbioru zadań wRecurringTaskScheduler
. 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ą.
źródło
SendEmailTask
z bazy danych, konfiguracja ta powinna być osobną klasą konfiguracji, którą również należy wstrzyknąć do twojejSendEmailTask
. Jeśli generujesz dane ze swojegoSendEmailTask
, powinieneś utworzyć obiekt memento do przechowywania stanu i umieścić go w swojej bazie danych.EMailTaskDefinitions
iEmailService
doSendEmailTask
? PotemRecurringTaskScheduler
potrzebuję wstrzyknąć coś takiego, na kimSendEmailTaskRepository
spoczywa ładowanie definicji i usługi, i wrzucam jeSendEmailTask
. Ale kłóciłbym się teraz oRecurringTaskScheduler
potrzebę znajomości repozytorium każdego zadaniaCleanDiskTaskRepository
. I muszę zmieniać zaRecurringTaskScheduler
każdym razem, gdy mam nowe zadanie (aby dodać repozytorium do programu planującego).RecurringTaskScheduler
Powinieneś być świadomy koncepcji uogólnionego repozytorium zadań iRecurringTask
. W ten sposób może zależeć od abstrakcji. Repozytoria zadań można wstrzyknąć do konstruktoraRecurringTaskScheduler
. Wówczas różne repozytoria muszą być znane tylko tam, gdzieRecurringTaskScheduler
są tworzone (lub mogą być ukryte w fabryce i wywoływane stamtąd). Ponieważ zależy to tylko od abstrakcji,RecurringTaskScheduler
nie musi się zmieniać przy każdym nowym zadaniu. To jest istota inwersji zależności.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
TaskDefinition
aTaskRunner
. 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ć tylkoEMailTaskDefinition
s i zwraca tylko wystąpieniaEMailTaskRunner
s. 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.
źródło
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.
To reguła biznesowa. Aby to zrobić, potrzebna jest usługa wysyłająca pocztę. Podmiot obsługujący
When X
powinien 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.
źródło
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
run
metod implementacji Double Dispatch.Następnie pozwala zdefiniować
Service
hierarchię. UżyjemyService
s, aby stworzyć Łańcuch Odpowiedzialności.Ostatni kawałek jest tym,
RecurringTaskScheduler
który koordynuje proces ładowania i uruchamiania.Oto przykładowa aplikacja demonstrująca system.
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”
źródło