A idéia básica por trás da OOP é que dados e comportamento (com base nesses dados) são inseparáveis e são acoplados à idéia de um objeto de uma classe. O objeto possui dados e métodos que funcionam com esse (e outros dados). Obviamente, pelos princípios da OOP, objetos que são apenas dados (como estruturas C) são considerados um antipadrão.
Por enquanto, tudo bem.
O problema é que notei que meu código parece estar indo cada vez mais na direção desse antipadrão ultimamente. Parece-me que quanto mais eu tento obter informações ocultas entre classes e designs fracamente acoplados, mais minhas classes passam a ser uma mistura de dados puros sem classes de comportamento e todo comportamento sem classes de dados.
Geralmente, eu desenho classes de uma maneira que minimize a conscientização da existência de outras classes e minimize o conhecimento das interfaces de outras classes. Eu imponho isso especialmente de cima para baixo, as classes de nível inferior não conhecem as classes de nível superior. Por exemplo:
Suponha que você tenha uma API geral de jogo de cartas. Você tem uma aula Card
. Agora essa Card
classe precisa determinar a visibilidade para os jogadores.
Uma maneira é ter boolean isVisible(Player p)
na Card
aula.
Outra é ter boolean isVisible(Card c)
na Player
aula.
Não gosto da primeira abordagem em particular, pois concede conhecimento sobre Player
classe de nível superior a uma Card
classe de nível inferior .
Em vez disso, optei pela terceira opção, onde temos uma Viewport
classe que, dada ae Player
uma lista de cartões, determina quais cartões são visíveis.
No entanto, essa abordagem rouba as classes Card
e as Player
classes de uma possível função membro. Depois de fazer isso para outras coisas que a visibilidade de cartões, você é deixado com Card
e Player
classes que contêm dados puramente como toda a funcionalidade é implementada em outras classes, que são principalmente as classes sem dados, apenas métodos, como o Viewport
acima.
Isso é claramente contra a idéia principal do POO.
Qual é o caminho correto? Como devo executar a tarefa de minimizar as interdependências de classe e minimizar o conhecimento e o acoplamento assumidos, mas sem acabar com um design estranho, onde todas as classes de baixo nível contêm apenas dados e as classes de alto nível contêm todos os métodos? Alguém tem alguma terceira solução ou perspectiva sobre o design de classe que evite todo o problema?
PS Aqui está outro exemplo:
Suponha que você tenha uma classe DocumentId
imutável, que tenha apenas um único BigDecimal id
membro e um getter para esse membro. Agora você precisa ter um método em algum lugar, que DocumentId
retorne Document
esse ID a partir de um banco de dados.
Você:
- Adicione
Document getDocument(SqlSession)
método àDocumentId
classe, introduzindo repentinamente conhecimento sobre sua persistência ("we're using a database and this query is used to retrieve document by id"
), a API usada para acessar o DB e similares. Além disso, essa classe agora requer o arquivo JAR de persistência apenas para compilar. - Adicione outra classe com o método
Document getDocument(DocumentId id)
, deixando aDocumentId
classe como morta, sem comportamento, como uma estrutura.
fonte
Respostas:
O que você descreve é conhecido como modelo de domínio anêmico . Como acontece com muitos princípios de design de POO (como Lei de Demeter etc.), não vale a pena reverter apenas para satisfazer uma regra.
Não há nada de errado em ter sacos de valores, desde que não atrapalhem a paisagem inteira e não confiem em outros objetos para fazer as tarefas domésticas que poderiam estar fazendo por si mesmos .
Certamente seria um cheiro de código se você tivesse uma classe separada apenas para modificar propriedades de
Card
- se for razoavelmente esperado que ela cuide delas por conta própria.Mas é realmente um trabalho de
Card
saber para quePlayer
é visível?E por que implementar
Card.isVisibleTo(Player p)
, mas nãoPlayer.isVisibleTo(Card c)
? Ou vice-versa?Sim, você pode tentar criar algum tipo de regra para isso, como fez - como
Player
ter um nível mais alto do que umCard
(?) -, mas não é tão fácil de adivinhar e vou ter que procurar em mais de um lugar para encontrar o métodoCom o tempo, isso pode levar a um comprometimento podre do projeto de implementação tanto
isVisibleTo
na classe quanto na classe, o que acredito ser um não-não. Por quê então? Como eu já imagino o dia vergonhoso em que retornará um valor diferente do que penso - é subjetivo - isso deve ser tornado impossível pelo design .Card
Player
player1.isVisibleTo(card1)
card1.isVisibleTo(player1).
Visibilidade mútua dos cartões e os jogadores devem ser melhor governada por algum tipo de um objeto de contexto - seja ele
Viewport
,Deal
ouGame
.Não é igual a ter funções globais. Afinal, pode haver muitos jogos simultâneos. Observe que o mesmo cartão pode ser usado simultaneamente em muitas mesas. Vamos criar muitas
Card
instâncias para cada ás de espada?Eu ainda pode implementar
isVisibleTo
emCard
, mas passar um objeto de contexto para ele e fazerCard
delegado a consulta. Programa para fazer interface para evitar acoplamentos altos.Quanto ao seu segundo exemplo - se o ID do documento consiste apenas em a
BigDecimal
, por que criar uma classe de wrapper para ele?Eu diria que tudo que você precisa é de um
DocumentRepository.getDocument(BigDecimal documentID);
A propósito, enquanto ausente do Java, há
struct
s em C #.Vejo
http://msdn.microsoft.com/en-us/library/ah19swz4.aspx
http://msdn.microsoft.com/en-us/library/0taef578.aspx
para referência. É uma linguagem altamente orientada a objetos, mas ninguém faz muita diferença.
fonte
Interface iObj = (Interface)obj;
, o comportamento de AFAIKiObj
não é afetado pelostruct
ouclass
status deobj
(exceto que será uma cópia em caixa dessa atribuição se for astruct
).Você está cometendo o erro comum de assumir que as classes são um conceito fundamental no OOP. As aulas são apenas uma maneira particularmente popular de obter o encapsulamento. Mas podemos permitir que isso deslize.
BONS CÉUS NO. Quando você está jogando Bridge, você pergunta aos sete de copas quando é hora de mudar a mão do manequim de um segredo conhecido apenas pelo manequim para ser conhecido por todos? Claro que não. Isso não é uma preocupação do cartão.
Ambos são horríveis; não faça nenhum deles. Nem o jogador nem a carta são responsáveis por implementar as regras do Bridge!
Eu nunca joguei cartas com uma "viewport" antes, então não tenho idéia do que essa classe deve encapsular. Eu ter jogado cartas com um par de baralhos de cartas, alguns jogadores, uma mesa e uma cópia de Hoyle. Qual dessas coisas a Viewport representa?
Boa!
Não; a idéia básica do OOP é que os objetos encapsulem suas preocupações . No seu sistema, um cartão não está preocupado com muita coisa. Nem é um jogador. Isso ocorre porque você está modelando com precisão o mundo . No mundo real, as propriedades são cartas relevantes para um jogo e extremamente simples. Poderíamos substituir as figuras dos cartões pelos números de 1 a 52 sem alterar muito o jogo. Poderíamos substituir as quatro pessoas por manequins rotulados como Norte, Sul, Leste e Oeste sem mudar muito o jogo. Jogadores e cartas são as coisas mais simples do mundo dos jogos de cartas. As regras são o que é complicado, então a classe que representa as regras é onde a complicação deve estar.
Agora, se um de seus jogadores for uma IA, seu estado interno poderá ser extremamente complicado. Mas essa IA não determina se pode ver um cartão. As regras determinam isso .
Aqui está como eu projetaria seu sistema.
Primeiro, as cartas são surpreendentemente complicadas se houver jogos com mais de um baralho. Você deve considerar a pergunta: os jogadores podem distinguir entre duas cartas do mesmo valor? Se o jogador um toca um dos sete de copas, e então algumas coisas acontecem, e o jogador dois joga um dos sete de copas, o jogador três pode determinar que eram os mesmos sete copas? Considere isso com cuidado. Mas, além dessa preocupação, os cartões devem ser muito simples; são apenas dados.
Em seguida, qual é a natureza de um jogador? Um jogador consome uma sequência de ações visíveis e produz uma ação .
O objeto de regras é o que coordena tudo isso. As regras produzem uma sequência de ações visíveis e informam os jogadores:
E então pede ao jogador uma ação.
E assim por diante.
Separe seus mecanismos de suas políticas . As políticas do jogo devem ser encapsuladas em um objeto de política , não nas cartas . Os cartões são apenas um mecanismo.
fonte
Você está certo de que o acoplamento de dados e comportamento é a ideia central do POO, mas há mais do que isso. Por exemplo, encapsulamento : OOP / programação modular nos permite separar uma interface pública dos detalhes da implementação. No POO, isso significa que os dados nunca devem estar acessíveis ao público e devem ser usados apenas via acessadores. Por essa definição, um objeto sem nenhum método é realmente inútil.
Uma classe que não oferece métodos além dos acessadores é essencialmente uma estrutura complicada demais. Mas isso não é ruim, porque OOP oferece a flexibilidade de alterar detalhes internos, o que uma estrutura não. Por exemplo, em vez de armazenar um valor em um campo de membro, ele pode ser recalculado a cada vez. Ou um algoritmo de apoio é alterado e, com ele, o estado que deve ser mantido.
Embora a POO tenha algumas vantagens claras (especialmente a programação processual simples), é ingênuo buscar a OOP "pura". Alguns problemas não são bem mapeados para uma abordagem orientada a objetos e são resolvidos mais facilmente por outros paradigmas. Ao encontrar esse problema, não insista em uma abordagem inferior.
Considere calcular a sequência de Fibonacci de maneira orientada a objetos . Não consigo pensar em uma maneira sã de fazer isso; Uma programação estruturada simples oferece a melhor solução para esse problema.
Sua
isVisible
relação pertence a ambas as classes, ou a nenhuma, ou realmente: ao contexto . Registros comportamentais são típicos de uma abordagem de programação funcional ou procedural, que parece ser a mais adequada ao seu problema. Não há nada errado come não há nada errado em
Card
não ter métodos alémrank
esuit
acessadores.fonte
fibonacci
método em uma instância deinteger
. Gostaria de enfatizar seu argumento de que OO é sobre encapsulamento, mesmo em lugares aparentemente pequenos. Deixe o número inteiro descobrir como fazer o trabalho. Mais tarde, você pode melhorar a implementação, adicionar armazenamento em cache para melhorar o desempenho. Diferentemente das funções, os métodos seguem os dados para que todos os chamadores obtenham o benefício de uma implementação aprimorada. Talvez números inteiros de precisão arbitrários sejam adicionados posteriormente, eles podem ser tratados de forma transparente como números inteiros normais e podem ter seu própriofibonacci
método aprimorado de desempenho .Fibonacci
é uma subclasse da classe abstrataSequence
, a sequência é utilizada por qualquer conjunto de números e é responsável por armazenar sementes, estado, cache e um iterador.Essa é uma pergunta difícil porque se baseia em algumas premissas defeituosas:
Não vou falar muito sobre o item 1-3, porque cada um pode gerar sua própria resposta e isso convida a muitas discussões baseadas em opiniões. Mas acho que a idéia de "OOP é sobre o acoplamento de dados e comportamento" é especialmente preocupante. Não apenas leva ao número 4, mas também à idéia de que tudo deve ser um método.
Há uma diferença entre as operações que definem um tipo e as maneiras como você pode usá-lo. Ser capaz de recuperar o
i
elemento th é essencial para o conceito de uma matriz, mas a classificação é apenas uma das muitas coisas que posso escolher fazer com uma. A classificação não precisa ser um método, assim como "criar uma nova matriz contendo apenas os elementos pares".OOP é sobre o uso de objetos. Objetos são apenas uma maneira de obter abstração . A abstração é um meio de evitar acoplamentos desnecessários no seu código, não um fim por si só. Se sua noção de cartão é definida apenas pelo valor de seu conjunto e classificação, é bom implementá-lo como uma tupla ou registro simples. Não há detalhes não essenciais nos quais qualquer outra parte do código possa formar uma dependência. Às vezes você simplesmente não tem nada a esconder.
Você não faria
isVisible
um método desseCard
tipo porque ser visível provavelmente não é essencial para a sua noção de cartão (a menos que você tenha cartões muito especiais que possam se tornar translúcidos ou opacos ...). Deve ser um método doPlayer
tipo? Bem, isso provavelmente também não é uma qualidade definidora dos jogadores. Deveria fazer parte de algumViewport
tipo? Mais uma vez, isso depende do que você define como uma viewport e se a noção de verificar a visibilidade dos cartões é essencial para definir uma viewport.É muito possível,
isVisible
deve ser apenas uma função gratuita.fonte
Não, eles não são. Os objetos Plain-Old-Data são um padrão perfeitamente válido e eu esperaria que eles em qualquer programa que lida com dados que precisam ser persistidos ou comunicados entre áreas distintas do seu programa.
Embora sua camada de dados possa gerar uma
Player
classe completa quando lê daPlayers
tabela, ela pode ser apenas uma biblioteca de dados geral que retorna um POD com os campos da tabela, que passa para outra área do programa que converte um jogador POD para suaPlayer
classe de concreto .O uso de objetos de dados, digitados ou não, pode não fazer sentido no seu programa, mas isso não os torna um antipadrão. Se eles fazem sentido, use-os e, se não, não.
fonte
Plain-Old-Data objects are a perfectly valid pattern
Eu não disse que eles não estavam, estou dizendo que é errado quando eles preenchem toda a metade inferior do aplicativo.Pessoalmente, acho que o Design Orientado a Domínio ajuda a esclarecer esse problema. A pergunta que faço é: como descrevo o jogo de cartas para seres humanos? Em outras palavras, o que estou modelando? Se a coisa que estou modelando inclui genuinamente a palavra "viewport" e um conceito que corresponde ao seu comportamento, eu criaria o objeto de viewport e o faria logicamente.
No entanto, se eu não tiver o conceito de viewport no meu jogo, é algo que acho que preciso, porque, caso contrário, o código "parecerá errado". Penso duas vezes em adicioná-lo ao meu modelo de domínio.
A palavra modelo significa que você está construindo uma representação de algo. Eu aviso contra colocar em uma classe que represente algo abstrato além do que você está representando.
Editarei para acrescentar que é possível que você precise do conceito de uma Viewport em outra parte do seu código, se precisar interagir com uma exibição. Mas, em termos de DDD, isso seria uma preocupação de infraestrutura e existiria fora do modelo de domínio.
fonte
Normalmente não faço autopromoção, mas o fato é que escrevi muito sobre questões de design de OOP no meu blog . Para resumir várias páginas: você não deve iniciar o design com classes. A partir de interfaces ou APIs e código de forma a partir daí, há maiores chances de fornecer abstrações significativas, ajustar especificações e evitar inchar classes concretas com código não reutilizável.
Como isso se aplica a
Card
-Player
problema: Criar umaViewPort
abstração faz sentido se você pensarCard
ePlayer
como sendo duas bibliotecas independentes (o que implicariaPlayer
às vezes é usado semCard
). No entanto, eu estou inclinado a pensar que aPlayer
detémCards
e deve fornecer umCollection<Card> getVisibleCards ()
assessor para eles. Ambas as soluções (ViewPort
e as minhas) são melhores do que fornecerisVisible
como métodoCard
ouPlayer
, em termos de criação de relacionamentos de código compreensíveis.Uma solução de fora da classe é muito, muito melhor para o
DocumentId
. Há pouca motivação para fazer (basicamente um número inteiro) depender de uma complexa biblioteca de banco de dados.fonte
Não sei se a pergunta em questão está sendo respondida no nível certo. Eu pedi aos sábios do fórum que pensassem ativamente no cerne da questão aqui.
U Mad está trazendo à tona uma situação em que ele acredita que a programação de acordo com seu entendimento de OOP geralmente resultaria em muitos nós de folha sendo titulares de dados, enquanto sua API de nível superior é a maior parte do comportamento.
Eu acho que o tópico foi um pouco tangencial para se isVisible seria definido no Card vs Player; foi um mero exemplo ilustrado, embora ingênuo.
Eu tinha pressionado os experientes aqui a olhar para o problema em questão. Eu acho que há uma boa pergunta que U Mad fez. Entendo que você empurraria as regras e a lógica em questão para um objeto próprio; mas pelo que entendi a pergunta é
Minha visão:
Acho que você está fazendo uma pergunta de granularidade difícil de acertar na programação orientada a objetos. Na minha pouca experiência, eu não incluiria uma entidade no meu modelo que não inclua nenhum comportamento por si só. Se fosse necessário, provavelmente usaria uma construção que é projetada para armazenar essa abstração, diferentemente de uma classe que tem a idéia de encapsular dados e comportamento.
fonte
Point
withgetX()
. Você pode imaginar que está recebendo um de seus atributos, mas também pode lê-lo do disco ou da Internet. Obter e configurar é um comportamento, e ter aulas que fazem exatamente isso é ótimo. Bancos de dados somente obter e definir fwiw dadosUma fonte comum de confusão no POO decorre do fato de que muitos objetos encapsulam dois aspectos do estado: as coisas que eles sabem e o que sabem sobre eles. As discussões sobre o estado dos objetos freqüentemente ignoram o último aspecto, já que em estruturas em que as referências a objetos são promíscuas, não há maneira geral de determinar o que as coisas podem saber sobre qualquer objeto cuja referência já tenha sido exposta ao mundo exterior.
Eu sugeriria que provavelmente seria útil ter um
CardEntity
objeto que encapsulasse esses aspectos do cartão em componentes separados. Um componente se relacionaria às marcações na carta (por exemplo, "Diamond King" ou "Lava Blast; os jogadores têm chance de esquivar-se do AC-3 ou sofrer dano em 2D6"). Pode-se dizer respeito a um aspecto único do estado, como a posição (por exemplo, no convés, na mão de Joe ou na mesa em frente a Larry). Um terço pode estar relacionado a ele (talvez ninguém, talvez um jogador ou talvez muitos jogadores). Para garantir que tudo seja mantido sincronizado, os locais onde um cartão pode estar não seriam encapsulados como campos simples, mas comoCardSpace
objetos; para mover um cartão para um espaço, daria-lhe uma referência aoCardSpace
objeto; ele se retiraria do espaço antigo e se colocaria no novo espaço).Encapsular explicitamente "quem sabe sobre X" separadamente de "o que X sabe" deve ajudar a evitar muita confusão. Às vezes, é necessário cuidado para evitar vazamentos de memória, especialmente com muitas associações (por exemplo, se novos cartões surgirem e cartões antigos desaparecerem, é preciso garantir que os cartões que devem ser abandonados não sejam permanentemente presos a objetos de vida longa. ), mas se a existência de referências a um objeto formar uma parte relevante de seu estado, é inteiramente apropriado que o próprio objeto encapsule explicitamente essas informações (mesmo que delegue para alguma outra classe o trabalho de realmente gerenciá-las).
fonte
E como isso é ruim / desaconselhado?
Para usar uma analogia semelhante ao exemplo de suas placas, considere a
Car
, aDriver
e você precisa determinar se aDriver
unidade podeCar
.OK, então você decidiu que não quer que você
Car
saiba se aDriver
chave do carro está correta ou não e, por algum motivo desconhecido, você também decidiu que não quer que vocêDriver
saiba sobre aCar
classe (você não sabia exatamente o que fazer) isso também na sua pergunta original). Portanto, você tem uma classe intermediária, algo ao longo das linhas de umaUtils
classe, que contém o método com regras de negócios para retornar umboolean
valor para a pergunta acima.Eu acho que isso está bem. A classe intermediária pode precisar apenas verificar as chaves do carro agora, mas pode ser refatorada para considerar se o motorista possui uma carteira de motorista válida, sob a influência de álcool ou, em um futuro distópico, verifique a biometria do DNA. Por encapsulamento, realmente não há grandes problemas com essas três classes coexistindo juntas.
fonte