Existe algum motivo para usar as classes "dados antigos simples"?

43

No código legado, ocasionalmente vejo classes que não passam de invólucros para dados. algo como:

class Bottle {
   int height;
   int diameter;
   Cap capType;

   getters/setters, maybe a constructor
}

Meu entendimento do OO é que as classes são estruturas para dados e os métodos de operação nesses dados. Isso parece impedir objetos desse tipo. Para mim, eles nada mais são do que um structstipo de derrota no propósito da OO. Eu não acho que seja necessariamente mau, embora possa ser um cheiro de código.

Existe um caso em que esses objetos seriam necessários? Se isso for usado com frequência, torna o design suspeito?

Michael K
fonte
1
Isso não responde muito bem à sua pergunta, mas parece relevante, no entanto: stackoverflow.com/questions/36701/struct-like-objects-in-java
Adam Lear
2
Eu acho que o termo que você está procurando é POD (Plain Old Data).
Gaurav
9
Este é um exemplo típico de programação estruturada. Não necessariamente ruim, apenas não orientado a objetos.
Konrad Rudolph
1
isso não deveria estar no estouro de pilha?
Muad'Dib
7
@ Muad'Dib: tecnicamente, trata- se de programadores. Seu compilador não se importa se você usa estruturas simples de dados antigos. Sua CPU provavelmente gosta disso (da maneira "eu amo o cheiro de dados frescos do cache"). São as pessoas que se preocupam com "isso torna minha metodologia menos pura?" questões.
Shog9

Respostas:

67

Definitivamente não é mau e nem um cheiro de código em minha mente. Os contêineres de dados são um cidadão OO válido. Às vezes, você deseja encapsular informações relacionadas juntas. É muito melhor ter um método como

public void DoStuffWithBottle(Bottle b)
{
    // do something that doesn't modify Bottle, so the method doesn't belong
    // on that class
}

do que

public void DoStuffWithBottle(int bottleHeight, int bottleDiameter, Cap capType)
{
}

O uso de uma classe também permite adicionar um parâmetro adicional ao Bottle sem modificar todos os chamadores de DoStuffWithBottle. E você pode subclassificar Bottle e aumentar ainda mais a legibilidade e a organização do seu código, se necessário.

Também existem objetos de dados simples que podem ser retornados como resultado de uma consulta ao banco de dados, por exemplo. Eu acredito que o termo para eles nesse caso é "Objeto de transferência de dados".

Em alguns idiomas, há outras considerações também. Por exemplo, no C #, as classes e estruturas se comportam de maneira diferente, pois as estruturas são do tipo valor e as classes são do tipo referência.

Adam Lear
fonte
25
Nah. DoStuffWithdeve ser um método da classe em OOP (e deve provavelmente ser imutável, também). O que você escreveu acima não é um bom padrão em uma linguagem OO (a menos que você esteja fazendo interface com uma API herdada). Ele é , no entanto, um projeto válido em um ambiente não-OO. Bottle
Konrad Rudolph
11
@Javier: Então Java também não é a melhor resposta. A ênfase de Java no OO é esmagadora, a linguagem basicamente não serve para nada.
Konrad Rudolph
11
@ JohnL: Claro que eu estava simplificando. Mas, em geral, os objetos em OO encapsulam estado , não dados. Esta é uma distinção boa, mas importante. O objetivo do POO é precisamente não ter muitos dados disponíveis. Está enviando mensagens entre estados que criam um novo estado. Não consigo ver como você pode enviar mensagens para ou de um objeto sem método. (E esta é a definição original de OOP que eu não iria considerá-lo falho.)
Konrad Rudolph
13
@ Konrad Rudolph: Foi por isso que fiz explicitamente o comentário dentro do método. Concordo que os métodos que afetam as instâncias do Bottle devem ser incluídos nessa classe. Mas se outro objeto precisar modificar seu estado com base nas informações do Bottle, acho que meu design seria bastante válido.
Adam Lear
10
@ Konrad, eu discordo que doStuffWithBottle deve ir na classe de garrafa. Por que uma garrafa deve saber fazer coisas consigo mesma? doStuffWithBottle indica que outra coisa fará algo com uma garrafa. Se a garrafa continha isso, seria um acoplamento apertado. No entanto, se a classe Bottle tivesse um método isFull (), isso seria totalmente apropriado.
Nemi
25

