Então, recentemente fiz algumas refatorações importantes no meu código. Uma das principais coisas que tentei fazer foi dividir minhas classes em objetos de dados e objetos de trabalho. Isso foi inspirado, entre outras coisas, por esta seção do Código Limpo :
Híbridos
Essa confusão às vezes leva a estruturas de dados híbridas infelizes que são meio objeto e meia estrutura de dados. Eles têm funções que fazem coisas significativas e também têm variáveis públicas ou acessadores e mutadores públicos que, para todos os efeitos, tornam públicas as variáveis privadas, tentando outras funções externas a usar essas variáveis da maneira que um programa procedural usaria. estrutura de dados.
Tais híbridos dificultam a adição de novas funções, mas também dificultam a adição de novas estruturas de dados. Eles são os piores dos dois mundos. Evite criá-los. Eles são indicativos de um design confuso cujos autores não têm certeza - ou pior, ignoram - se precisam de proteção contra funções ou tipos.
Recentemente, eu estava olhando o código para um dos meus objetos de trabalho (que implementa o Padrão de Visitante ) e vi isso:
@Override
public void visit(MarketTrade trade) {
this.data.handleTrade(trade);
updateRun(trade);
}
private void updateRun(MarketTrade newTrade) {
if(this.data.getLastAggressor() != newTrade.getAggressor()) {
this.data.setRunLength(0);
this.data.setLastAggressor(newTrade.getAggressor());
}
this.data.setRunLength(this.data.getRunLength() + newTrade.getLots());
}
Eu disse imediatamente a mim mesmo "característica invejosa! Essa lógica deve estar na Data
classe - especificamente no handleTrade
método. handleTrade
E sempreupdateRun
deve acontecer juntos". Mas então pensei: "a classe de dados é apenas uma estrutura de dados; se eu começar a fazer isso, será um objeto híbrido!"public
O que é melhor e por quê? Como você decide o que fazer?
Respostas:
O texto que você citou tem bons conselhos, embora eu substitua "estruturas de dados" por "registros", assumindo que algo parecido com estruturas seja pretendido. Os registros são apenas agregações estúpidas de dados. Embora possam ser mutáveis (e, portanto, com estado em uma mentalidade de programação funcional), eles não têm nenhum estado interno, nenhum invariável que precise ser protegido. É completamente válido adicionar operações a um registro que facilite seu uso.
Por exemplo, podemos argumentar que um vetor 3D é um registro idiota. No entanto, isso não deve impedir a adição de um método como o
add
que facilita a adição de vetores. A adição de comportamento não transforma um registro (não tão burro) em um híbrido.Essa linha é cruzada quando a interface pública de um objeto nos permite quebrar o encapsulamento: Existem algumas partes internas às quais podemos acessar diretamente, trazendo o objeto para um estado inválido. Parece-me que
Data
tem estado e que pode ser trazido para um estado inválido:Se algum estado for válido para seus dados, tudo estará bem com seu código e você poderá continuar. Caso contrário: A
Data
classe é responsável por sua própria consistência de dados. Se lidar com uma negociação sempre envolve atualizar o agressor, esse comportamento deve fazer parte daData
classe. Se alterar o agressor envolve definir o comprimento da execução como zero, esse comportamento deve fazer parte daData
classe.Data
nunca foi um disco idiota. Você já o transformou em híbrido adicionando setters públicos.Há um cenário em que você pode considerar relaxar essas responsabilidades estritas: se
Data
for privado do seu projeto e, portanto, não fizer parte de nenhuma interface pública, você ainda poderá garantir o uso adequado da classe. No entanto, isso coloca a responsabilidade de manter aData
consistência em todo o código, em vez de coletá-los em um local central.Recentemente, escrevi uma resposta sobre o encapsulamento , que detalha o que é o encapsulamento e como você pode garantir isso.
fonte
O fato de que
handleTrade()
eupdateRun()
ocorrem sempre em conjunto (e o segundo método é realmente sobre o visitante e chama vários outros métodos no objeto de dados) tem cheiro de acoplamento temporais . Isso significa que você deve chamar métodos em uma ordem específica, e eu acho que chamar métodos fora de ordem quebrará algo na pior das hipóteses ou falhará em fornecer um resultado significativo na melhor das hipóteses. Não é bom.Normalmente, a maneira correta de refatorar essa dependência é que cada método retorne um resultado que pode ser alimentado no próximo método ou usado diretamente.
Código antigo:
Novo Código:
Isso tem várias vantagens:
fonte
updateRun
método era privado . Evitar o acoplamento seqüencial é um bom conselho, mas se aplica apenas ao design da API / interfaces públicas, e não aos detalhes da implementação. A verdadeira questão parece ser seupdateRun
deve estar no visitante ou na classe de dados, e não vejo como essa resposta soluciona esse problema.updateRun
é irrelevante, o que é importante é a implementação,this.data
que não está presente na pergunta e é o objeto que está sendo manipulado pelo objeto visitante.Do meu ponto de vista, uma classe deve conter "valores para estado (variáveis-membro) e implementações de comportamento (funções-membro, métodos)".
As "infelizes estruturas de dados híbridas" surgem se você tornar públicas as variáveis-membro do estado da classe (ou seus getters / setters) que não devem ser públicas.
Portanto, não vejo necessidade de ter classes separadas para objetos de dados de dados e objetos de trabalho.
Você deve manter variáveis de membro de estado não públicas (sua camada de banco de dados deve poder lidar com variáveis de membro não públicas)
Uma inveja de recursos é uma classe que usa métodos de outra classe excessivamente. Veja Code_smell . Ter uma classe com métodos e estado eliminaria isso.
fonte