Existe alguma razão para usar classes “plain old data”?

38

No código legado, ocasionalmente vejo classes que não são nada além de wrappers para dados. algo como:

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

   getters/setters, maybe a constructor
}

Meu entendimento de OO é que classes são estruturas para dados e os métodos de operação nesses dados. Isto parece impedir objetos deste tipo. Para mim eles não são nada mais do que structs e meio que derrotam o propósito da OO. Eu não acho que seja necessariamente malvado, embora possa ser um cheiro de código.

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

    
por Michael K 27.12.2010 / 15:35
fonte

10 respostas

65

Definitivamente não é mal e nem um cheiro de código em minha mente. Contêineres de dados são um cidadão OO válido. Às vezes você quer 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 cada chamador de DoStuffWithBottle. E você pode subclassificar o Bottle e aumentar a legibilidade e a organização do seu código, se necessário.

Há também 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, existem outras considerações também. Por exemplo, em classes C # e estruturas se comportam de maneira diferente, já que structs são um tipo de valor e classes são tipos de referência.

    
por 27.12.2010 / 15:49
fonte
25

As classes de dados são válidas em alguns casos. DTO's são um bom exemplo mencionado por Anna Lear. Em geral, você deve considerá-los como a semente de uma classe cujos métodos ainda não brotaram. E se você estiver usando muitos códigos antigos, trate-os como um strong cheiro de código. Eles são freqüentemente usados por antigos programadores C / C ++ que nunca fizeram a transisão para programação OO e são um sinal de programação procedural. Confiar em getters e setters 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 de Bottle .

Aqui Bottle é 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 apresentamos Bottle algum 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 Tell-Don't-Ask e, mantendo Bottle dumb, deixamos o conhecimento implícito sobre as garrafas, como o que faz um fragmento (o Cap ), entrar na lógica que é fora da classe Bottle . Você precisa ficar alerta para evitar esse tipo de "vazamento" quando você habitualmente depende de getters.

O segundo método solicita apenas Bottle para o que precisa fazer seu trabalho e deixa Bottle decidir 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 deveria residir na classe com esses campos. Descubra o que é essa lógica e mova-a para onde ela pertence.

    
por 28.02.2012 / 18:09
fonte
7

Se este é 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.

    
por 27.12.2010 / 19:29
fonte
6

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

.

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

  1. Crie mais classes do que elas precisam (estrutura de dados redundante). Quando uma estrutura de dados contém mais componentes do que o mínimo necessário, ela não é normalizada e, portanto, contém estados inconsistentes. Em outras palavras, quando é alterado, ele precisa ser alterado em mais de um lugar para permanecer consistente. O não cumprimento de todas as alterações coordenadas torna-o inconsistente e é um erro.

  2. Resolva o problema 1 colocando métodos notification , para que, se a parte A for modificada, tente propagar as alterações necessárias nas partes B e C. Essa é a principal razão pela qual é recomendado ter métodos de acesso get-and-set. Como essa é uma prática recomendada, parece desculpar o problema 1, causando mais problemas 1 e mais da solução 2. Isso resulta não apenas em bugs devido à implementação incompleta das notificações, mas a um problema de desempenho de 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 em aplicativos monstruosos de milhões de linhas crivados com esses problemas.

Veja o que tento fazer:

  1. Mantenha os dados o mais normalizados possível, de modo que, quando uma alteração for feita nos dados, seja feita com o menor número possível de pontos de código, para minimizar a probabilidade de entrar em um estado inconsistente.

  2. Quando os dados precisam ser desnormalizados e a redundância é inevitável, não use as notificações na tentativa de mantê-las consistentes. Em vez disso, tolerar inconsistência temporária. Resolver inconsistência com varreduras periódicas através dos 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 que as notificações são propensas. Isso resulta em código muito menor, livre de erros e eficiente.

por 27.12.2010 / 17:28
fonte
3

Esse tipo de classe é bastante útil quando você está lidando com aplicativos de médio e grande porte, por alguns motivos:

  • é muito fácil criar algum teste casos e garantir que os dados são consistente.
  • contém todos os tipos de comportamentos envolver essa informação, para que o tempo de rastreamento de bugs de dados seja reduzido
  • O uso deles deve manter os argumentos de método leves.
  • Ao usar ORMs, essas classes fornecem flexibilidade e consistência. Adicionando um atributo complexo que é calculado com base em informações simples já na classe, resutls por escrito 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.

    
por 27.12.2010 / 16:12
fonte
3

Com o design do jogo, a sobrecarga de chamadas de função em 1000, e os ouvintes de eventos podem às vezes valer a pena ter classes que armazenam dados apenas, e têm outras classes que percorrem todas as classes de dados para executar a lógica. / p>     