As classes de dados são válidas em alguns casos. Os DTOs são um bom exemplo mencionado por Anna Lear. Em geral, porém, você deve considerá-los como a semente de uma classe cujos métodos ainda não surgiram. E se você estiver encontrando muitos deles no código antigo, trate-os como um forte cheiro de código. Eles são frequentemente usados ​​por programadores antigos de C / C ++ que nunca fizeram a transição para a programação OO e são um sinal de programação procedural. Confiar em levantadores e levantadores o tempo todo (ou pior ainda, no acesso direto de membros não privados) pode causar problemas antes que você perceba. Considere um exemplo de um método externo que precisa de informações Bottle.

Aqui Bottleestá uma classe de dados):

void selectShippingContainer(Bottle bottle) {
    if (bottle.getDiameter() > MAX_DIMENSION || bottle.getHeight() > MAX_DIMENSION ||
            bottle.getCapType() == Cap.FANCY_CAP ) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

Aqui nós demos Bottlealgum comportamento):

void selectShippingContainer(Bottle bottle) {
    if (bottle.isBiggerThan(MAX_DIMENSION) || bottle.isFragile()) {
        shippingContainer = WOODEN_CRATE;
    } else {
        shippingContainer = CARDBOARD_BOX;
    }
}

O primeiro método viola o princípio do Tell-Don't-Ask, e, mantendo- Bottlenos mudos, permitimos que o conhecimento implícito sobre garrafas, como o que a torna frágil, caia Capna lógica que está fora da Bottleclasse. Você precisa estar atento para evitar esse tipo de 'vazamento' quando habitualmente confia em getters.

O segundo método pede Bottleapenas o que ele precisa para fazer seu trabalho e Bottledecide se é frágil ou maior que um determinado tamanho. O resultado é um acoplamento muito mais flexível entre o método e a implementação do Bottle. Um efeito colateral agradável é que o método é mais limpo e mais expressivo.

Você raramente fará uso desses muitos campos de um objeto sem escrever alguma lógica que deva residir na classe com esses campos. Descubra o que é essa lógica e mova-a para onde ela pertence.

Mike E
fonte
1
Não posso acreditar que esta resposta não tem votos (bem, você tem uma agora). Este pode ser um exemplo simples, mas quando o OO é abusado o suficiente, você obtém classes de serviço que se transformam em pesadelos, contendo toneladas de funcionalidades que deveriam ter sido encapsuladas nas classes.
Alb
"por antigos programadores de C / C ++ que nunca fizeram a transição (sic) para OO"? Programadores C ++ normalmente são bastante OO, como é uma linguagem OO, ou seja, toda a ponto de usar C ++ em vez de C.
nappyfalcon
7

Se esse é o tipo de coisa que você precisa, tudo bem, mas por favor, por favor, faça como

public class Bottle {
    public int height;
    public int diameter;
    public Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }
}

em vez de algo como

public class Bottle {
    private int height;
    private int diameter;
    private Cap capType;

