Le but de ma tâche est de concevoir un petit système capable d'exécuter des tâches récurrentes planifiées. Une tâche récurrente est quelque chose comme "envoyer un e-mail à l'administrateur toutes les heures de 8h00 à 17h00, du lundi au vendredi".
J'ai une classe de base appelée 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(){
}
}
Et j'ai plusieurs classes héritées de RecurringTask . L'un d'eux s'appelle SendEmailTask .
public class SendEmailTask : RecurringTask{
private Email email;
public SendEmailTask(Email email){
this.email = email;
}
public override void Run(){
// need to send out email
}
}
Et j'ai un EmailService qui peut m'aider à envoyer un email.
La dernière classe est RecurringTaskScheduler , elle est chargée de charger les tâches depuis le cache ou la base de données et d'exécuter la tâche.
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();
}
}
}
}
Voici mon problème: où dois-je mettre EmailService ?
Option1 : injecter EmailService dans 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);
}
}
Il y a déjà des discussions sur l'opportunité d'injecter un service dans une entité, et la plupart des gens conviennent que ce n'est pas une bonne pratique. Voir cet article .
Option 2: Si ... Sinon dans 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
}
}
}
}
}
On m'a dit que si ... Sinon et lancer comme ci-dessus n'est pas OO, et apportera plus de problèmes.
Option3: modifiez la signature de Run et créez ServiceBundle .
public class ServiceBundle{
public EmailService EmailService{get;set}
public CleanDiskService CleanDiskService{get;set;}
// and other services for other recurring tasks
}
Injectez cette classe dans 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);
}
}
}
}
La méthode Run de SendEmailTask serait
public void Run(ServiceBundle serviceBundle){
serviceBundle.EmailService.send(this.email);
}
Je ne vois pas de gros problèmes avec cette approche.
Option4 : modèle de visiteur.
L'idée de base est de créer un visiteur qui encapsulera des services comme 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){
//
}
}
Et nous devons également modifier la signature de la méthode Run . La méthode Run de SendEmailTask est
public void Run(RecurringTaskVisitor visitor){
visitor.visit(this);
}
Il s'agit d'une implémentation typique de Visitor Pattern, et le visiteur sera injecté dans RecurringTaskScheduler .
En résumé: Parmi ces quatre approches, laquelle est la meilleure pour mon scénario? Et y a-t-il une grande différence entre Option3 et Option4 pour ce problème?
Ou avez-vous une meilleure idée de ce problème? Merci!
Mise à jour du 22/05/2015 : Je pense que la réponse d'Andy résume très bien mon intention; si vous êtes toujours confus au sujet du problème lui-même, je vous suggère de lire d'abord son article.
Je viens de découvrir que mon problème est très similaire au problème d' envoi de messages , ce qui conduit à l'option 5.
Option 5 : convertir mon problème en envoi de messages .
Il existe un mappage un à un entre mon problème et le problème d' envoi de messages :
Répartiteur de messages : recevez IMessage et envoyez des sous-classes IMessage à leurs gestionnaires correspondants. → RecurringTaskScheduler
IMessage : une interface ou une classe abstraite. → Tâche récurrente
MessageA : S'étend de IMessage , ayant des informations supplémentaires. → SendEmailTask
MessageB : une autre sous-classe de IMessage . → CleanDiskTask
MessageAHandler : lorsque vous recevez MessageA , gérez-le → SendEmailTaskHandler, qui contient EmailService, et enverra un e-mail lorsqu'il recevra SendEmailTask
MessageBHandler : Identique à MessageAHandler , mais gère MessageB à la place. → CleanDiskTaskHandler
La partie la plus difficile est de savoir comment envoyer différents types d' IMessage à différents gestionnaires. Voici un lien utile .
J'aime vraiment cette approche, elle ne pollue pas mon entité avec le service et elle n'a pas de classe divine .
SendEmailTask
me semble plus un service qu'une entité. J'irais pour l'option 1 sans hésitation.accept
visiteurs. La motivation de Visitor est que vous avez de nombreux types de classes dans certains agrégats qui nécessitent une visite, et il n'est pas pratique de modifier leur code pour chaque nouvelle fonctionnalité (opération). Je ne vois toujours pas ce que sont ces objets agrégés et je pense que Visitor n'est pas approprié. Si c'est le cas, vous devez modifier votre question (qui fait référence au visiteur).Réponses:
Je dirais que l' option 1 est la meilleure voie à suivre. La raison pour laquelle vous ne devez pas le rejeter est que ce
SendEmailTask
n'est pas une entité. Une entité est un objet concerné par la conservation des données et de l'état. Votre classe en a très peu. En fait, ce n'est pas une entité, mais elle contient une entité: l'Email
objet que vous stockez. Cela signifie que celaEmail
ne devrait pas prendre un service, ou avoir une#Send
méthode. Au lieu de cela, vous devriez avoir des services qui prennent des entités, comme la vôtreEmailService
. Vous suivez donc déjà l'idée de garder les services hors des entités.Puisqu'il
SendEmailTask
ne s'agit pas d'une entité, il est donc tout à fait correct d'y injecter l'e-mail et le service, et cela doit être fait par le constructeur. En faisant l'injection de constructeur, nous pouvons être sûrs qu'ilSendEmailTask
est toujours prêt à effectuer son travail.Voyons maintenant pourquoi ne pas faire les autres options (en particulier en ce qui concerne SOLID ).
Option 2
On vous a dit à juste titre que la ramification sur un type comme celui-ci entraînerait plus de maux de tête. Voyons pourquoi. Tout d'abord, les
if
s ont tendance à se regrouper et à croître. Aujourd'hui, c'est une tâche d'envoyer des e-mails, demain, chaque type de classe a besoin d'un service différent ou d'un autre comportement. Gérer cetteif
déclaration devient un cauchemar. Puisque nous nous branchons sur le type (et dans ce cas le type explicite ), nous subvertissons le système de type intégré dans notre langage.L'option 2 n'est pas la responsabilité unique (SRP) car l'ancien, réutilisable,
RecurringTaskScheduler
doit désormais connaître tous ces différents types de tâches et tous les types de services et de comportements dont ils pourraient avoir besoin. Cette classe est beaucoup plus difficile à réutiliser. Il n'est pas non plus ouvert / fermé (OCP). Parce qu'il a besoin de connaître ce type de tâche ou celle-là (ou ce type de service ou celle-là), des modifications disparates des tâches ou des services pourraient forcer des changements ici. Ajouter une nouvelle tâche? Ajouter un nouveau service? Changer la façon dont les e-mails sont traités? ChangerRecurringTaskScheduler
. Parce que le type de tâche est important, elle n'adhère pas à la substitution de Liskov (LSP). Il ne peut pas simplement obtenir une tâche et être fait. Il doit demander le type et en fonction du type faire ceci ou cela. Plutôt que de résumer les différences dans les tâches, nous tirons tout cela dans leRecurringTaskScheduler
.Option 3
L'option 3 présente de gros problèmes. Même dans l'article auquel vous créez un lien , l'auteur déconseille de faire ceci:
Vous créez un localisateur de services avec votre
ServiceBundle
classe. Dans ce cas, il ne semble pas être statique, mais il présente encore de nombreux problèmes inhérents à un localisateur de services. Vos dépendances sont désormais cachées sous celaServiceBundle
. Si je vous donne l'API suivante de ma nouvelle tâche cool:Quels sont les services que j'utilise? Quels services doivent être simulés lors d'un test? Qu'est-ce qui m'empêche d'utiliser tous les services du système, simplement parce que?
Si je veux utiliser votre système de tâches pour exécuter certaines tâches, je dépend maintenant de tous les services de votre système, même si je n'en utilise que quelques-uns, voire aucun.
Ce
ServiceBundle
n'est pas vraiment SRP car il doit connaître tous les services de votre système. Ce n'est pas non plus OCP. L'ajout de nouveaux services signifie des changements dans leServiceBundle
, et des changements dans leServiceBundle
peuvent signifier des changements disparates dans les tâches ailleurs.ServiceBundle
ne sépare pas son interface (ISP). Il a une interface tentaculaire de tous ces services, et parce que ce n'est qu'un fournisseur pour ces services, nous pourrions considérer que son interface englobe également les interfaces de tous les services qu'il fournit. Les tâches n'adhèrent plus à l'inversion de dépendance (DIP), car leurs dépendances sont masquées derrière leServiceBundle
. Cela n'adhère pas non plus au principe de moindre connaissance (alias la loi de Déméter) parce que les choses en savent beaucoup plus qu'elles ne le doivent.Option 4
Auparavant, vous disposiez de nombreux petits objets capables de fonctionner indépendamment. L'option 4 prend tous ces objets et les brise en un seul
Visitor
objet. Cet objet agit comme un objet divin sur toutes vos tâches. Il réduit vosRecurringTask
objets à des ombres anémiques qui appellent simplement un visiteur. Tout le comportement se déplace vers leVisitor
. Besoin de changer de comportement? Besoin d'ajouter une nouvelle tâche? ChangerVisitor
.La partie la plus difficile est que, parce que tous les différents comportements sont tous dans une seule classe, la modification de certains traînées polymorphes le long de tous les autres comportements. Par exemple, nous voulons avoir deux façons différentes d'envoyer des e-mails (ils devraient peut-être utiliser des serveurs différents?). Comment le ferions-nous? Nous pourrions créer une
IVisitor
interface et l'implémenter, en dupliquant potentiellement du code, comme celui#Visit(ClearDiskTask)
de notre visiteur d'origine. Ensuite, si nous trouvons une nouvelle façon d'effacer un disque, nous devons l'implémenter et la dupliquer à nouveau. Ensuite, nous voulons les deux types de changements. Implémentez et dupliquez à nouveau. Ces deux comportements différents et disparates sont inextricablement liés.Peut-être que nous pourrions simplement sous-classer
Visitor
? Sous-classe avec un nouveau comportement de messagerie, sous-classe avec un nouveau comportement de disque. Pas de duplication jusqu'à présent! Sous-classe avec les deux? Maintenant, l'un ou l'autre doit être dupliqué (ou les deux si c'est votre préférence).Comparons à l'option 1: nous avons besoin d'un nouveau comportement de messagerie. Nous pouvons créer un nouveau
RecurringTask
qui fait le nouveau comportement, injecter dans ses dépendances et l'ajouter à la collection de tâches dans leRecurringTaskScheduler
. Nous n'avons même pas besoin de parler de nettoyer les disques, car cette responsabilité est entièrement ailleurs. Nous avons également toujours la gamme complète d'outils OO à notre disposition. Nous pourrions décorer cette tâche avec la journalisation, par exemple.L'option 1 vous apportera le moins de douleur et est la façon la plus correcte de gérer cette situation.
la source
SendEmailTask
d'une base de données, alors cette configuration doit être une classe de configuration distincte qui doit également être injectée dans votreSendEmailTask
. Si vous générez des données à partir de votreSendEmailTask
, vous devez créer un objet memento pour stocker l'état et le mettre dans votre base de données.EMailTaskDefinitions
etEmailService
dansSendEmailTask
? Ensuite, dans leRecurringTaskScheduler
, j'ai besoin d'injecter quelque chose commeSendEmailTaskRepository
la responsabilité de charger la définition et le service et de les injecterSendEmailTask
. Mais je dirais maintenant laRecurringTaskScheduler
nécessité de connaître le référentiel de chaque tâche, commeCleanDiskTaskRepository
. Et je dois changerRecurringTaskScheduler
chaque fois que j'ai une nouvelle tâche (pour ajouter un référentiel dans le planificateur).RecurringTaskScheduler
ne devrait connaître que le concept d'un référentiel de tâches généralisé et aRecurringTask
. Ce faisant, cela peut dépendre des abstractions. Les référentiels de tâches peuvent être injectés dans le constructeur deRecurringTaskScheduler
. Il suffit alors de savoir où les différents référentielsRecurringTaskScheduler
sont instanciés (ou peuvent être cachés dans une usine et appelés à partir de là). Parce qu'il ne dépend que des abstractions,RecurringTaskScheduler
n'a pas besoin de changer à chaque nouvelle tâche. C'est l'essence de l'inversion de dépendance.Avez-vous jeté un œil aux bibliothèques existantes, par exemple quartz à ressort ou lot de ressorts (je ne sais pas ce qui correspond le mieux à vos besoins)?
A votre question:
Je suppose que le problème est que vous voulez conserver certaines métadonnées à la tâche de manière polymorphe, donc une tâche de messagerie a des adresses de messagerie attribuées, une tâche de journal un niveau de journal, etc. Vous pouvez stocker une liste de ceux-ci en mémoire ou dans votre base de données, mais pour séparer les préoccupations, vous ne voulez pas que l'entité soit polluée par le code de service.
Ma solution proposée:
Je séparerais la partie courante et la partie données de la tâche, pour avoir par exemple
TaskDefinition
et aTaskRunner
. La TaskDefinition a une référence à un TaskRunner ou à une fabrique qui en crée un (par exemple si une configuration est requise comme l'hôte smtp). L'usine est spécifique - elle ne peut gérer queEMailTaskDefinition
s et ne renvoie que les instances deEMailTaskRunner
s. De cette façon, il est plus OO et sûr du changement - si vous introduisez un nouveau type de tâche, vous devez introduire une nouvelle usine spécifique (ou en réutiliser une), sinon, vous ne pouvez pas compiler.De cette façon, vous vous retrouveriez avec une dépendance: couche d'entité -> couche de service et vice-versa, car le Runner a besoin d'informations stockées dans l'entité et souhaite probablement mettre à jour son état dans la base de données.
Vous pouvez briser le cercle en utilisant une fabrique générique, qui prend une TaskDefinition et retourne un TaskRunner spécifique , mais cela nécessiterait beaucoup de ifs. Vous pouvez utiliser la réflexion pour trouver un exécuteur portant le même nom que votre définition, mais soyez prudent, cette approche peut coûter des performances et entraîner des erreurs d'exécution.
PS Je suppose Java ici. Je pense que c'est similaire en .net. Le principal problème ici est la double liaison.
Vers le modèle visiteur
Je pense qu'il était plutôt destiné à être utilisé pour échanger un algorithme pour différents types d'objets de données lors de l'exécution, qu'à des fins de double liaison pure. Par exemple, si vous avez différents types d'assurances et différents types de calcul, par exemple parce que différents pays l'exigent. Ensuite, vous choisissez une méthode de calcul spécifique et l'appliquez sur plusieurs assurances.
Dans votre cas, vous choisiriez une stratégie de tâche spécifique (par exemple, un e-mail) et l'appliqueriez à toutes vos tâches, ce qui est faux car toutes ne sont pas des tâches de messagerie.
PS Je ne l'ai pas testé, mais je pense que votre option 4 ne fonctionnera pas non plus, car elle est à nouveau contraignante.
la source
Je suis totalement en désaccord avec cet article. Les services (concrètement leur "API") sont une partie importante du domaine d'activité et en tant que tels, ils existeront au sein du modèle de domaine. Et il n'y a aucun problème avec les entités du domaine métier référençant autre chose dans le même domaine métier.
Est une règle commerciale. Et pour ce faire, un service d'envoi de courrier est nécessaire. Et l'entité qui gère
When X
doit connaître ce service.Mais il y a quelques problèmes avec la mise en œuvre. Il doit être transparent pour l'utilisateur de l'entité, que l'entité utilise un service. Donc, ajouter le service dans constructeur n'est pas une bonne chose. C'est également un problème lorsque vous désérialisez l'entité de la base de données, car vous devez définir à la fois les données de l'entité et les instances de services. La meilleure solution à laquelle je peux penser est d'utiliser l'injection de propriété après la création de l'entité. Peut-être forcer chaque instance nouvellement créée d'une entité à passer par la méthode "initialiser" qui injecte toutes les entités dont l'entité a besoin.
la source
C'est une grande question et un problème intéressant. Je vous propose d'utiliser une combinaison de modèles de chaîne de responsabilité et de double répartition (exemples de modèles ici ).
Permet d'abord de définir la hiérarchie des tâches. Notez qu'il existe désormais plusieurs
run
méthodes pour implémenter la double répartition.Permet ensuite de définir la
Service
hiérarchie. Nous utiliseronsService
s pour former la chaîne de responsabilité.La dernière pièce est celle
RecurringTaskScheduler
qui orchestre le processus de chargement et d'exécution.Maintenant, voici l'exemple d'application illustrant le système.
Exécution des sorties de l'application:
EmailService exécutant SendEmailTask avec contenu 'voici le premier email'
EmailService exécutant SendEmailTask avec contenu 'voici le deuxième email'
ExecuteService exécutant ExecuteTask avec contenu '/ root / python'
ExecuteService exécutant ExecuteTask avec contenu '/ bin / cat'
EmailService exécutant SendEmailTask avec contenu 'voici le troisième e-mail'
ExecuteService exécutant ExecuteTask avec le contenu '/ bin / grep'
la source