Como dividir classes grandes e fortemente acopladas?

14

Eu tenho algumas classes enormes de mais de 2k linhas de código (e em crescimento) que eu gostaria de refatorar, se possível, para ter um design mais leve e limpo.

A razão de ser tão grande é principalmente porque essas classes lidam com um conjunto de mapas que a maioria dos métodos precisa acessar, e os métodos estão muito conectados entre si.

Vou dar um exemplo muito concreto: tenho uma classe chamada Serverque lida com as mensagens recebidas. Ele tem métodos como joinChatroom, searchUsers, sendPrivateMessage, etc. Todos estes métodos manipular os mapas, como users, chatrooms, servers, ...

Talvez seria bom se eu pudesse ter uma classe lidando com mensagens relacionadas ao Chatrooms, outra tratando tudo sobre Usuários, etc. mas o principal problema aqui é que eu preciso usar todos os mapas na maioria dos métodos. É por isso que, por enquanto, eles estão todos presos na Serverclasse, pois todos confiam nesses mapas comuns e os métodos estão muito conectados um ao outro.

Eu precisaria criar uma classe Chatrooms, mas com uma referência a cada um dos outros objetos. Os usuários de uma classe novamente com uma referência a todos os outros objetos etc.

Sinto como se estivesse fazendo algo errado.

Mateus
fonte
Se você criar classes como User e Chatroom, essas classes precisariam apenas de uma referência à estrutura de dados comum ou se refeririam uma à outra?
Existem várias respostas satisfatórias aqui, você deve escolher uma.
precisa saber é o seguinte
@jeremyjjbrown a pergunta foi movida e eu a perdi. Escolheu uma resposta, thx.
Matthew

Respostas:

10

Pela sua descrição, acho que seus mapas são puramente sacos de dados, com toda a lógica dos Servermétodos. Ao inserir toda a lógica da sala de chat em uma classe separada, você ainda fica preso aos mapas que contêm dados.

Em vez disso, tente modelar salas de chat individuais, usuários etc. como objetos. Dessa forma, você passará apenas objetos específicos necessários para um determinado método, em vez de enormes mapas de dados.

Por exemplo:

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

Agora é fácil chamar alguns métodos específicos para lidar com mensagens:

Quer participar de uma sala de bate-papo?

chatroom.add(user);

Deseja enviar uma mensagem privada?

user.sendMessage(msg);

Deseja enviar uma mensagem pública?

chatroom.sendMessage(msg);
Casablanca
fonte
5

Você deve conseguir criar uma classe que armazene cada coleção. Embora Serverprecise de uma referência para cada uma dessas coleções, ela precisa apenas da quantidade mínima de lógica que não envolve o acesso ou a manutenção das coleções subjacentes. Isso tornará mais óbvio exatamente o que o servidor está fazendo e separará como ele o faz.

Peter Lawrey
fonte
4

Quando eu vi grandes turmas como essa, descobri que muitas vezes há uma turma (ou mais) tentando sair. Se você conhece um método que acha que pode não estar relacionado a essa classe, torne-o estático. O compilador lhe dirá outros métodos que esse método chama. Java insistirá que eles também são estáticos. Você os torna estáticos. Novamente, o compilador informará sobre qualquer método que ele chamar. Você continua fazendo isso repetidamente até não ter mais falhas de compilação. Então você tem vários métodos estáticos em sua grande classe. Agora você pode puxá-los para uma nova classe e tornar o método não estático. Você pode chamar essa nova classe da sua grande classe original (que agora deve conter menos linhas)

Você pode repetir o processo até ficar satisfeito com o design da classe.

O livro de Martin Fowler é realmente uma boa leitura, então eu recomendaria isso também, pois há momentos em que você não pode usar esse truque estático.

RNJ
fonte
1
Este livro de Martin Fowler martinfowler.com/books/refactoring.html
Arul
1

Como a maior parte do seu código existe, eu sugeriria o uso de classes auxiliares para mudar seus métodos. Isso ajudará a refatoração fácil. Portanto, sua classe de servidor ainda conterá os mapas nela. Mas faz uso de uma classe auxiliar, como ChatroomHelper, com métodos como join (salas de chat de mapas, usuário de String), listar getUsers (salas de chat de mapas), map getChatrooms (usuário de String).