    public Bottle(int height, int diameter, Cap capType) {
        this.height = height;
        this.diameter = diameter;
        this.capType = capType;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public int getDiameter() {
        return diameter;
    }

    public void setDiameter(int diameter) {
        this.diameter = diameter;
    }

    public Cap getCapType() {
        return capType;
    }

    public void setCapType(Cap capType) {
        this.capType = capType;
    }
}

Por favor.

compman
fonte
14
O único problema com isso é que não há validação. Qualquer lógica sobre o que é um Bottle válido deve estar na classe Bottle. No entanto, usando a implementação proposta, posso ter uma garrafa com altura e diâmetro negativos - não há como impor regras de negócios ao objeto sem validar toda vez que o objeto for usado. Usando o segundo método, posso garantir que, se eu tenho um objeto Bottle, ele foi, é e sempre será um objeto Bottle válido, de acordo com meu contrato.
Thomas Owens
6
Essa é uma área em que o .NET possui uma ligeira vantagem sobre as propriedades, pois você pode adicionar um acessador de propriedades com lógica de validação, com a mesma sintaxe como se estivesse acessando um campo. Você também pode definir uma propriedade em que as classes possam obter o valor da propriedade, mas não defini-lo.
johnl
3
@ user9521 Se tiver certeza de que seu código não causará um erro fatal com valores "ruins", siga seu método. No entanto, se você precisar de mais validação, a capacidade de usar carregamento lento ou outras verificações quando os dados forem lidos ou gravados, use getters e setters explícitos. Pessoalmente, costumo manter minhas variáveis ​​privadas e usar getters e setters para obter consistência. Dessa forma, todas as minhas variáveis ​​são tratadas da mesma forma, independentemente da validação e / ou outras técnicas "avançadas".
Jonathan
2
A vantagem de usar o construtor é que torna muito mais fácil tornar a classe imutável. Isso é essencial se você deseja escrever código multiencadeado.
Fortyrunner
7
Eu tornaria os campos finais sempre que possível. IMHO Eu teria preferido que os campos fossem finais por padrão e tivessem uma palavra-chave para campos mutáveis. por exemplo, var
Peter Lawrey
6

Como @Anna disse, definitivamente não é mau. Claro que você pode colocar operações (métodos) em classes, mas somente se desejar . Você não precisa .

Permita-me uma pequena queixa sobre a idéia de que você deve colocar operações em classes, juntamente com a idéia de que classes são abstrações. Na prática, isso incentiva os programadores a

  1. Crie mais classes do que precisam (estrutura de dados redundante). Quando uma estrutura de dados contém mais componentes do que o minimamente necessário, ela não é normalizada e, portanto, contém estados inconsistentes. Em outras palavras, quando é alterado, precisa ser alterado em mais de um local para permanecer consistente. A falha em executar todas as alterações coordenadas a torna inconsistente e é um bug.

  2. Resolva o problema 1 inserindo métodos de notificação , para que, se a parte A for modificada, ele tente propagar as alterações necessárias nas partes B e C. Esse é o principal motivo pelo qual é recomendável ter métodos de acesso de obtenção e configuração. Como essa prática é recomendada, parece desculpar o problema 1, causando mais do problema 1 e mais da solução 2. Isso resulta não apenas em erros devido à implementação incompleta das notificações, mas também em um problema de redução de desempenho das notificações descontroladas. Estes não são cálculos infinitos, apenas muito longos.

Esses conceitos são ensinados como coisas boas, geralmente por professores que não tiveram que trabalhar com aplicativos monstruosos de milhões de linhas, repletos desses problemas.

Aqui está o que eu tento fazer:

  1. Mantenha os dados o mais normalizados possível, para que, quando uma alteração for feita, os dados sejam feitos no menor número possível de códigos, para minimizar a probabilidade de entrada em um estado inconsistente.

  2. Quando os dados tiverem de ser normalizados e a redundância for inevitável, não use notificações na tentativa de mantê-los consistentes. Em vez disso, tolere inconsistência temporária. Resolva a inconsistência com varreduras periódicas pelos dados por um processo que faz apenas isso. Isso centraliza a responsabilidade de manter a consistência, evitando os problemas de desempenho e correção aos quais as notificações estão sujeitas. Isso resulta em um código muito menor, sem erros e eficiente.

Mike Dunlavey
fonte
3

Esse tipo de classe é bastante útil quando você lida com aplicativos de tamanho médio / grande, por alguns motivos:

