Le modèle de visiteur est-il valide dans ce scénario?

9

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 .

Sher10ck
la source
Vous n'avez pas tagué de langue ou de plate-forme, mais je vous recommande d'examiner Cron . Votre plate-forme peut avoir une bibliothèque qui fonctionne de manière similaire (par exemple jcron qui semble en quelque sorte disparu). La planification des travaux et des tâches est en grande partie un problème résolu: avez-vous examiné d'autres options avant de lancer la vôtre? Y avait-il des raisons de ne pas les utiliser?
@Snowman Nous pourrons passer à une bibliothèque mature plus tard. Tout dépend de mon manager. La raison pour laquelle je poste cette question est que je veux trouver un moyen de résoudre ce «genre» de problème. J'ai vu ce genre de problème plus d'une fois et je n'ai pas trouvé de solution élégante. Je me demande donc si j'ai fait quelque chose de mal.
Sher10ck
Assez juste, j'essaie toujours de recommander la réutilisation du code si possible.
1
SendEmailTaskme semble plus un service qu'une entité. J'irais pour l'option 1 sans hésitation.
Bart van Ingen Schenau
3
Ce qui me manque (pour moi) pour Visitor, c'est la structure de classe des acceptvisiteurs. 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).
Fuhrmanator

Réponses:

4

Je dirais que l' option 1 est la meilleure voie à suivre. La raison pour laquelle vous ne devez pas le rejeter est que ce SendEmailTaskn'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' Emailobjet que vous stockez. Cela signifie que cela Emailne devrait pas prendre un service, ou avoir une #Sendméthode. Au lieu de cela, vous devriez avoir des services qui prennent des entités, comme la vôtre EmailService. Vous suivez donc déjà l'idée de garder les services hors des entités.

Puisqu'il SendEmailTaskne 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'il SendEmailTaskest 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 ifs 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 cette ifdé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, RecurringTaskSchedulerdoit 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? Changer RecurringTaskScheduler. 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 le RecurringTaskScheduler.

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 pouvez toujours utiliser un localisateur de service statique…
  • J'évite le localisateur de service quand je le peux, surtout lorsque le localisateur de service doit être statique…

Vous créez un localisateur de services avec votre ServiceBundleclasse. 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 cela ServiceBundle. Si je vous donne l'API suivante de ma nouvelle tâche cool:

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

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 ServiceBundlen'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 le ServiceBundle, et des changements dans le ServiceBundlepeuvent signifier des changements disparates dans les tâches ailleurs. ServiceBundlene 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 le ServiceBundle. 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 Visitorobjet. Cet objet agit comme un objet divin sur toutes vos tâches. Il réduit vos RecurringTaskobjets à des ombres anémiques qui appellent simplement un visiteur. Tout le comportement se déplace vers le Visitor. Besoin de changer de comportement? Besoin d'ajouter une nouvelle tâche? Changer Visitor.

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 IVisitorinterface 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 RecurringTaskqui fait le nouveau comportement, injecter dans ses dépendances et l'ajouter à la collection de tâches dans le RecurringTaskScheduler. 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.

cbojar
la source
Votre analyse sur Otion2,3,4 est fantastique! Cela m'aide beaucoup. Mais pour Option1, je dirais que * SendEmailTask ​​* est une entité. Il a id, il a son modèle récurrent et d'autres informations utiles qui devraient être stockées dans db. Je pense qu'Andy résume bien mon intention. Peut-être qu'un nom comme * EMailTaskDefinitions * est plus approprié. je ne veux pas polluer mon entité avec mon code de service. Euphoric mentionne un problème si j'injecte un service dans l'entité. Je mets également à jour ma question et j'inclus Option5, qui, à mon avis, est la meilleure solution jusqu'à présent.
Sher10ck
@ Sher10ck Si vous extrayez la configuration de votre SendEmailTaskd'une base de données, alors cette configuration doit être une classe de configuration distincte qui doit également être injectée dans votre SendEmailTask. Si vous générez des données à partir de votre SendEmailTask, vous devez créer un objet memento pour stocker l'état et le mettre dans votre base de données.
cbojar
J'ai besoin de retirer la configuration de db, alors u suggère-t-il d'injecter à la fois EMailTaskDefinitionset EmailServicedans SendEmailTask? Ensuite, dans le RecurringTaskScheduler, j'ai besoin d'injecter quelque chose comme SendEmailTaskRepositoryla responsabilité de charger la définition et le service et de les injecter SendEmailTask. Mais je dirais maintenant la RecurringTaskSchedulernécessité de connaître le référentiel de chaque tâche, comme CleanDiskTaskRepository. Et je dois changer RecurringTaskSchedulerchaque fois que j'ai une nouvelle tâche (pour ajouter un référentiel dans le planificateur).
Sher10ck
@ Sher10ck The RecurringTaskSchedulerne devrait connaître que le concept d'un référentiel de tâches généralisé et a RecurringTask. Ce faisant, cela peut dépendre des abstractions. Les référentiels de tâches peuvent être injectés dans le constructeur de RecurringTaskScheduler. Il suffit alors de savoir où les différents référentiels RecurringTaskSchedulersont 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, RecurringTaskSchedulern'a pas besoin de changer à chaque nouvelle tâche. C'est l'essence de l'inversion de dépendance.
cbojar
3

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 TaskDefinitionet a TaskRunner. 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 que EMailTaskDefinitions et ne renvoie que les instances de EMailTaskRunners. 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.

Andy
la source
Tu résumes très bien mon intention, merci! Je voudrais briser le cercle. Parce que laisser TaskDefiniton contenir une référence à TaskRunner ou à l' usine a le même problème que Option1. Je traite l' usine ou TaskRunner comme un service. Si TaskDefinition a besoin de leur contenir une référence, vous devez soit injecter le service dans TaskDefinition , soit utiliser une méthode statique, ce que j'essaie d'éviter.
Sher10ck
1

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.

Lorsque X envoie un courrier à Y.

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 Xdoit 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.

Euphorique
la source
De quel article parlez-vous avec lequel vous n'êtes pas d'accord? Cependant, point de vue intéressant sur le modèle de domaine. Vous pouvez probablement le voir comme ça, cependant, les gens évitent généralement de mélanger les services en entités, car cela créera très rapidement un couplage étroit.
Andy
@Andy Celui que Sher10ck a mentionné dans sa question. Et je ne vois pas comment cela créerait un couplage serré. Tout code mal écrit peut introduire un couplage étroit.
Euphoric
1

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 runméthodes pour implémenter la double répartition.

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

Permet ensuite de définir la Servicehiérarchie. Nous utiliserons Services pour former la chaîne de responsabilité.

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

La dernière pièce est celle RecurringTaskSchedulerqui orchestre le processus de chargement et d'exécution.

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

Maintenant, voici l'exemple d'application illustrant le système.

public class App {

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

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'

iluwatar
la source
J'ai peut-être beaucoup de tâches . Chaque fois que j'ajoute une nouvelle tâche , je dois changer RecurringTask et je dois également changer toutes ses sous-classes, car j'ai besoin d'ajouter une nouvelle fonction comme l'exécution booléenne abstraite publique (OtherService otherService) . Je pense que l'option 4, le modèle de visiteur qui implémente également la double répartition, a le même problème.
Sher10ck
Bon point. J'ai modifié ma réponse afin que les méthodes d'exécution (service) soient définies dans la tâche récurrente et retournent false par défaut. De cette façon, lorsque vous devez ajouter une autre classe de tâches, vous n'avez pas besoin de toucher aux tâches de la fratrie.
iluwatar