2008-10-09 7 views
8

Courir FxCop sur mon code, je reçois cet avertissement:Le couplage est trop élevé - comment mieux concevoir cette classe?

Microsoft.Maintainability: « FooBar.ctor est couplé avec 99 différents types de 9 différents namespaces. Récrire ou réusiner la méthode pour diminuer son raccord de classe, ou envisager de déplacer la méthode à une d'autres types, il est étroitement couplé avec . Un couplage de classe supérieur à 40 indique une mauvaise maintenabilité, un couplage de classe entre 40 et 30 indique une maintenabilité modérée, et un couplage de classe inférieur à 30 indique une bonne maintenabilité.

Ma classe est une zone d'atterrissage pour tous les messages du serveur. Le serveur peut nous envoyer des messages de différents types de EventArgs:

public FooBar() 
{ 
    var messageHandlers = new Dictionary<Type, Action<EventArgs>>(); 
    messageHandlers.Add(typeof(YouHaveBeenLoggedOutEventArgs), HandleSignOut); 
    messageHandlers.Add(typeof(TestConnectionEventArgs), HandleConnectionTest); 
    // ... etc for 90 other types 
} 

Les méthodes "HandleSignOut" et "HandleConnectionTest" ont peu de code en eux; ils passent généralement le travail à une fonction dans une autre classe.

Comment puis-je améliorer cette classe avec un couplage inférieur?

+0

J'aime les réponses ici, mais peut-être un endroit comme RefactorMyCode.com pourrait être un meilleur endroit pour cela. Il est un peu plus facile pour tout le monde d'afficher du code dans leurs réponses sur ce site. –

+0

Ok, j'ai posté l'exemple, vérifiez-le –

Répondre

15

Demandez aux classes qui font le registre de travail pour les événements qui les intéressent ... un modèle event broker.

class EventBroker { 
    private Dictionary<Type, Action<EventArgs>> messageHandlers; 

    void Register<T>(Action<EventArgs> subscriber) where T:EventArgs { 
     // may have to combine delegates if more than 1 listener 
     messageHandlers[typeof(T)] = subscriber; 
    } 

    void Send<T>(T e) where T:EventArgs { 
     var d = messageHandlers[typeof(T)]; 
     if (d != null) { 
     d(e); 
     } 
    } 
} 
+0

Exactement ce dont je parlais juste. Je suis en train de voter le vôtre à cause de l'exemple. – NotMe

+0

Intéressant! Merci pour la réponse, nous allons prendre cela en considération. Je pense que je vais accepter le vôtre comme la réponse jusqu'à ce que je vois un meilleur. –

+0

Intéressant! J'ai rencontré le même problème. Est-ce assez rapide? Je veux dire, puis-je l'utiliser dans un jeu avec une boucle de mise à jour? – Vinz243

0

Peut-être au lieu d'avoir une classe différente pour chaque message, utilisez un drapeau qui identifie le message.

Cela réduirait considérablement le nombre de messages que vous avez et augmenterait la maintenabilité. Ma conjecture est que la plupart des classes de message ont environ zéro différence.

Il est difficile de choisir une autre façon d'attaquer parce que le reste de l'architecture est inconnue (pour moi).

Si vous regardez Windows, par exemple, il ne sait pas comment gérer nativement chaque message qui pourrait être jeté au sujet. Au lieu de cela, les gestionnaires de messages sous-jacents enregistrent des fonctions de rappel avec le thread principal.

Vous pourriez adopter une approche similaire. Chaque classe de message aurait besoin de savoir comment se gérer et pourrait s'inscrire avec l'application plus grande. Cela devrait grandement simplifier le code et se débarrasser du couplage serré.

+0

Merci pour la réponse. Nous avons essayé cela il y a quelque temps, mais cela ne fait que déplacer la complexité vers les fonctions du gestionnaire - au lieu de 99 minuscules fonctions de gestionnaire, vous avez 20 fonctions très importantes. De meilleures idées? –

5