  • é muito fácil criar alguns casos de teste e garantir que os dados sejam consistentes.
  • ele possui todos os tipos de comportamentos que envolvem essas informações; portanto, o tempo de rastreamento de erros de dados é reduzido
  • Usá-los deve manter o método args leve.
  • Ao usar ORMs, essas classes fornecem flexibilidade e consistência. A adição de um atributo complexo calculado com base em informações simples já existentes na classe, resutls ao escrever um método simples. Isso é muito mais ágil e produtivo do que ter que verificar seu banco de dados e garantir que todos os bancos de dados sejam corrigidos com novas modificações.

Então, para resumir, na minha experiência, eles geralmente são mais úteis do que irritantes.

guiman
fonte
3

Com o design do jogo, a sobrecarga de milhares de chamadas de função e ouvintes de eventos às vezes podem fazer valer a pena ter classes que armazenam apenas dados e outras classes que percorrem todas as classes somente de dados para executar a lógica.

AttackingHobo
fonte
3

Concordo com a Anna Lear,

Definitivamente não é mau e nem um cheiro de código em minha mente. Os contêineres de dados são um cidadão OO válido. Às vezes, você deseja encapsular informações relacionadas juntas. É muito melhor ter um método como ...

Às vezes, as pessoas esquecem de ler as Convenções de codificação Java de 1999, que deixam bem claro que esse tipo de programação é perfeitamente adequado. De fato, se você evitá-lo, seu código terá um cheiro! (muitos getters / setters)

Das Convenções de Código Java 1999: Um exemplo de variáveis ​​de instância pública apropriadas é o caso em que a classe é essencialmente uma estrutura de dados, sem comportamento. Em outras palavras, se você usasse uma estrutura em vez de uma classe (se Java suportasse estrutura), é apropriado tornar públicas as variáveis ​​de instância da classe. http://www.oracle.com/technetwork/java/javase/documentation/codeconventions-137265.html#177

Quando usados ​​corretamente, os PODs (estruturas de dados antigas simples) são melhores que os POJOs, assim como os POJOs geralmente são melhores que os EJBs.
http://en.wikipedia.org/wiki/Plain_Old_Data_Structures

Richard Faber
fonte
3

As estruturas têm seu lugar, mesmo em Java. Você deve usá-los apenas se as duas coisas seguintes forem verdadeiras:

  • Você só precisa agregar dados que não têm nenhum comportamento, por exemplo, passar como parâmetro
  • Não importa nem um pouco que tipo de valores agregados os dados têm

Se for esse o caso, você deve tornar os campos públicos e pular os getters / setters. Getters e setters são desajeitados de qualquer maneira, e Java é tolo por não ter propriedades como uma linguagem útil. Como seu objeto de estrutura não deve ter nenhum método, os campos públicos fazem mais sentido.

No entanto, se um deles não se aplicar, você estará lidando com uma classe real. Isso significa que todos os campos devem ser privados. (Se você precisar absolutamente de um campo em um escopo mais acessível, use um getter / setter.)

Para verificar se sua suposta estrutura possui comportamento, observe quando os campos são usados. Se isso parece violar o tell, não pergunte , você precisa mudar esse comportamento para a sua classe.

Se alguns dos seus dados não mudarem, será necessário finalizar todos esses campos. Você pode considerar tornar sua classe imutável . Se você precisar validar seus dados, forneça a validação nos configuradores e construtores. (Um truque útil é definir um setter privado e modificar seu campo dentro da sua classe usando apenas esse setter.)

Seu exemplo de garrafa provavelmente falharia nos dois testes. Você poderia ter um código (artificial) parecido com este:

public double calculateVolumeAsCylinder(Bottle bottle) {
    return bottle.height * (bottle.diameter / 2.0) * Math.PI);
}

Em vez disso, deve ser

double volume = bottle.calculateVolumeAsCylinder();

Se você alterasse a altura e o diâmetro, seria a mesma garrafa? Provavelmente não. Aqueles devem ser finais. Um valor negativo está ok para o diâmetro? Sua garrafa deve ser mais alta do que larga? O limite pode ser nulo? Não? Como você está validando isso? Suponha que o cliente seja estúpido ou mau. ( É impossível dizer a diferença. ) Você precisa verificar esses valores.

É assim que sua nova classe Bottle pode ser:

public class Bottle {

    private final int height, diameter;

    private Cap capType;

    public Bottle(final int height, final int diameter, final Cap capType) {
        if (diameter < 1) throw new IllegalArgumentException("diameter must be positive");
        if (height < diameter) throw new IllegalArgumentException("bottle must be taller than its diameter");

        setCapType(capType);
        this.height = height;
        this.diameter = diameter;
    }

    public double getVolumeAsCylinder() {
        return height * (diameter / 2.0) * Math.PI;
    }

    public void setCapType(final Cap capType) {
        if (capType == null) throw new NullPointerException("capType cannot be null");
        this.capType = capType;
    }

    // potentially more methods...

}
Eva
fonte
0

IMHO muitas vezes não classes suficientes como esta em sistemas fortemente orientados a objetos. Eu preciso qualificar isso com cuidado.

