Come dividere le classi grandi e strettamente accoppiate?


14

Ho alcune enormi classi di oltre 2k righe di codice (e in crescita) che vorrei refactoring se possibile, per avere un design più leggero e pulito.

Il motivo per cui è così grande è principalmente perché queste classi gestiscono un insieme di mappe a cui la maggior parte dei metodi deve accedere e i metodi sono molto collegati tra loro.

Faccio un esempio molto concreto: ho una classe chiamata Serverche gestisce i messaggi in arrivo. Ha metodi come joinChatroom, searchUsers, sendPrivateMessage, ecc Tutti questi metodi di manipolare le mappe, come users, chatrooms, servers, ...

Forse sarebbe bello se potessi avere una classe che gestisse i messaggi riguardanti le Chatroom, un'altra che trattasse tutto degli Utenti, ecc. Ma il problema principale qui è che ho bisogno di usare tutte le mappe nella maggior parte dei metodi. Ecco perché per ora sono tutti attaccati alla Serverclasse in quanto si basano tutti su queste mappe comuni e i metodi sono molto collegati tra loro.

Avrei bisogno di creare una classe Chatroom, ma con riferimento a ciascuno degli altri oggetti. Utenti di una classe di nuovo con un riferimento a tutti gli altri oggetti, ecc.

Sento che avrei fatto qualcosa di sbagliato.


Se dovessi creare classi come Utente e Chatroom, queste classi necessiterebbero solo di un riferimento alla struttura dati comune o si farebbero riferimento a vicenda?

Ci sono diverse risposte soddisfacenti qui, dovresti sceglierne una.
jeremyjjbrown,

@jeremyjjbrown la domanda è stata spostata e l'ho persa. Ho scelto una risposta, grazie.
Matteo,

Risposte:


10

Dalla tua descrizione, immagino che le tue mappe siano puramente sacche di dati, con tutta la logica nei Servermetodi. Spingendo tutta la logica della chatroom in una classe separata, sei ancora bloccato con le mappe contenenti dati.

Invece, prova a modellare singole chat, utenti ecc. Come oggetti. In questo modo, passerai in giro solo oggetti specifici richiesti per un determinato metodo, anziché enormi mappe di dati.

Per esempio:

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

Ora è facile chiamare alcuni metodi specifici per gestire i messaggi:

Vuoi entrare in una chatroom?

chatroom.add(user);

Vuoi inviare un messaggio privato?

user.sendMessage(msg);

Vuoi inviare un messaggio pubblico?

chatroom.sendMessage(msg);

5

Dovresti essere in grado di creare una classe che contiene ciascuna raccolta. Sebbene Servernecessiti di un riferimento a ciascuna di queste raccolte, necessita solo della minima quantità di logica che non implica l'accesso o la manutenzione delle raccolte sottostanti. Ciò renderà più ovvio esattamente cosa sta facendo il server e separerà come lo fa.


4

Quando ho visto grandi classi come questa ho scoperto che spesso c'è una classe (o più) lì dentro che cerca di uscire. Se conosci un metodo che ritieni possa non essere correlato a questa classe, rendilo statico. Il compilatore ti dirà quindi di altri metodi che questo methd chiama. Java insisterà che siano anche statici. Li rendi statici. Ancora una volta il compilatore ti dirà di ogni metodo che chiama. Continui a farlo ripetutamente fino a quando non avrai più errori di compilazione. Quindi hai un sacco di metodi statici nella tua grande classe. Ora puoi estrarli in una nuova classe e rendere il metodo non statico. Puoi quindi chiamare questa nuova classe dalla tua grande classe originale (che ora dovrebbe contenere meno righe)

È quindi possibile ripetere il processo fino a quando non si è soddisfatti del design della classe.

Il libro di Martin Fowler è davvero una buona lettura, quindi lo consiglio anche io perché ci sono volte in cui non puoi usare questo trucco statico.


1
Questo libro di Martin Fowler martinfowler.com/books/refactoring.html
Arul

1

Poiché la maggior parte del tuo codice esiste, suggerirei di utilizzare le classi di supporto per spostare i tuoi metodi. Ciò contribuirà a un facile refactoring. Quindi, la tua classe server conterrà comunque le mappe in essa contenute. Ma si avvale di una classe di supporto che dice ChatroomHelper con metodi come join (Mappa chat room, Utente stringa), Elenco getUsers (Mappa chat room), Mappa getChatrooms (Utente stringa).