Vous pouvez également utiliser une sorte de cadre IoC, comme Spring.NET, pour injecter le dictionnaire. De cette façon, si vous obtenez un nouveau type de message, vous n'avez pas besoin de recompiler ce concentrateur central - il suffit de changer un fichier de configuration.


L'exemple tant attendu:

Créer une nouvelle application de la console, nommée exemple, et ajoutez ceci:

using System; 
using System.Collections.Generic; 
using Spring.Context.Support; 

namespace Example 
{ 
    internal class Program 
    { 
     private static void Main(string[] args) 
     { 
      MessageBroker broker = (MessageBroker) ContextRegistry.GetContext()["messageBroker"]; 
      broker.Dispatch(null, new Type1EventArgs()); 
      broker.Dispatch(null, new Type2EventArgs()); 
      broker.Dispatch(null, new EventArgs()); 
     } 
    } 

    public class MessageBroker 
    { 
     private Dictionary<Type, object> handlers; 

     public Dictionary<Type, object> Handlers 
     { 
      get { return handlers; } 
      set { handlers = value; } 
     } 

     public void Dispatch<T>(object sender, T e) where T : EventArgs 
     { 
      object entry; 
      if (Handlers.TryGetValue(e.GetType(), out entry)) 
      { 
       MessageHandler<T> handler = entry as MessageHandler<T>; 
       if (handler != null) 
       { 
        handler.HandleMessage(sender, e); 
       } 
       else 
       { 
        //I'd log an error here 
        Console.WriteLine("The handler defined for event type '" + e.GetType().Name + "' doesn't implement the correct interface!"); 
       } 
      } 
      else 
      { 
       //I'd log a warning here 
       Console.WriteLine("No handler defined for event type: " + e.GetType().Name); 
      } 
     } 
    } 

    public interface MessageHandler<T> where T : EventArgs 
    { 
     void HandleMessage(object sender, T message); 
    } 

    public class Type1MessageHandler : MessageHandler<Type1EventArgs> 
    { 
     public void HandleMessage(object sender, Type1EventArgs args) 
     { 
      Console.WriteLine("Type 1, " + args.ToString()); 
     } 
    } 

    public class Type2MessageHandler : MessageHandler<Type2EventArgs> 
    { 
     public void HandleMessage(object sender, Type2EventArgs args) 
     { 
      Console.WriteLine("Type 2, " + args.ToString()); 
     } 
    } 

    public class Type1EventArgs : EventArgs {} 

    public class Type2EventArgs : EventArgs {} 
} 

Et un fichier app.config:

<?xml version="1.0" encoding="utf-8" ?> 
<configuration> 
    <configSections> 
    <sectionGroup name="spring"> 
     <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/> 
     <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core"/> 
    </sectionGroup> 
    </configSections> 

    <spring> 
    <context> 
     <resource uri="config://spring/objects"/> 
    </context> 
    <objects xmlns="http://www.springframework.net"> 

     <object id="messageBroker" type="Example.MessageBroker, Example"> 
     <property name="handlers"> 
      <dictionary key-type="System.Type" value-type="object"> 
      <entry key="Example.Type1EventArgs, Example" value-ref="type1Handler"/> 
      <entry key="Example.Type2EventArgs, Example" value-ref="type2Handler"/> 
      </dictionary> 
     </property> 
     </object> 
     <object id="type1Handler" type="Example.Type1MessageHandler, Example"/> 
     <object id="type2Handler" type="Example.Type2MessageHandler, Example"/> 
    </objects> 
    </spring> 
</configuration> 

Sortie:

 
Type 1, Example.Type1EventArgs 
Type 2, Example.Type2EventArgs 
No handler defined for event type: EventArgs 

Comme vous pouvez le voir, MessageBroker ne connaît pas l'un des gestionnaires, et les gestionnaires ne connaissent pas MessageBroker. Tout le mappage est fait dans l'application.fichier de configuration, de sorte que si vous devez gérer un nouveau type d'événement, vous pouvez l'ajouter dans le fichier de configuration. Cela est particulièrement agréable si d'autres équipes définissent des types d'événements et des gestionnaires - ils peuvent simplement compiler leur contenu dans une DLL, vous le déposez dans votre déploiement et vous ajoutez simplement un mappage.