A classe do servidor manterá uma instância do ChatroomHelper, UserHelper etc., movendo os métodos para suas classes auxiliares lógicas. Com isso, você pode deixar intactos os métodos públicos no servidor, para que qualquer chamador não precise ser alterado.

techuser soma
fonte
1

Para adicionar à resposta perspicaz de casablanca - se várias classes precisarem fazer as mesmas coisas básicas com algum tipo de entidade (adicionar usuários a uma coleção, manipular mensagens etc.), esses processos também deverão ser mantidos separados.

Existem várias maneiras de fazer isso - por herança ou composição. Para herança, você pode usar classes base abstratas com métodos concretos que fornecem os campos e a funcionalidade para manipular usuários ou mensagens, por exemplo, e ter entidades como chatroome user(ou qualquer outra) estender essas classes.

Por várias razões , é uma boa regra geral usar a composição sobre a herança. Você pode usar a composição para fazer isso de várias maneiras. Como lidar com usuários ou mensagens é uma função central da responsabilidade das classes, pode-se argumentar que a injeção de construtores é mais apropriada. Dessa forma, a dependência é transparente e um objeto não pode ser criado sem a funcionalidade necessária. Se é provável que a maneira como os usuários ou as mensagens sejam tratadas mude ou seja ampliada, considere usar algo como o padrão de estratégia .

Nos dois casos, certifique-se de codificar para interfaces, não classes concretas para flexibilidade.

Tudo isso dito, sempre considere o custo da complexidade adicionada ao usar esses padrões - se você não precisar, não codifique. Se você sabe que provavelmente não vai mudar a maneira como o tratamento de usuários / mensagens é feito, não precisa da complexidade estrutural de um padrão de estratégia - mas, para separar as preocupações e evitar a repetição, você ainda deve se divorciar da funcionalidade comum. a partir das instâncias concretas que o usam - e, se não existirem razões contrárias, componha os usuários dessa funcionalidade de manipulação (salas de chat, usuários) com um objeto que faça a manipulação.

Entao, para resumir:

  1. Como casablanca escreveu: Separe e encapsule salas de chat, usuários etc.
  2. Funcionalidade comum separada
  3. Considere separar a funcionalidade individual para separar a representação de dados (bem como o acesso e a mutação) da funcionalidade mais complexa sobre instâncias individuais de dados ou agregados dos mesmos (por exemplo, algo como searchUserspoderia entrar em uma classe de coleção ou em um mapa de repositório / identidade )
Michael Bauer
fonte
0

Olha, acho que sua pergunta é genérica demais para responder, já que não temos realmente uma descrição completa do problema, portanto, é impossível oferecer um bom design com tão pouco conhecimento. Como exemplo, posso abordar uma de suas preocupações sobre a possível futilidade de um melhor design.

Você diz que sua classe Server e sua futura classe Chatroom compartilham dados sobre usuários, mas esses dados devem ser diferentes. O servidor provavelmente tem um conjunto de usuários conectados a ele, enquanto um Chatroom, que pertence a um servidor, tem um conjunto diferente de usuários, um subconjunto do primeiro conjunto, apenas dos usuários atualmente conectados a um Chatroom específico.

Esta não é a mesma informação, mesmo que os tipos de dados sejam idênticos.
Existem muitas vantagens em um bom design.

Ainda não li o livro de Fowler acima mencionado, mas li outras coisas de Folwer e isso me foi recomendado por pessoas em quem confio, por isso me sinto confortável o suficiente para concordar com os outros.

Shrulik
fonte
0

A necessidade de acessar os mapas não justifica a megaclasse. É necessário separar a lógica em várias classes, e cada classe deve ter um método getMap para que outras classes possam acessar os mapas.

Tulains Córdova
fonte
0

Eu usaria a mesma resposta que forneci em outros lugares: pegue a classe monolítica e divida suas responsabilidades entre outras. O DCI e o Padrão do visitante oferecem boas opções para isso.

Mario T. Lanza
fonte
-1

Em termos de métrica de software, grande classe é saco. Existem documentos ilimitados comprovando essa afirmação. Por que é que ? porque classes grandes são mais difíceis de entender do que classes pequenas e é necessário mais tempo para serem modificadas. Além disso, grandes classes são tão difíceis quando você faz testes. E grandes classes são muito difíceis para você quando você deseja reutilizá-lo, porque é muito provável que contenha coisas indesejadas.

cat_minhv0
fonte