La classe Server conterrà un'istanza di ChatroomHelper, UserHelper ecc. Spostando così i metodi nelle sue classi helper logiche. In questo modo è possibile lasciare intatti i metodi pubblici nel server, quindi non è necessario modificare alcun chiamante.


1

Per aggiungere alla perspicace risposta di Casablanca - se più classi devono fare le stesse cose di base con un certo tipo di entità (aggiungere utenti a una raccolta, gestire messaggi ecc.), Anche questi processi dovrebbero essere tenuti separati.

Esistono diversi modi per farlo: per eredità o composizione. Per ereditarietà, è possibile utilizzare classi di base astratte con metodi concreti che forniscono campi e funzionalità per gestire utenti o messaggi, ad esempio, e avere entità come chatroome user(o qualsiasi altra) estendere tali classi.

Per vari motivi , è una buona regola generale usare la composizione sull'eredità. Puoi usare la composizione per farlo in vari modi. Poiché la gestione di utenti o messaggi sono funzioni fondamentali per la responsabilità delle classi, si può sostenere che l'iniezione del costruttore sia la più appropriata. In questo modo, la dipendenza è trasparente e un oggetto non può essere creato senza avere la funzionalità richiesta. Se è probabile che il modo in cui vengono gestiti gli utenti o i messaggi cambi o venga esteso, dovresti prendere in considerazione l'utilizzo di qualcosa come il modello di strategia .

In entrambi i casi, assicurati di codificare verso interfacce, non per classi concrete per la flessibilità.

Detto questo, considera sempre il costo di una maggiore complessità quando usi tali schemi - se non ne avrai bisogno, non codificarlo. Se sai che molto probabilmente non cambierai il modo in cui viene gestita la gestione di utenti / messaggi, non hai bisogno della complessità strutturale di un modello di strategia, ma per separare le preoccupazioni ed evitare la ripetizione, dovresti comunque divorziare da funzionalità comuni dai casi concreti che lo utilizzano - e, se non esistono ragioni imperative contrarie, comporre gli utenti di tale funzionalità di gestione (chat, utenti) con un oggetto che esegue la gestione.

Quindi, per riassumere:

  1. Come scrisse Casablanca: chat room separate, incapsulate, utenti ecc.
  2. Separare le funzionalità comuni
  3. Prendi in considerazione la possibilità di separare le singole funzionalità per separare la rappresentazione dei dati (così come l'accesso e la mutazione) da funzionalità più complesse su singole istanze di dati o aggregati di essi (ad esempio, qualcosa di simile searchUserspotrebbe andare in una classe di raccolta o in un repository / mappa di identità )

0

Senti, penso che la tua domanda sia troppo generica per rispondere dato che non abbiamo una descrizione completa del problema, quindi offrire un buon design con così poca conoscenza è impossibile. Posso, solo per fare un esempio, rispondere a una delle tue preoccupazioni sulla possibile futilità di un design migliore.

Dici che la tua classe Server e la tua futura classe Chatroom condividono dati sugli utenti, ma questi dati dovrebbero essere diversi. Il server ha probabilmente un set di utenti connessi ad esso, mentre un Chatroom, che a sua volta appartiene a un Server, ha un set diverso di utenti, un sottoinsieme del primo set, solo degli utenti attualmente connessi a una Chatroom specifica.

Non si tratta delle stesse informazioni, anche se i tipi di dati sono identici.
Ci sono molti vantaggi per un buon design.

Non ho ancora letto il suddetto libro di Fowler, ma ho letto altre cose di Folwer e mi è stato consigliato da persone di cui mi fido, quindi mi sento abbastanza a mio agio da concordare con gli altri.


0

La necessità di accedere alle mappe non giustifica la classe mega. Devi separare la logica in più classi e ogni classe deve avere un metodo getMap in modo che altre classi possano accedere alle mappe.


0

Vorrei usare la stessa risposta che ho fornito altrove: prendere la classe monolitica e dividere le sue responsabilità tra le altre classi. Sia DCI che il Visitor Pattern offrono buone opzioni per farlo.


-1

In termini di metrica del software, la grande classe è la borsa. Ci sono documenti illimitati che dimostrano questa affermazione. Perché ? perché le classi grandi sono più difficili da comprendere rispetto alle classi piccole ed è necessario più tempo per modificarle. Inoltre, le grandi classi sono così difficili quando si eseguono i test. E le grandi classi sono molto difficili per te quando vuoi riutilizzarle perché molto probabilmente contengono cose indesiderate.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.