Obviamente, se os campos de dados tiverem amplo escopo e visibilidade, isso poderá ser extremamente indesejável se houver centenas ou milhares de locais em sua base de código adulterando esses dados. Isso está pedindo problemas e dificuldades para manter os invariantes. No entanto, ao mesmo tempo, isso não significa que todas as classes de toda a base de código se beneficiam da ocultação de informações.

Mas há muitos casos em que esses campos de dados terão escopo muito restrito. Um exemplo muito direto é uma Nodeclasse privada de uma estrutura de dados. Muitas vezes, pode simplificar bastante o código, reduzindo o número de interações entre objetos, se o dito Nodepuder simplesmente consistir em dados brutos. Isso serve como um mecanismo de dissociação, já que a versão alternativa pode exigir acoplamento bidirecional de, digamos, Tree->Nodee Node->Treeem vez de simplesmente Tree->Node Data.

Um exemplo mais complexo seria o sistema de componentes de entidade, como costuma ser usado em mecanismos de jogos. Nesses casos, os componentes geralmente são apenas dados brutos e classes como a que você mostrou. No entanto, seu escopo / visibilidade tende a ser limitado, pois normalmente existem apenas um ou dois sistemas que podem acessar esse tipo específico de componente. Como resultado, você ainda acha bastante fácil manter invariantes nesses sistemas e, além disso, esses sistemas têm muito poucas object->objectinterações, facilitando a compreensão do que está acontecendo no nível do olho do pássaro.

Nesses casos, você pode ter algo mais parecido com isso no que diz respeito às interações (este diagrama indica interações, não acoplamentos, pois um diagrama de acoplamento pode incluir interfaces abstratas para a segunda imagem abaixo):

insira a descrição da imagem aqui

... em oposição a isso:

insira a descrição da imagem aqui

... e o primeiro tipo de sistema tende a ser muito mais fácil de manter e raciocinar em termos de correção, apesar do fato de que as dependências estão realmente fluindo em direção aos dados. Você obtém muito menos acoplamento principalmente porque muitas coisas podem ser transformadas em dados brutos, em vez de objetos interagindo entre si, formando um gráfico muito complexo de interações.


fonte
-1

Existem até muitas criaturas estranhas no mundo OOP. Uma vez eu criei uma classe, que é abstrata e não contém nada, apenas para ser um pai comum de outras duas classes ... é a classe Port:

SceneMember 
Message extends SceneMember
Port extends SceneMember
InputPort extends Port
OutputPort extends Port

Então SceneMember é a classe base, ele faz todo o trabalho necessário para aparecer em cena: adicionar, excluir, ID de configuração, etc. Mensagem é a conexão entre portas, tem vida própria. InputPort e OutputPort contêm suas próprias funções específicas de E / S.

A porta está vazia. É usado apenas para agrupar InputPort e OutputPort juntos para, por exemplo, uma lista de portas. Está vazio porque o SceneMember contém todos os itens para atuar como membro, e o InputPort e OutputPort contêm as tarefas especificadas na porta. InputPort e OutputPort são tão diferentes que não possuem funções comuns (exceto SceneMember). Então, Port está vazio. Mas é legal.

Talvez seja um antipadrão , mas eu gosto.

(É como a palavra "companheiro", usada para "esposa" e "marido". Você nunca usa a palavra "companheiro" para uma pessoa concreta, porque conhece o sexo dela e isso não importa. se ele é casado ou não, então você usa "alguém", mas ele ainda existe e é usado em raras situações abstratas, por exemplo, um texto jurídico.)

ern0
fonte
1
Por que suas portas precisam estender o SceneMember? por que não criar as classes de porta para operar no SceneMembers?
Michael K
1
Então, por que não usar o padrão de interface de marcador padrão? Basicamente idêntico à sua classe base abstrata vazia, mas é um idioma muito mais comum.
TMN
1
@ Michael: Apenas por razões teóricas, reservei o Port para uso futuro, mas esse futuro ainda não chegou e talvez nunca chegue. Eu não percebi que eles não têm nada em comum, ao contrário de seus nomes. Vou pagar uma compensação para todos que sofreu qualquer perda ocorreu por essa classe vazia ...
ern0
1
@TMN: Os tipos SceneMember (derivado) têm o método getMemberType (), existem vários casos em que a Scene é examinada em busca de subconjunto de objetos SceneMember.
ern0
4
Isso não responde à pergunta.
Eva