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 structs
tipo 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?
java
code-quality
object-oriented
code-smell
Michael K
fonte
fonte
Respostas:
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
do que
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.
fonte
DoStuffWith
deve 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
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
Bottle
está uma classe de dados):Aqui nós demos
Bottle
algum comportamento):O primeiro método viola o princípio do Tell-Don't-Ask, e, mantendo-
Bottle
nos mudos, permitimos que o conhecimento implícito sobre garrafas, como o que a torna frágil, caiaCap
na lógica que está fora daBottle
classe. Você precisa estar atento para evitar esse tipo de 'vazamento' quando habitualmente confia em getters.O segundo método pede
Bottle
apenas o que ele precisa para fazer seu trabalho eBottle
decide 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.
fonte
Se esse é o tipo de coisa que você precisa, tudo bem, mas por favor, por favor, faça como
em vez de algo como
Por favor.
fonte
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
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.
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:
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.
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.
fonte
Esse tipo de classe é bastante útil quando você lida com aplicativos de tamanho médio / grande, por alguns motivos:
Então, para resumir, na minha experiência, eles geralmente são mais úteis do que irritantes.
fonte
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.
fonte
Concordo com a Anna Lear,
À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
fonte
As estruturas têm seu lugar, mesmo em Java. Você deve usá-los apenas se as duas coisas seguintes forem verdadeiras:
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:
Em vez disso, deve ser
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:
fonte
IMHO muitas vezes não há 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
Node
classe 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 ditoNode
puder 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->Node
eNode->Tree
em vez de simplesmenteTree->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->object
interaçõ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):
... em oposição a isso:
... 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
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:
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.)
fonte