por 27.12.2010 / 21:15
fonte
3

Concorde com a Anna Lear,

Definitely not evil and not a code smell in my mind. Data containers are a valid OO citizen. Sometimes you want to encapsulate related information together. It's a lot better to have a method like...

Às vezes, as pessoas esquecem de ler as Convenções de codificação Java de 1999, que deixe bem claro que este tipo de programação está perfeitamente bem. Na verdade, se você evitá-lo, seu código vai cheirar! (muitos getters / setters)

Das convenções de código Java de 1999: Um exemplo de variáveis de instância públicas apropriadas é o caso em que a classe é essencialmente uma estrutura de dados, sem comportamento. Em outras palavras, se você tivesse usado uma struct em vez de uma classe (se Java suportasse struct), seria apropriado tornar as variáveis de instância da classe públicas. link

Quando usados corretamente, os PODs (estruturas de dados antigas simples) são melhores que os POJOs assim como os POJOs costumam ser melhores que os EJBs.
link

    
por 28.02.2012 / 14:33
fonte
3

As estruturas têm o seu lugar, mesmo em Java. Você só deve usá-los se as duas coisas a seguir forem verdadeiras:

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

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

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

Para verificar se sua suposta estrutura tem comportamento, observe quando os campos são usados. Se parece violar dizer, não pergunte , então você precisa mover esse comportamento para sua classe.

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

Seu exemplo do Bottle provavelmente falharia nos dois testes. Você poderia ter um código (artificial) que se parece com isso:

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 ok para o diâmetro? Sua garrafa deve ser mais alta do que larga? O Cap 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.

Esta é a aparência da sua nova classe Bottle:

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...

}
    
por 28.01.2013 / 16:23
fonte
0

IMHO muitas vezes não existem classes suficientes como esta em sistemas strongmente orientados a objetos. Eu preciso qualificar cuidadosamente isso.

É claro que, se os campos de dados tiverem amplo escopo e visibilidade, isso pode ser extremamente indesejável se houver centenas ou milhares de lugares em sua base de código adulterando tais dados. Isso é pedir problemas e dificuldades para manter invariantes. No entanto, ao mesmo tempo, isso não significa que todas as classes da base de código inteira se beneficiam da ocultação de informações.

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

Um exemplo mais complexo seria o de sistemas de componentes de entidade usados com frequência em mecanismos de jogos. Nesses casos, os componentes geralmente são apenas dados brutos e classes como o que você mostrou. No entanto, seu escopo / visibilidade tende a ser limitado, pois geralmente há apenas um ou dois sistemas que podem acessar esse tipo específico de componente. Como resultado, você ainda tende a achar muito fácil manter invariantes nesses sistemas e, além disso, tais sistemas têm muito poucas interações object->object , tornando muito fácil compreender o que está acontecendo no nível dos olhos da ave.

Nesses casos, você pode acabar com algo mais parecido com as interações (esse diagrama indica interações, não acoplamento, já que um diagrama de acoplamento pode incluir interfaces abstratas para a segunda imagem abaixo):

...emoposiçãoaisso:

... 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 para os dados. Você obtém muito menos acoplamento principalmente porque muitas coisas podem ser transformadas em dados brutos em vez de objetos que interagem entre si formando um gráfico muito complexo de interações.

    
por 16.12.2017 / 10:31
fonte
-1

Existem até criaturas muito estranhas no mundo OOP. Certa vez, 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, faz todo o trabalho, que é necessário para aparecer na cena: adicionar, excluir, ID get-set, etc. A mensagem é a conexão entre as portas, tem sua própria vida. InputPort e OutputPort contêm as funções específicas de E / S próprias.

A porta está vazia. É usada apenas para agrupar InputPort e OutputPort para, digamos, uma lista de portas. Está vazio porque SceneMember contém todas as coisas que são para agir como um membro e InputPort e OutputPort contêm as tarefas especificadas pela porta. InputPort e OutputPort são tão diferentes que não possuem funções comuns (exceto SceneMember). Então, o Porto está vazio. Mas é legal.

Talvez seja um antipattern , mas eu gosto disso.

(É como a palavra "mate", que é usada tanto para "esposa" quanto para "marido". Você nunca usa a palavra "companheira" para uma pessoa concreta, porque você sabe o sexo dela e ela Não importa se ele é casado ou não, então você usa "alguém" em vez disso, mas ele ainda existe e é usado em situações abstratas raras, por exemplo, um texto legal.

    
por 27.12.2010 / 16:06
fonte