Comment diviser de grandes classes étroitement couplées?

14

J'ai d'énormes classes de plus de 2k lignes de code (et en pleine croissance) que je voudrais refactoriser si possible, pour avoir un design plus léger et plus propre.

La raison de sa taille est principalement due au fait que ces classes gèrent un ensemble de cartes auquel la plupart des méthodes doivent accéder et que les méthodes sont très connectées les unes aux autres.

Je vais donner un exemple très concret: j'ai une classe appelée Serverqui gère les messages entrants. Il a des méthodes comme joinChatroom, searchUsers, sendPrivateMessage, etc. Toutes ces méthodes manipuler les cartes telles que users, chatrooms, servers, ...

Peut-être que ce serait bien si je pouvais avoir une classe qui gère les messages concernant les Chatrooms, une autre qui traite des utilisateurs, etc. mais le problème principal ici est que je dois utiliser toutes les cartes dans la plupart des méthodes. C'est pourquoi pour l'instant, ils sont tous collés dans la Serverclasse car ils s'appuient tous sur ces cartes communes et les méthodes sont très liées les unes aux autres.

J'aurais besoin de créer une classe Chatrooms, mais avec une référence à chacun des autres objets. Une classe utilise à nouveau une référence à tous les autres objets, etc.

J'ai l'impression que je ferais quelque chose de mal.

Matthieu
la source
Si vous créez des classes comme User et Chatroom, ces classes auraient-elles seulement besoin d'une référence à la structure de données commune ou se référenceraient-elles les unes les autres?
Il y a plusieurs réponses satisfaisantes ici, vous devez en choisir une.
jeremyjjbrown
@jeremyjjbrown la question a été déplacée et je l'ai perdue. J'ai choisi une réponse, merci.
Matthew

Réponses:

10

D'après votre description, je suppose que vos cartes ne sont que des sacs de données, avec toute la logique des Serverméthodes. En poussant toute la logique du chat dans une classe distincte, vous êtes toujours coincé avec des cartes contenant des données.

Essayez plutôt de modéliser des salons de discussion, des utilisateurs, etc. en tant qu'objets. De cette façon, vous ne passerez que des objets spécifiques requis pour une certaine méthode, au lieu d'énormes cartes de données.

Par exemple:

public class User {
  private String name;
  ...

  public void sendMessage(String message) {
    ...
  }
}

public class Chatroom {
  // users in this chatroom
  private Collection<User> users;

  public void add(User user) {
    users.add(user);
  }

  public void sendMessage(String msg) {
    for (User user : users)
      user.sendMessage(msg);
  }
}

public class Server {
  // all users on the server
  private Collection<User> users;

  // all chatrooms on the server
  private Collection<Chatroom> chatrooms;

  /* methods to handle incoming messages */
}

Il est maintenant facile d'appeler quelques méthodes spécifiques pour gérer les messages:

Vous souhaitez rejoindre un salon de discussion?

chatroom.add(user);

Vous souhaitez envoyer un message privé?

user.sendMessage(msg);

Vous souhaitez envoyer un message public?

chatroom.sendMessage(msg);
casablanca
la source
5

Vous devriez pouvoir créer une classe contenant chaque collection. Bien qu'il Serveraura besoin d'une référence à chacune de ces collections, il n'a besoin que de la quantité minimale de logique qui n'impliquera pas l'accès ou la maintenance des collections sous-jacentes. Cela rendra plus évidente exactement ce que fait le serveur et séparera comment il le fait.

Peter Lawrey
la source
4

Quand j'ai vu de grandes classes comme celle-ci, j'ai constaté qu'il y avait souvent une classe (ou plus) à essayer de sortir. Si vous connaissez une méthode qui, selon vous, pourrait ne pas être liée à cette classe, rendez-la statique. Le compilateur vous indiquera ensuite d'autres méthodes que cette méthd appelle. Java insistera également sur le fait qu'ils sont statiques. Vous les rendez statiques. Encore une fois, le compilateur vous indiquera toute méthode qu'il appelle. Vous continuez à le faire encore et encore jusqu'à ce que vous n'ayez plus d'échecs de compilation. Ensuite, vous avez un tas de méthodes statiques dans votre grande classe. Vous pouvez maintenant les extraire dans une nouvelle classe et rendre la méthode non statique. Vous pouvez ensuite appeler cette nouvelle classe à partir de votre grande classe d'origine (qui devrait maintenant contenir moins de lignes)

Vous pouvez ensuite répéter le processus jusqu'à ce que vous soyez satisfait de la conception de la classe.

Le livre de Martin Fowler est une très bonne lecture, donc je le recommanderais aussi car il y a des moments où vous ne pouvez pas utiliser cette astuce statique.

RNJ
la source
1
Ce livre de Martin Fowler martinfowler.com/books/refactoring.html
Arul
1

Étant donné que la plupart de votre code existe, je suggérerais d'utiliser des classes d'assistance pour déplacer vos méthodes. Cela facilitera la refactorisation. Ainsi, votre classe de serveur contiendra toujours les cartes. Mais il utilise une classe d'assistance, comme ChatroomHelper avec des méthodes telles que join (Map chatrooms, String user), List getUsers (Map chatrooms), Map getChatrooms (String user).