Le dictionnaire a des valeurs de type objet au lieu de MessageHandler<> parce que les gestionnaires réels ne peuvent pas être jeté à MessageHandler<EventArgs>, donc je devais pirater autour un peu. Je pense que la solution est toujours propre et qu'elle gère bien les erreurs de mappage. Notez que vous devrez également référencer Spring.Core.dll dans ce projet. Vous pouvez trouver les bibliothèques here et la documentation here. Le est pertinent pour cela. Notez également qu'il n'y a aucune raison d'utiliser Spring.NET pour cela - l'idée importante ici est l'injection de dépendance. D'une manière ou d'une autre, quelque chose va devoir dire au courtier d'envoyer des messages de type a à x, et l'utilisation d'un conteneur IoC pour l'injection de dépendances est un bon moyen pour que le courtier ne sache pas x, et vice versa.

Une autre SO question relative à IoC et DI:

+0

Merci pour la réponse. Cool, cela semble intéressant.Je ne suis pas clair sur un aspect: le framework IoC peut-il encore utiliser mes fonctions de gestionnaire? Je veux dire, mon dictionnaire est composé de clés EventArg et de valeurs de fonction de gestionnaire. Ioc fonctionne avec les valeurs de fonction de gestionnaire encore? –

+0

Les gestionnaires doivent être divisés en classes distinctes qui implémentent une interface, puis vous appelez leur méthode "handleMessage". Ou, vous pouvez transmettre des chaînes, et utiliser la réflexion pour récupérer les méthodes, bien que ce soit du code-malodorant. Je peux éditer avec un exemple si vous aimez. –

+0

Ok, donc au lieu de FooBar connaissant environ 99 types, nous aurions 99 types connaissant FooBar. J'aimerais voir un échantillon. –

0

Je ne vois pas le reste de votre code, mais je voudrais essayer créer un nombre beaucoup plus petit de classes d'argument d'événement. Au lieu de cela créez quelques uns qui sont similaires les uns aux autres en termes de données contenues et/ou la façon dont vous les manipulez plus tard et ajoutez un champ qui vous dira quel type exact d'événement s'est produit (probablement vous devriez utiliser une énumération).

Idéalement, vous ne serait pas seulement ce constructeur bien plus lisible, mais aussi la façon dont les messages sont traités (messages de groupe qui sont traités de manière similaire dans un seul gestionnaire d'événement)

+0

Merci pour la réponse. Chris Lively a suggéré le plus petit nombre de classes d'argument d'événement. Nous avons essayé cela, et cela ne fait que déplacer la complexité: au lieu de 99 minuscules fonctions de gestionnaire, nous avons maintenant 20 grandes fonctions de gestionnaire. –

+0

En ce qui concerne les "messages de groupe qui sont traités de manière similaire dans un seul gestionnaire d'événements", oui, nous le faisons. Nous avons environ 20 arguments d'événements qui sont traités de la même manière et sont passés au même gestionnaire. –

0

De toute évidence, vous avez besoin d'un mécanisme de répartition: en fonction de l'événement que vous recevez, vous voulez exécuter un code différent.

Vous semblez utiliser le système de type pour identifier les événements, alors qu'il est en fait censé prendre en charge le polymorphisme. Comme le suggère Chris Lively, vous pouvez tout aussi bien (sans abuser du système de caractères) utiliser un énumération pour identifier les messages. Ou vous pouvez utiliser la puissance du système de type et créer un objet de registre où chaque type d'événement est enregistré (par une instance statique, un fichier de configuration ou quoi que ce soit). Ensuite, vous pouvez utiliser le modèle Chaîne de responsabilité pour trouver le gestionnaire approprié. Soit le gestionnaire s'occupe lui-même, soit il peut s'agir d'une fabrique, créant un objet qui gère l'événement.

Cette dernière méthode ressemble un peu underspecified et overengineered, mais dans le cas de 99 types d'événements (déjà), il me semble approprié.

+0

Je ne comprends pas votre suggestion. Pouvez-vous me montrer plus de ressources ou publier un exemple? –

Questions connexes