¿Cómo dividir clases grandes y estrechamente acopladas?

14

Tengo algunas clases enormes de más de 2k líneas de código (y en crecimiento) que me gustaría refactorizar si es posible, para tener un diseño más claro y limpio.

La razón por la que es tan grande se debe principalmente a que estas clases manejan un conjunto de mapas a los que la mayoría de los métodos necesitan acceder, y los métodos están muy conectados entre sí.

Daré un ejemplo muy concreto: tengo una clase llamada Serverque maneja los mensajes entrantes. Tiene métodos como joinChatroom, searchUsers, sendPrivateMessage, etc. Todos estos métodos manipular los mapas tales como users, chatrooms, servers, ...

Tal vez sería bueno si pudiera tener una clase que maneje mensajes relacionados con salas de chat, otra que maneje todo sobre usuarios, etc. pero el problema principal aquí es que necesito usar todos los mapas en la mayoría de los métodos. Es por eso que por ahora todos están atrapados en la Serverclase, ya que todos confían en estos mapas comunes y los métodos están muy conectados entre sí.

Necesitaría crear una sala de chat de clase, pero con una referencia a cada uno de los otros objetos. Una clase de usuarios nuevamente con una referencia a todos los demás objetos, etc.

Siento que estaría haciendo algo mal.

Mateo
fuente
Si hiciera clases como User y Chatroom, ¿estas clases solo necesitarían una referencia a la estructura de datos común o se referirían entre sí?
Aquí hay varias respuestas satisfactorias, debe elegir una.
jeremyjjbrown
@jeremyjjbrown la pregunta se ha movido y la perdí. Elegí una respuesta, gracias.
Mateo

Respuestas:

10

Por su descripción, supongo que sus mapas son puramente bolsas de datos, con toda la lógica en los Servermétodos. Al insertar toda la lógica de la sala de chat en una clase separada, todavía está atascado con mapas que contienen datos.

En su lugar, intente modelar salas de chat individuales, usuarios, etc. como objetos. De esa manera, solo pasará objetos específicos necesarios para un determinado método, en lugar de grandes mapas de datos.

Por ejemplo:

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

Ahora es fácil llamar a algunos métodos específicos para manejar mensajes:

¿Quieres unirte a una sala de chat?

chatroom.add(user);

¿Quieres enviar un mensaje privado?

user.sendMessage(msg);

¿Quieres enviar un mensaje público?

chatroom.sendMessage(msg);
casablanca
fuente
5

Debería poder crear una clase que contenga cada colección. Si bien Servernecesitará una referencia a cada una de estas colecciones, solo necesita la cantidad mínima de lógica que no implicará acceder o mantener las colecciones subyacentes. Esto hará más obvio exactamente lo que está haciendo el Servidor y separará cómo lo hace.

Peter Lawrey
fuente
4

Cuando he visto grandes clases como esta, he descubierto que a menudo hay una clase (o más) tratando de salir. Si conoce un método que cree que podría no estar relacionado con esta clase, hágalo estático. El compilador le informará de otros métodos que este método llama. Java insistirá en que también son estáticos. Los haces estáticos. Nuevamente, el compilador le informará de cualquier método que llame. Sigue haciendo esto una y otra vez hasta que no tenga más fallas de compilación. Entonces tienes una carga de métodos estáticos en tu gran clase. Ahora puede sacarlos a una nueva clase y hacer que el método no sea estático. Luego puede llamar a esta nueva clase desde su clase grande original (que ahora debería contener menos líneas)

Luego puede repetir el proceso hasta que esté satisfecho con el diseño de la clase.

El libro de Martin Fowler es una muy buena lectura, así que también lo recomendaría, ya que hay veces que no puedes usar este truco estático.

RNJ
fuente
1
Este libro de Martin Fowler martinfowler.com/books/refactoring.html
Arul
1

Dado que la mayor parte de su código existe, sugeriría usar clases auxiliares para mover sus métodos. Eso ayudará a refactorizar fácilmente. Por lo tanto, su clase de servidor seguirá conteniendo los mapas. Pero hace uso de una clase auxiliar, por ejemplo, ChatroomHelper con métodos como join (Map chatrooms, String user), List getUsers (Map chatrooms), Map getChatrooms (String user).

La clase de servidor contendrá una instancia de ChatroomHelper, UserHelper, etc., moviendo así los métodos a sus clases auxiliares lógicas. Con esto, puede dejar los métodos públicos en el servidor intactos, por lo que cualquier persona que llama no necesita cambiar.

