Código limpo e objetos híbridos e inveja de recursos

10

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 Dataclasse - especificamente no handleTrademétodo. handleTradeE 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?

durron597
fonte
2
Por que 'dados' tem que ser uma estrutura de dados? Tem um comportamento claro. Portanto, basta desligar todos os getters e setters, para que nenhum objeto possa manipular o estado interno.
Cormac Mulhall

Respostas:

9

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 addque 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 Datatem estado e que pode ser trazido para um estado inválido:

  • Após lidar com uma negociação, o último agressor pode não ser atualizado.
  • O último agressor pode ser atualizado mesmo quando não ocorreram novas trocas.
  • A duração da execução pode manter seu valor antigo, mesmo quando o agressor foi atualizado.
  • etc.

Se algum estado for válido para seus dados, tudo estará bem com seu código e você poderá continuar. Caso contrário: A Dataclasse é 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 da Dataclasse. Se alterar o agressor envolve definir o comprimento da execução como zero, esse comportamento deve fazer parte da Dataclasse. Datanunca 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 Datafor 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 a Dataconsistê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.

amon
fonte
5

O fato de que handleTrade()e updateRun()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:

MyObject x = ...;
x.actionOne();
x.actionTwo();
String result = x.actionThree();

Novo Código:

MyObject x = ...;
OneResult r1 = x.actionOne();
TwoResult r2 = r1.actionTwo();
String result = r2.actionThree();

Isso tem várias vantagens:

  • Move preocupações separadas para objetos separados ( SRP ).
  • Ele remove o acoplamento temporal: é impossível chamar métodos fora de ordem, e as assinaturas de métodos fornecem documentação implícita sobre como chamá-los. Você já olhou para a documentação, viu o objeto que deseja e trabalhou para trás? Quero o objeto Z. Mas preciso de um Y para obter um Z. Para obter um Y, preciso de um X. Aha! Eu tenho um W, que é necessário para obter um X. Encadeie tudo, e seu W agora pode ser usado para obter um Z.
  • Dividir objetos como esse tem mais probabilidade de torná-los imutáveis, o que tem inúmeras vantagens além do escopo desta questão. A explicação rápida é que objetos imutáveis ​​tendem a levar a códigos mais seguros.

fonte
Não há acoplamento temporal entre essas duas chamadas de método. Troque a ordem deles e o comportamento não muda.
durron597
1
Inicialmente, também pensei em acoplamento seqüencial / temporal ao ler a pergunta, mas depois notei que o updateRunmé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 se updateRundeve estar no visitante ou na classe de dados, e não vejo como essa resposta soluciona esse problema.
amon
A visibilidade de updateRuné irrelevante, o que é importante é a implementação, this.dataque não está presente na pergunta e é o objeto que está sendo manipulado pelo objeto visitante.
De qualquer forma, o fato de esse visitante estar simplesmente chamando um grupo de setters e não processando nada é uma razão para o acoplamento temporal não estar presente. Provavelmente, não importa para que ordem os setters estão sendo chamados.
0

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.

k3b
fonte