2010-06-02 3 views
3

Je m'excuse d'avance mais ce sera une longue question.Question de conception - comment briser la dépendance entre les classes en utilisant une interface?

Je suis bloqué. J'essaie d'apprendre les tests unitaires, C#, et les modèles de conception - tout à la fois. (C'est peut-être mon problème.) En tant que tel, je suis en train de lire l'Art of Unit Testing (Osherove), et Clean Code (Martin), et Head First Design Patterns (O'Reilly). Je commence à peine à comprendre les délégués et les événements (que vous verriez si vous étiez à la traîne de mes questions récentes). Je ne comprends toujours pas les lambdas.

Pour contextualiser tout cela, je me suis donné un projet d'apprentissage que j'appelle goAlarms. J'ai une classe d'alarme avec des membres auxquels vous vous attendez (NextAlarmTime, Name, AlarmGroup, Event Trigger, etc.)

Je voulais que le "Timer" de l'alarme soit extensible, j'ai donc créé une interface IAlarmScheduler comme suit ...

public interface AlarmScheduler 
{ 
    Dictionary<string,Alarm> AlarmList { get; } 
    void Startup(); 
    void Shutdown(); 
    void AddTrigger(string triggerName, string groupName, Alarm alarm); 
    void RemoveTrigger(string triggerName); 
    void PauseTrigger(string triggerName); 
    void ResumeTrigger(string triggerName); 
    void PauseTriggerGroup(string groupName); 
    void ResumeTriggerGroup(string groupName); 
    void SetSnoozeTrigger(string triggerName, int duration); 
    void SetNextOccurrence (string triggerName, DateTime nextOccurrence); 
} 

Cette interface IAlarmScheduler définit un composant qui déclenche une alarme (déclenchement) qui se propagera jusqu'au ma classe d'alarme et augmenter le déclenchement de l'événement de l'alarme lui-même. C'est essentiellement le composant "Timer".

J'ai trouvé que le composant Quartz.net est parfaitement adapté pour cela, j'ai donc créé une classe QuartzAlarmScheduler qui implémente IAlarmScheduler.

Tout ce qui va bien. Mon problème est que la classe d'alarme est abstraite et je veux créer beaucoup de différents types d'alarme. Par exemple, j'ai déjà une alarme Heartbeat (déclenchée tous les (int) intervalles de minutes), AppointmentAlarm (déclenchée sur la date et l'heure définies), Alarme quotidienne (déclenchée tous les jours à X) et peut-être d'autres.

Et Quartz.NET est parfaitement adapté pour gérer cela.

Mon problème est un problème de conception. Je veux être capable d'instancier une alarme de n'importe quelle sorte sans que ma classe d'alarme (ou aucune classe dérivée) ne sache quoi que ce soit à propos de Quartz. Le problème est que Quartz a des usines impressionnantes qui retournent juste la bonne configuration pour les déclencheurs qui seront nécessaires par mes classes d'alarme. Ainsi, par exemple, je peux obtenir un déclencheur de Quartz en utilisant TriggerUtils.MakeMinutelyTrigger pour créer un déclencheur pour l'alarme de battement de coeur décrite ci-dessus. Ou TriggerUtils.MakeDailyTrigger pour l'alarme quotidienne. Je suppose que je pourrais résumer de cette façon. Indirectement ou directement, je veux que mes classes d'alarmes puissent consommer les classes TriggerUtils.Make * sans rien savoir d'elles. Je sais que c'est une contradiction, mais c'est pourquoi je pose la question. Je pensais mettre un champ de délégué dans l'alarme qui serait assignée à l'une de ces méthodes de Make mais en faisant cela je crée une dépendance dure entre l'alarme et le quartz que je veux éviter à la fois pour des buts d'essai d'unité et de conception . J'ai pensé utiliser un switch pour le type dans QuartzAlarmScheduler par here mais je sais que c'est un mauvais design et j'essaie d'apprendre un bon design.

Vous êtes géniaux et merci d'avance pour vos réponses.

Seth

Répondre

1

Peut-être que vous pourriez avoir une classe AlarmFactory qui retourne le type de référence IAlarmScheduler approprié. Ce pourrait être une façon d'isoler la connaissance de Quartz et sa relation avec différentes sous-classes.

Je ne suis pas sûr qu'il sera possible de faire en sorte de sous-classes IAlarmScheduler sont complètement ignorants des détails Quartz, mais il est tout à fait possible de faire en sorte clients de IAlarmScheduler sont. C'est ce que la combinaison d'interface et d'usine fait pour vous.

0

