Das Ziel meiner Aufgabe ist es, ein kleines System zu entwerfen, das geplante wiederkehrende Aufgaben ausführen kann. Eine wiederkehrende Aufgabe ist etwa "Senden Sie von Montag bis Freitag stündlich von 8.00 bis 17.00 Uhr eine E-Mail an den Administrator".
Ich habe eine Basisklasse namens 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(){
}
}
Und ich habe mehrere Klassen, die von RecurringTask geerbt werden . Eine davon heißt SendEmailTask .
public class SendEmailTask : RecurringTask{
private Email email;
public SendEmailTask(Email email){
this.email = email;
}
public override void Run(){
// need to send out email
}
}
Und ich habe einen EmailService, der mir beim Versenden einer E-Mail helfen kann.
Die letzte Klasse ist RecurringTaskScheduler . Sie ist dafür verantwortlich, Aufgaben aus dem Cache oder der Datenbank zu laden und die Aufgabe auszuführen.
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();
}
}
}
}
Hier ist mein Problem: Wo soll ich EmailService platzieren ?
Option 1 : Injizieren Sie EmailService in 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);
}
}
Es gibt bereits einige Diskussionen darüber, ob wir einem Unternehmen einen Service hinzufügen sollten, und die meisten Menschen sind sich einig, dass dies keine gute Praxis ist. Siehe diesen Artikel .
Option 2: Wenn ... sonst in 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
}
}
}
}
}
Mir wurde gesagt, wenn ... Sonst und Besetzung wie oben ist nicht OO, und wird mehr Probleme bringen.
Option 3: Ändern Sie die Signatur von Ausführen und erstellen Sie ServiceBundle .
public class ServiceBundle{
public EmailService EmailService{get;set}
public CleanDiskService CleanDiskService{get;set;}
// and other services for other recurring tasks
}
Fügen Sie diese Klasse in RecurringTaskScheduler ein
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);
}
}
}
}
Die Run- Methode von SendEmailTask wäre
public void Run(ServiceBundle serviceBundle){
serviceBundle.EmailService.send(this.email);
}
Ich sehe keine großen Probleme mit diesem Ansatz.
Option 4 : Besuchermuster.
Die Grundidee besteht darin, einen Besucher zu erstellen, der Dienste wie ServiceBundle kapselt .
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){
//
}
}
Außerdem müssen wir die Signatur der Run- Methode ändern . Die Run- Methode von SendEmailTask lautet
public void Run(RecurringTaskVisitor visitor){
visitor.visit(this);
}
Dies ist eine typische Implementierung des Besuchermusters, und der Besucher wird in RecurringTaskScheduler eingefügt .
Zusammenfassend: Welcher dieser vier Ansätze ist der beste für mein Szenario? Und gibt es für dieses Problem einen großen Unterschied zwischen Option3 und Option4?
Oder haben Sie eine bessere Vorstellung von diesem Problem? Vielen Dank!
Update 22.05.2015 : Ich denke, Andys Antwort fasst meine Absicht wirklich gut zusammen. Wenn Sie immer noch verwirrt sind über das Problem selbst, empfehle ich, zuerst seinen Beitrag zu lesen.
Ich habe gerade herausgefunden, dass mein Problem dem Problem des Nachrichtenversands sehr ähnlich ist , was zu Option 5 führt.
Option 5 : Konvertieren Sie mein Problem in Message Dispatch .
Es gibt eine Eins-zu-Eins-Zuordnung zwischen meinem Problem und dem Problem des Nachrichtenversands :
Message Dispatcher : Empfangen Sie IMessage und senden Sie Unterklassen von IMessage an die entsprechenden Handler. → RecurringTaskScheduler
IMessage : Eine Schnittstelle oder eine abstrakte Klasse. → RecurringTask
MessageA : Erweitert sich aus IMessage und enthält einige zusätzliche Informationen. → SendEmailTask
NachrichtB : Eine weitere Unterklasse von IMessage . → CleanDiskTask
MessageAHandler : Wenn Sie MessageA empfangen , behandeln Sie es → SendEmailTaskHandler, das EmailService enthält, und senden Sie eine E-Mail, wenn SendEmailTask empfangen wird
MessageBHandler : Wie MessageAHandler , jedoch stattdessen MessageB verarbeiten . → CleanDiskTaskHandler
Der schwierigste Teil ist, wie verschiedene Arten von IMessage an verschiedene Handler gesendet werden. Hier ist ein nützlicher Link .
Ich mag diesen Ansatz wirklich, er verschmutzt mein Wesen nicht mit Dienst und es gibt keine Gottklasse .
SendEmailTask
scheint mir eher eine Dienstleistung als eine Einheit zu sein. Ich würde ohne zu zögern für Option 1 gehen.accept
Besucher sind. Die Motivation für Besucher ist, dass Sie viele Klassentypen in einem Aggregat haben, die besucht werden müssen, und es nicht bequem ist, ihren Code für jede neue Funktionalität (Operation) zu ändern. Ich sehe immer noch nicht, was diese aggregierten Objekte sind, und denke, dass Besucher nicht angemessen ist. Wenn dies der Fall ist, sollten Sie Ihre Frage bearbeiten (die sich auf den Besucher bezieht).Antworten:
Ich würde sagen, Option 1 ist der beste Weg. Der Grund, warum Sie es nicht entlassen sollten, ist, dass
SendEmailTask
es sich nicht um eine Entität handelt. Eine Entität ist ein Objekt, das Daten und Status enthält. Ihre Klasse hat sehr wenig davon. Tatsächlich ist es keine Entität, aber es enthält eine Entität: dasEmail
Objekt, das Sie speichern. Das heißt, dasEmail
sollte keinen Dienst annehmen oder eine#Send
Methode haben. Stattdessen sollten Sie über Dienste verfügen, die Entitäten wie Ihre übernehmenEmailService
. Sie verfolgen also bereits die Idee, Dienste von Entitäten fernzuhalten.Da
SendEmailTask
es sich nicht um eine Entität handelt, ist es daher vollkommen in Ordnung, die E-Mail und den Dienst in sie einzufügen, und dies sollte über den Konstruktor erfolgen. Durch die Konstruktorinjektion können wir sicher sein, dassSendEmailTask
das Unternehmen immer bereit ist, seine Aufgabe auszuführen.Schauen wir uns nun an, warum Sie die anderen Optionen nicht ausführen sollten (insbesondere in Bezug auf SOLID ).
Option 2
Ihnen wurde zu Recht gesagt, dass eine solche Verzweigung mehr Kopfschmerzen verursachen wird. Schauen wir uns an, warum. Erstens
if
neigen s dazu, sich zu sammeln und zu wachsen. Heute ist es eine Aufgabe, E-Mails zu senden. Morgen benötigt jede andere Art von Klasse einen anderen Dienst oder ein anderes Verhalten. Das Verwalten dieserif
Aussage wird zum Albtraum. Da wir nach Typ (und in diesem Fall nach explizitem Typ ) verzweigen , untergraben wir das in unsere Sprache integrierte Typsystem.Option 2 ist nicht Single Responsibility (SRP), da der früher wiederverwendbare Benutzer
RecurringTaskScheduler
nun über all diese verschiedenen Arten von Aufgaben und über alle Arten von Diensten und Verhaltensweisen Bescheid wissen muss, die er möglicherweise benötigt. Diese Klasse ist viel schwieriger wiederzuverwenden. Es ist auch nicht offen / geschlossen (OCP). Da es über diese oder jene Art von Aufgabe (oder diese oder jene Art von Dienst) Bescheid wissen muss, können unterschiedliche Änderungen an Aufgaben oder Diensten hier Änderungen erzwingen. Neue Aufgabe hinzufügen? Neuen Service hinzufügen? Ändern Sie die Art und Weise, wie E-Mails behandelt werden? ÄndernRecurringTaskScheduler
. Da die Art der Aufgabe von Bedeutung ist, wird die Liskov-Substitution (LSP) nicht eingehalten. Es kann nicht einfach eine Aufgabe bekommen und erledigt werden. Es muss nach dem Typ fragen und basierend auf dem Typ dies oder das tun. Anstatt die Unterschiede in die Aufgaben einzubeziehen, ziehen wir all das in dieRecurringTaskScheduler
.Option 3
Option 3 hat einige große Probleme. Selbst in dem Artikel, auf den Sie verlinken , rät der Autor davon ab:
Sie erstellen mit Ihrer Klasse einen Service Locator
ServiceBundle
. In diesem Fall scheint es nicht statisch zu sein, aber es weist immer noch viele der Probleme auf, die einem Service Locator inhärent sind. Ihre Abhängigkeiten sind jetzt darunter verstecktServiceBundle
. Wenn ich dir die folgende API meiner coolen neuen Aufgabe gebe:Welche Dienste nutze ich? Welche Dienste müssen in einem Test verspottet werden? Was hindert mich daran, jeden Dienst im System zu nutzen, nur weil?
Wenn ich Ihr Task-System zum Ausführen einiger Aufgaben verwenden möchte, bin ich jetzt von jedem Dienst in Ihrem System abhängig, auch wenn ich nur wenige oder gar keine verwende.
Das
ServiceBundle
ist nicht wirklich SRP, weil es über jeden Dienst in Ihrem System Bescheid wissen muss. Es ist auch nicht OCP. Das Hinzufügen neuer Dienste bedeutet Änderungen an derServiceBundle
und Änderungen an derServiceBundle
können unterschiedliche Änderungen an Aufgaben an anderer Stelle bedeuten.ServiceBundle
trennt seine Schnittstelle (ISP) nicht. Es verfügt über eine weitläufige Schnittstelle all dieser Dienste, und da es nur ein Anbieter für diese Dienste ist, können wir davon ausgehen, dass die Schnittstelle auch die Schnittstellen aller von ihm bereitgestellten Dienste umfasst. Aufgaben unterliegen nicht mehr der Abhängigkeitsinversion (DIP), da ihre Abhängigkeiten hinter dem verschleiert sindServiceBundle
. Dies entspricht auch nicht dem Prinzip des geringsten Wissens (auch bekannt als das Gesetz von Demeter), da die Dinge über viel mehr Dinge wissen, als sie müssen.Option 4
Zuvor hatten Sie viele kleine Objekte, die unabhängig voneinander arbeiten konnten. Option 4 nimmt alle diese Objekte und zerlegt sie zu einem einzigen
Visitor
Objekt. Dieses Objekt fungiert bei all Ihren Aufgaben als Gottobjekt . Es reduziert IhreRecurringTask
Objekte auf anämische Schatten, die einfach einen Besucher ansprechen. Das gesamte Verhalten bewegt sich zumVisitor
. Müssen Sie das Verhalten ändern? Müssen Sie eine neue Aufgabe hinzufügen? ÄndernVisitor
.Der schwierigere Teil ist, dass, da alle verschiedenen Verhaltensweisen in einer einzigen Klasse sind, einige polymorphe Änderungen entlang des gesamten anderen Verhaltens gezogen werden. Zum Beispiel möchten wir zwei verschiedene Möglichkeiten zum Senden von E-Mails haben (sollten sie möglicherweise unterschiedliche Server verwenden?). Wie würden wir das machen? Wir könnten eine
IVisitor
Schnittstelle erstellen und diesen Code möglicherweise duplizieren, wie#Visit(ClearDiskTask)
von unserem ursprünglichen Besucher. Wenn wir dann eine neue Methode zum Löschen einer Festplatte finden, müssen wir sie erneut implementieren und duplizieren. Dann wollen wir beide Arten von Änderungen. Implementieren und erneut duplizieren. Diese beiden unterschiedlichen Verhaltensweisen sind untrennbar miteinander verbunden.Vielleicht könnten wir stattdessen einfach eine Unterklasse bilden
Visitor
? Unterklasse mit neuem E-Mail-Verhalten, Unterklasse mit neuem Festplattenverhalten. Bisher keine Vervielfältigung! Unterklasse mit beiden? Jetzt muss das eine oder andere dupliziert werden (oder beides, wenn Sie dies bevorzugen).Vergleichen wir mit Option 1: Wir brauchen ein neues E-Mail-Verhalten. Wir können ein neues erstellen
RecurringTask
, das das neue Verhalten ausführt, seine Abhängigkeiten einfügen und es der Auflistung von Aufgaben in der hinzufügenRecurringTaskScheduler
. Wir müssen nicht einmal über das Löschen von Datenträgern sprechen, da diese Verantwortung ganz woanders liegt. Wir haben auch noch die gesamte Palette an OO-Tools zur Verfügung. Wir könnten diese Aufgabe zum Beispiel mit der Protokollierung dekorieren.Option 1 schmerzt Sie am wenigsten und ist der richtige Weg, um mit dieser Situation umzugehen.
quelle
SendEmailTask
aus einer Datenbank ziehen, sollte diese Konfiguration eine separate Konfigurationsklasse sein, die auch in Ihre Datenbank eingefügt werden sollteSendEmailTask
. Wenn Sie Daten aus Ihrem generierenSendEmailTask
, sollten Sie ein Erinnerungsobjekt erstellen, um den Status zu speichern und in Ihre Datenbank aufzunehmen.EMailTaskDefinitions
undEmailService
in zu injizierenSendEmailTask
? DannRecurringTaskScheduler
muss ich in der so etwas wie,SendEmailTaskRepository
dessen Verantwortung darin besteht, Definition und Service zu laden, injizieren und sie injizierenSendEmailTask
. Aber ich würde jetzt argumentieren, dieRecurringTaskScheduler
Notwendigkeit, Repository für jede Aufgabe zu kennen, wieCleanDiskTaskRepository
. Und ich mussRecurringTaskScheduler
jedes Mal ändern, wenn ich eine neue Aufgabe habe (um dem Scheduler ein Repository hinzuzufügen).RecurringTaskScheduler
sollte nur das Konzept eines verallgemeinerten Task-Repositorys kennen und aRecurringTask
. Auf diese Weise kann es von Abstraktionen abhängen. Die Task-Repositorys können in den Konstruktor von eingefügt werdenRecurringTaskScheduler
. Dann müssen die verschiedenen Repositorys nur bekannt sein, wo sieRecurringTaskScheduler
instanziiert werden (oder sie könnten in einer Fabrik versteckt und von dort aufgerufen werden). Da es nur auf die Abstraktionen ankommt,RecurringTaskScheduler
muss es sich nicht bei jeder neuen Aufgabe ändern. Das ist die Essenz der Abhängigkeitsinversion.Haben Sie sich vorhandene Bibliotheken angesehen, z. B. Spring Quarz oder Spring Batch (ich bin mir nicht sicher, was Ihren Anforderungen am besten entspricht)?
Zu Ihrer Frage:
Ich gehe davon aus, dass das Problem darin besteht, dass Sie einige Metadaten auf polymorphe Weise für die Aufgabe beibehalten möchten, sodass einer E-Mail-Aufgabe E-Mail-Adressen zugewiesen sind, einer Protokollaufgabe eine Protokollebene usw. Sie können eine Liste dieser Daten im Speicher oder in Ihrer Datenbank speichern. Um jedoch Bedenken auszuräumen, möchten Sie nicht, dass die Entität mit Service-Code verschmutzt wird.
Meine vorgeschlagene Lösung:
Ich würde den laufenden und den Datenteil der Aufgabe trennen, um zB
TaskDefinition
und a zu habenTaskRunner
. Die TaskDefinition enthält einen Verweis auf einen TaskRunner oder eine Factory, die einen erstellt (z. B. wenn ein Setup wie der SMTP-Host erforderlich ist). Die Factory ist eine bestimmte - sie kann nurEMailTaskDefinition
s verarbeiten und gibt nur Instanzen vonEMailTaskRunner
s zurück. Auf diese Weise ist es mehr OO und änderungssicher - wenn Sie einen neuen Aufgabentyp einführen, müssen Sie eine neue spezifische Factory einführen (oder eine wiederverwenden), wenn Sie dies nicht tun, können Sie nicht kompilieren.Auf diese Weise erhalten Sie eine Abhängigkeit: Entity-Schicht -> Service-Schicht und wieder zurück, da der Runner Informationen benötigt, die in der Entität gespeichert sind, und wahrscheinlich eine Aktualisierung seines Status in der Datenbank vornehmen möchte.
Sie könnten den Kreis durchbrechen, indem Sie eine generische Factory verwenden, die eine TaskDefinition verwendet und einen bestimmten TaskRunner zurückgibt. Dies würde jedoch viele Wenns erfordern. Sie können Reflection verwenden, um einen Runner zu finden, der ähnlich wie Ihre Definition benannt ist. Seien Sie jedoch vorsichtig. Dieser Ansatz kann einige Leistung kosten und zu Laufzeitfehlern führen.
PS Ich gehe hier von Java aus. Ich denke, dass es in .net ähnlich ist. Das Hauptproblem hierbei ist die Doppelbindung.
Zum Besuchermuster
Ich denke, es war eher dazu gedacht, einen Algorithmus zur Laufzeit gegen verschiedene Arten von Datenobjekten auszutauschen, als für reine Doppelbindungszwecke. Zum Beispiel, wenn Sie verschiedene Arten von Versicherungen haben und diese unterschiedlich berechnen, z. B. weil verschiedene Länder dies verlangen. Dann wählen Sie eine bestimmte Berechnungsmethode und wenden sie auf mehrere Versicherungen an.
In Ihrem Fall würden Sie eine bestimmte Aufgabenstrategie (z. B. E-Mail) auswählen und auf alle Ihre Aufgaben anwenden, was falsch ist, da nicht alle E-Mail-Aufgaben sind.
PS Ich habe es nicht getestet, aber ich denke, Ihre Option 4 wird auch nicht funktionieren, da sie wieder doppelt bindend ist.
quelle
Ich bin mit diesem Artikel überhaupt nicht einverstanden. Services (konkret ihre "API") sind wichtige Parteien der Geschäftsdomäne und werden als solche innerhalb des Domänenmodells existieren. Und es gibt kein Problem mit Entitäten in der Geschäftsdomäne, die auf etwas anderes in derselben Geschäftsdomäne verweisen.
Ist eine Geschäftsregel. Dazu wird ein Dienst benötigt, der E-Mails sendet. Und Entitäten, die damit umgehen,
When X
sollten über diesen Service Bescheid wissen.Es gibt jedoch einige Probleme bei der Implementierung. Für den Benutzer der Entität sollte transparent sein, dass die Entität einen Dienst verwendet. Das Hinzufügen des Dienstes im Konstruktor ist also keine gute Sache. Dies ist auch ein Problem, wenn Sie die Entität aus der Datenbank deserialisieren, da Sie sowohl die Daten der Entität als auch die Instanzen von Diensten festlegen müssen. Die beste Lösung, die ich mir vorstellen kann, ist die Verwendung der Eigenschaftsinjektion, nachdem die Entität erstellt wurde. Möglicherweise wird jede neu erstellte Instanz einer Entität gezwungen, die "Initialisierungs" -Methode zu durchlaufen, mit der alle Entitäten eingefügt werden, die die Entität benötigt.
quelle
Das ist eine gute Frage und ein interessantes Problem. Ich schlage vor, dass Sie eine Kombination aus Chain of Responsibility- und Double Dispatch- Mustern verwenden (Musterbeispiele hier ).
Definieren wir zunächst die Aufgabenhierarchie. Beachten Sie, dass es jetzt mehrere
run
Methoden gibt, um den doppelten Versand zu implementieren.Als nächstes definieren wir die
Service
Hierarchie. Wir werdenService
s verwenden, um die Kette der Verantwortung zu bilden.Das letzte Stück ist das,
RecurringTaskScheduler
das den Lade- und Ausführungsprozess koordiniert.Hier ist die Beispielanwendung, die das System demonstriert.
Ausführen der Anwendungsausgaben:
EmailService mit SendEmailTask mit Inhalt 'hier kommt die erste E-Mail'
EmailService mit SendEmailTask mit Inhalt 'hier ist die zweite E-Mail'
ExecuteService mit ExecuteTask mit Inhalt '/ root / python'
ExecuteService mit ExecuteTask mit Inhalt '/ bin / cat'
EmailService mit SendEmailTask mit Inhalt 'Hier ist die dritte E-Mail'
ExecuteService, auf dem ExecuteTask mit Inhalt '/ bin / grep' ausgeführt wird.
quelle