techuser soma
fuente
1

Para agregar a la perspicaz respuesta de casablanca : si varias clases necesitan hacer las mismas cosas básicas con algún tipo de entidad (agregar usuarios a una colección, manejar mensajes, etc.), esos procesos también deben mantenerse separados.

Hay varias formas de hacer esto: por herencia o composición. Para la herencia, puede usar clases base abstractas con métodos concretos que proporcionan los campos y la funcionalidad para manejar usuarios o mensajes, por ejemplo, y tienen entidades como chatroomy user(o cualquier otra) extienden esas clases.

Por varias razones , es una buena regla general usar la composición sobre la herencia. Puede usar la composición para hacer esto de varias maneras. Dado que el manejo de usuarios o mensajes son funciones centrales para la responsabilidad de las clases, se puede argumentar que la inyección de constructor es la más apropiada. De esta manera, la dependencia es transparente y no se puede crear un objeto sin tener la funcionalidad requerida. Si la forma en que se manejan los usuarios o los mensajes es probable que cambie o se extienda, debe considerar usar algo como el patrón de estrategia .

En ambos casos, asegúrese de codificar hacia interfaces, no clases concretas para mayor flexibilidad.

Dicho todo esto, siempre tenga en cuenta el costo de la complejidad adicional al usar dichos patrones; si no lo va a necesitar, no lo codifique. Si sabe que probablemente no va a cambiar la forma en que se realiza el manejo de los usuarios / mensajes, no necesita la complejidad estructural de un patrón de estrategia, pero para separar las preocupaciones y evitar la repetición, aún debe divorciarse de la funcionalidad común a partir de las instancias concretas que lo usan, y, si no existen razones imperiosas para lo contrario, componga a los usuarios de dicha funcionalidad de manejo (salas de chat, usuarios) con un objeto que haga el manejo.

Así que para resumir:

  1. Como escribió Casablanca: separe y encapsule salas de chat, usuarios, etc.
  2. Funcionalidad común separada
  3. Considere separar la funcionalidad individual para divorciar la representación de datos (así como el acceso y la mutación) de una funcionalidad más compleja sobre instancias individuales de datos o agregados de los mismos (por ejemplo, algo como searchUserspodría ir en una clase de colección, o un repositorio / mapa de identidad )
Michael Bauer
fuente
0

Mire, creo que su pregunta es demasiado genérica para responder ya que realmente no tenemos una descripción completa del problema, por lo que es imposible ofrecer un buen diseño con tan poco conocimiento. Puedo, solo como ejemplo, abordar una de sus preocupaciones sobre la posible inutilidad de un mejor diseño.

Usted dice que su clase de Servidor y su futura clase de Chatroom comparten datos sobre los usuarios, pero estos datos deberían ser diferentes. El servidor probablemente tiene un conjunto de usuarios conectados a él, mientras que una sala de chat, que pertenece a un servidor, tiene un conjunto diferente de usuarios, un subconjunto del primer conjunto, de solo los usuarios actualmente conectados a una sala de chat específica.

Esta no es la misma información, incluso si los tipos de datos son idénticos.
Hay muchas ventajas para un buen diseño.

Todavía no he leído el libro de Fowler antes mencionado, pero he leído otras cosas de Folwer y me lo recomendaron personas en las que confío, así que me siento lo suficientemente cómodo como para estar de acuerdo con los demás.

Shrulik
fuente
0

La necesidad de acceder a los mapas no justifica la megaclase. Debe separar la lógica en varias clases, y cada clase debe tener un método getMap para que otras clases puedan acceder a los mapas.

Tulains Córdova
fuente
0

Usaría la misma respuesta que proporcioné en otra parte: tome la clase monolítica y divida sus responsabilidades entre otras clases. Tanto DCI como el patrón de visitante ofrecen buenas opciones para hacerlo.

Mario T. Lanza
fuente
-1

En términos de métrica de software, la gran clase es bolsa. Hay documentos ilimitados que prueban esta declaración. Porqué es eso ? porque las clases grandes son más difíciles de entender que las clases pequeñas y se necesita más tiempo para modificarlas. Además, las clases grandes son muy difíciles cuando haces las pruebas. Y las clases grandes son muy difíciles para ti cuando quieres reutilizarlas porque es muy probable que contengan cosas no deseadas.

cat_minhv0
fuente