Ceci est un problème assez commun. Vous ne contrôlez pas la classe de base de l'infrastructure et vous souhaitez coder une interface. Ce même problème se produit dans ASP.NET avec la classe HttpContext. Si vous google HttpContextWrapper ASP.NET MVC, vous trouverez beaucoup d'articles utiles qui expliquent comment Microsoft a brisé la dépendance avec l'espace de noms System.Web.Abstractions.

Voici le résumé. C'est un problème à contourner, mais c'est un modèle bien compris.

Vous définissez un AlarmSchedulerWrapper qui accepte un IAlarmScheduler en tant que paramètre constructeur. AlarmSchedulerWrapper transmet tous les appels au IAlarmScheduler composé. Vous pouvez maintenant passer votre QuartzAlarmScheduler qui implémente IAlarmScheduler. C'est le seul endroit où vous aurez besoin de la dépendance. Pour tester, vous pouvez soit simuler AlarmScheduler, soit créer un double (AlarmSchedulerDouble). Le AlarmSchedulerDouble peut être écrit pour simuler des événements d'alarme et enregistrer des appels contre lui (ce qui en fait un "Spy").

Si vous envisagez d'implémenter un grand nombre d'instances AlarmScheduler concrètes, vous créez généralement une AlarmSchedulerBase. AlarmSchedulerWrapper et AlarmSchedulerDouble l'utiliseraient comme leur classe de base. Edit: Lisez votre question plus en détail et je vois que ce sont les déclencheurs dont vous avez besoin de faire abstraction. Réponse similaire Définissez IAlarmTriggers, un wrapper qui les implémente en redirigeant vers Quartz, et un double pour les tests. Notez que les utilitaires de déclenchement sont probablement statiques. Toujours, il suffit de les composer à partir d'une classe instanciée.

0

Je pense que je comprends ce que vous voulez et ce que votre problème, mais laissez-moi savoir si je ne suis pas :)

Tout d'abord, pour le moment, juste dans le but de l'apprentissage OOD proprement dit, oublier les délégués, les événements et les lambdas. Il y a une raison pour laquelle je dis cela, vous le comprendrez plus tard. Pour l'instant, concentrez-vous sur les interfaces et leurs implémentations. Ce que vous appelez "event" peut également être implémenté en tant qu'interface (par exemple IHaveAMethodThatYouShouldCall avec la méthode étant TheMethodToCall). Maintenant, votre alarme implémenterait l'interface IAmAnAlarmAndWhenAlarmSchedulerThinkItIsTheTimeHeWillLetMeKnow, avec seulement la méthode ItIsTheTime.

Votre AlarmScheduler mettrait en œuvre IAmAlarmSchedulerThatWillNotifyAlarmWhenItIsTheTime d'interface avec la seule méthode RegisterAlarm (alarme IAmAnAlarmAndWhenAlarmSchedulerThinkItIsTheTimeHeWillLetMeKnow).

En outre, votre alarme prendra dans son constructeur un objet de type IAmAlarmSchedulerThatWillNotifyAlarmWhenItIsTheTime et appellera son RegisterAlarm en lui passant son propre self. À ce moment, le AlarmScheduler passé à l'intérieur stockera le pointeur sur Alarm dans sa variable privée et appellera sa méthode ItIsTheTime si nécessaire.

Chaque fois que vous créez une alarme, vous devez d'abord créer AlarmScheduler et transmettre son instance au constructeur de l'alarme.

J'espère que cela a du sens.

0

Je suis d'accord avec Rob.AlarmScheduler n'est pas la chose qui a besoin d'une interface. ITrigger a besoin d'une interface.

Modifier AlarmScheduler pour que ses méthodes fonctionnent avec les interfaces ITrigger. De cette façon, AlarmScheduler peut travailler avec des implémentations de ITrigger sans savoir ce qu'elles sont ou comment elles sont construites. À partir de là, vous avez le choix de la manière d'intégrer ITriggers dans le planificateur.

  1. Créez un bootstrappeur pour les instancier manuellement au démarrage.
  2. Utilisez une structure d'injection de dépendance pour faire la même chose.
  3. Créer une classe AlarmFactory avec des méthodes telles que:

ITrigger CreateMyCustomTriggerType1(param1, param2, param3)
et
ITrigger CreateMyCustomTriggerType2(param1)

Et pendant que nous sommes sur le sujet - il n'y a rien de mal à utiliser énumérations pour dicter le type. L'utilisation d'une énumération dicterait une sorte d'instruction Switch pour décider du type à créer. Du point de vue de la conception, cela ne pose aucun problème tant que l'instruction switch n'est pas répétée. Si vous isolez l'instruction Switch derrière une méthode FACTORY, vous avez fait preuve de diligence raisonnable pour ce qui est de la bonne conception OOP.

Questions connexes