La classe serveur contiendra une instance de ChatroomHelper, UserHelper, etc., déplaçant ainsi les méthodes vers ses classes d'assistance logique. Avec cela, vous pouvez laisser les méthodes publiques du serveur intactes, de sorte que tout appelant n'a pas besoin de changer.

techuser soma
la source
1

Pour ajouter à la réponse perspicace de casablanca - si plusieurs classes doivent faire les mêmes choses de base avec un certain type d'entité (ajouter des utilisateurs à une collection, gérer les messages, etc.), ces processus doivent également être séparés.

Il existe plusieurs façons de le faire - par héritage ou par composition. Pour l'héritage, vous pouvez utiliser des classes de base abstraites avec des méthodes concrètes qui fournissent les champs et les fonctionnalités pour gérer les utilisateurs ou les messages par exemple, et avoir des entités comme chatroomet user(ou toute autre) étendre ces classes.

Pour diverses raisons , c'est une bonne règle générale d'utiliser la composition plutôt que l'héritage. Vous pouvez utiliser la composition pour le faire de différentes manières. Étant donné que la gestion des utilisateurs ou des messages est une fonction centrale de la responsabilité des classes, on peut soutenir que l'injection de constructeur est la plus appropriée. De cette façon, la dépendance est transparente et un objet ne peut pas être créé sans avoir la fonctionnalité requise. Si la façon dont les utilisateurs ou les messages sont traités est susceptible de changer ou d'être étendue, vous devriez envisager d'utiliser quelque chose comme le modèle de stratégie .

Dans les deux cas, assurez-vous de coder vers des interfaces, pas des classes concrètes pour plus de flexibilité.

Cela étant dit, tenez toujours compte du coût de la complexité supplémentaire lors de l'utilisation de tels modèles - si vous n'en avez pas besoin, ne le codez pas. Si vous savez que vous n'allez probablement pas changer la façon dont la gestion des utilisateurs / messages est effectuée, vous n'avez pas besoin de la complexité structurelle d'un modèle de stratégie - mais pour séparer les préoccupations et éviter la répétition, vous devez toujours divorcer des fonctionnalités communes à partir des instances concrètes qui l'utilisent - et, s'il n'existe aucune raison impérieuse du contraire, composez les utilisateurs de ces fonctionnalités de gestion (chatrooms, utilisateurs) avec un objet qui effectue la gestion.

Pour résumer:

  1. Comme l'a écrit casablanca: Séparez et encapsulez les salons de discussion, les utilisateurs, etc.
  2. Fonctionnalité commune séparée
  3. Envisagez de séparer les fonctionnalités individuelles pour séparer la représentation des données (ainsi que l'accès et la mutation) des fonctionnalités plus complexes sur des instances individuelles de données ou des agrégats de celles-ci (par exemple, quelque chose comme searchUserspourrait aller dans une classe de collection, ou un référentiel / carte d'identité )
Michael Bauer
la source
0

Écoutez, je pense que votre question est trop générique pour y répondre car nous n'avons pas vraiment de description complète du problème, il est donc impossible de proposer un bon design avec si peu de connaissances. Je peux, à titre d'exemple, répondre à l'une de vos préoccupations concernant l'éventuelle futilité d'une meilleure conception.

Vous dites que votre classe Server et votre future classe Chatroom partagent des données sur les utilisateurs, mais ces données devraient être différentes. Un serveur a probablement un ensemble d'utilisateurs connectés, tandis qu'un Chatroom, qui lui-même appartient à un serveur, a un ensemble d'utilisateurs différent, un sous-ensemble du premier ensemble, composé uniquement des utilisateurs actuellement connectés à un Chatroom spécifique.

Ce ne sont pas les mêmes informations, même si les types de données sont identiques.
Il existe de nombreux avantages à une bonne conception.

Je n'ai pas encore lu le livre de Fowler susmentionné, mais j'ai lu d'autres choses de Folwer et il m'a été recommandé par des personnes en qui j'ai confiance, donc je me sens assez à l'aise pour être d'accord avec les autres.

Shrulik
la source
0

La nécessité d'accéder aux cartes ne justifie pas la méga-classe. Vous devez séparer la logique en plusieurs classes, et chaque classe doit avoir une méthode getMap pour que les autres classes puissent accéder aux cartes.

Tulains Córdova
la source
0

J'utiliserais la même réponse que celle que j'ai donnée ailleurs: prendre la classe monolithique et répartir ses responsabilités entre les autres classes. DCI et le modèle de visiteur offrent tous deux de bonnes options pour ce faire.

Mario T. Lanza
la source
-1

En termes de métrique logicielle, la grande classe est sac. Il existe des documents illimités prouvant cette déclaration. Pourquoi donc ? car les grandes classes sont plus difficiles à comprendre que les petites classes et il faut plus de temps pour les modifier. De plus, les grandes classes sont si difficiles lorsque vous faites des tests. Et les grandes classes sont très difficiles pour vous lorsque vous souhaitez les réutiliser, car elles contiennent très probablement des éléments indésirables.

cat_minhv0
la source