Zero objetos de comportamento em OOP - meu dilema de design

90

A idéia básica por trás da POO é que os dados e o comportamento (com base nesses dados) são inseparáveis e são acoplados pela ideia de um objeto de uma classe. Objeto tem dados e métodos que trabalham com isso (e outros dados). Obviamente, pelos princípios da POO, objetos que são apenas dados (como estruturas C) são considerados um antipadrão.

Até aí tudo bem.

O problema é que eu notei que o meu código parece estar indo mais e mais na direção desse anti-padrão ultimamente. Parece-me que, quanto mais eu tento obter informações escondidas entre classes e desenhos fracamente acoplados, mais minhas aulas chegam a ser uma mistura de dados puros sem classes de comportamento e todo o comportamento sem classes de dados.

Eu geralmente projeto classes de uma forma que minimiza sua consciência da existência de outras classes e minimiza seu conhecimento de interfaces de outras classes. Eu especialmente aplico isso de uma forma top-down, classes de baixo nível não sabem sobre classes de nível mais alto. Por exemplo:

Suponha que você tenha uma API geral de jogos de cartas. Você tem uma classe Card . Agora, essa classe Card precisa determinar a visibilidade para os jogadores.

Uma maneira é ter boolean isVisible(Player p) na classe Card .

Outra é ter boolean isVisible(Card c) na classe Player .

Eu não gosto da primeira abordagem em particular, pois ela concede conhecimento sobre a classe Player de nível mais alto para uma classe Card de nível mais baixo.

Em vez disso, optei pela terceira opção, onde temos uma classe Viewport , que, dada uma Player e uma lista de cartões, determina quais cards são visíveis.

No entanto, essa abordagem rouba as classes Card e Player de uma possível função-membro. Depois de fazer isso para outras coisas além da visibilidade de cartões, você fica com as classes Card e Player que contêm dados puros, pois todas as funcionalidades são implementadas em outras classes, que são principalmente classes sem dados, apenas métodos, como Viewport acima.

Isto é claramente contra a ideia principal da OOP.

Qual é o caminho correto? Como devo fazer a tarefa de minimizar as interdependências de classe e minimizar o conhecimento e acoplamento assumidos, mas sem acabar com o design estranho, onde todas as classes de baixo nível contêm somente dados e as classes de alto nível contêm todos os métodos? Alguém tem alguma terceira solução ou perspectiva sobre design de classe que evite todo o problema?

P.S. Aqui está outro exemplo:

Suponha que você tenha a classe DocumentId , que é imutável, tenha apenas um único membro BigDecimal id e um getter para esse membro. Agora você precisa ter um método em algum lugar, que recebe um DocumentId retorna Document para esse id de um banco de dados.

Você:

  • Adicione o método Document getDocument(SqlSession) à classe DocumentId , introduzindo de repente o 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 banco de dados e afins. 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 a classe DocumentId como morta, sem comportamento, com classe struct.
por U Mad 02.04.2014 / 14:23
fonte

10 respostas

42

O que você descreve é conhecido como modelo de domínio anêmico . Tal como acontece com muitos princípios de design OOP (como a Lei de Demeter, etc), não vale a pena dobrar para trás apenas para satisfazer uma regra.

Nada de errado em ter sacos de valores, contanto que eles não atravancem toda a paisagem e não dependam de outros objetos para fazer as tarefas domésticas que eles poderiam estar fazendo por si mesmos . p>

Certamente seria um cheiro de código se você tivesse uma classe separada apenas para modificar propriedades de Card - se fosse razoavelmente esperado que ele cuidasse delas por conta própria.

Mas é realmente um trabalho de Card saber a que Player é visível?

E por que implementar Card.isVisibleTo(Player p) , mas não Player.isVisibleTo(Card c) ? Ou vice-versa?

Sim, você pode tentar criar algum tipo de regra para isso - como Player sendo mais alto que um Card (?) - mas não é tão fácil adivinhar e eu Teremos que procurar em mais de um lugar para encontrar o método.

Com o tempo, isso pode levar a um comprometimento de projeto podre na implementação de isVisibleTo em ambos Card e Player class, o que acredito ser um não-não. Por quê? Porque eu já imaginei o dia vergonhoso quando player1.isVisibleTo(card1) retornará um valor diferente de card1.isVisibleTo(player1). eu acho - é subjetivo - isso deve ser impossível por design .

A visibilidade mútua de cartões e jogadores deve ser melhor governada por algum tipo de objeto de contexto - seja Viewport , Deal ou Game .

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 várias tabelas. Devemos criar muitas Card instâncias para cada ás de espada?

Eu ainda posso implementar isVisibleTo em Card , mas passar um objeto de contexto para ele e tornar Card delegar a consulta. Programa para interface para evitar alto acoplamento.

Quanto ao segundo exemplo - se o ID do documento consistir apenas em BigDecimal , por que criar uma classe de wrapper?

Eu diria que tudo que você precisa é de DocumentRepository.getDocument(BigDecimal documentID);

A propósito, enquanto ausente do Java, existem struct s em C #.

Veja

para referência. É uma linguagem altamente orientada a objetos, mas ninguém faz grande diferença com isso.

    
por 02.04.2014 / 15:52
fonte
145

The basic idea behind OOP is that data and behavior (upon that data) are inseparable and they are coupled by the idea of an object of a class.

Você está cometendo o erro comum de assumir que as classes são um conceito fundamental no OOP. As classes são apenas uma forma particularmente popular de conseguir o encapsulamento. Mas podemos permitir que isso ocorra.

Suppose you have a general card game API. You have a class Card. Now this Card class needs to determine visibility to players.

BONS CENOS NO. Quando você está jogando Bridge, você pergunta aos sete corações quando é hora de trocar a mão do manequim de um segredo conhecido apenas por manequim para ser conhecido por todos? Claro que não. Isso não é uma preocupação do cartão.

One way is to have boolean isVisible(Player p) on Card class. Another is to have boolean isVisible(Card c) on Player class.

Ambos são horríveis; não faça nenhum desses. Nem o jogador nem a carta são responsáveis por implementar as regras do Bridge!

Instead I opted for the third option where we have a Viewport class which, given a Player and a list of cards determines which cards are visible.

Eu nunca joguei cartas com uma "viewport" antes, então não tenho ideia do que essa classe deve encapsular. Eu tenho jogado cartas com alguns baralhos de cartas, alguns jogadores, uma mesa e uma cópia de Hoyle. Qual dessas coisas a Viewport representa?

However this approach robs both Card and Player classes of a possible member function.

Bom!

Once you do this for other stuff than visibility of cards, you are left with Card and Player classes which contain purely data as all functionality is implemented in other classes, which are mostly classes with no data, just methods, like the Viewport above. This is clearly against the principal idea of OOP.

Não; A idéia básica da OOP é que os objetos encapsulam suas preocupações . No seu sistema, um cartão não se preocupa muito. Nenhum é um jogador. Isso porque você está modelando com precisão o mundo . No mundo real, as propriedades são cartas que são relevantes para um jogo são extremamente simples. Poderíamos substituir as imagens nos cartões com os números de 1 a 52 sem mudar muito o jogo. Poderíamos substituir as quatro pessoas com manequins marcados Norte, Sul, Leste e Oeste sem mudar muito o jogo. Jogadores e cartas são as coisas mais simples no mundo dos jogos de cartas. As regras são complicadas, então a classe que representa as regras é onde a complicação deve ser.

Agora, se um de seus jogadores é um AI, seu estado interno pode ser extremamente complicado. Mas essa IA não determina se pode ver uma carta. As regras determinam isso .

Veja como eu projetaria seu sistema.

Primeiramente, os cards são surpreendentemente complicados se houver jogos com mais de um deck. Você tem que considerar a questão: os jogadores podem distinguir entre duas cartas do mesmo valor? Se o jogador um joga um dos sete de copas, e então algumas coisas acontecem, e então o jogador dois joga um dos sete de copas, o jogador três pode determinar que foram os mesmos sete de copas? Considere isso com cuidado. Mas, além dessa preocupação, os cartões devem ser muito simples; eles 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:

  • O jogador um, o dez de copas foi entregue a você pelo jogador três.
  • O segundo jogador, um cartão foi entregue ao jogador um por jogador três.

E depois pede ao jogador uma ação.

  • Jogador um, o que você quer fazer?
  • O jogador um diz: triplique o fromp.
  • O primeiro jogador, que é uma ação ilegal porque um triplo de p produz um gambito indefensável.
  • Jogador um, o que você quer fazer?
  • O jogador 1 diz: descarte a dama de espadas.
  • O segundo jogador, o jogador um descartou a dama de espadas.

E assim por diante.

Separe seus mecanismos das suas políticas . As políticas do jogo devem ser encapsuladas em um objeto de política , não nas cartas . As cartas são apenas um mecanismo.

    
por 02.04.2014 / 19:40
fonte
28

Você está correto em dizer que o acoplamento de dados e comportamento é a ideia central da OOP, mas há mais do que isso. Por exemplo, encapsulamento : programação OOP / modular nos permite separar uma interface pública de detalhes de implementação. Em OOP, isso significa que os dados nunca devem ser acessíveis publicamente e devem ser usados apenas por meio de acessadores. Por esta definição, um objeto sem qualquer método é realmente inútil.

Uma classe que não oferece nenhum método além dos acessadores é essencialmente uma estrutura supercomplicada. Mas isso não é ruim, porque a POO oferece a flexibilidade de alterar detalhes internos, o que uma estrutura não oferece. Por exemplo, em vez de armazenar um valor em um campo de membro, ele poderia ser recalculado a cada vez. Ou um algoritmo de backing é alterado e, com ele, o estado que deve ser monitorado.

Embora o OOP tenha algumas vantagens claras (especialmente programação processual simples), é ingênuo lutar pela OOP “pura”. Alguns problemas não mapeiam bem uma abordagem orientada a objetos e são resolvidos mais facilmente por outros paradigmas. Quando se deparar com tal problema, não insista em uma abordagem inferior.

  • Considere calcular a sequência de Fibonacci de uma maneira orientada a objeto . Não consigo pensar em uma maneira sensata de fazer isso; programação estruturada simples oferece a melhor solução para este problema.

  • Sua relação isVisible pertence a ambas as classes, ou a nenhuma delas, ou, na verdade: ao contexto . Os registros comportamentais são típicos de uma abordagem de programação funcional ou procedural, que parece ser a melhor opção para o seu problema. Não há nada de errado com

    static boolean isVisible(Card c, Player p);
    

    e não há nada de errado com Card sem métodos além de rank e suit accessors.

por 02.04.2014 / 14:59
fonte
19

The basic idea behind OOP is that data and behavior (upon that data) are inseparable and they are coupled by the idea of an object of a class. Object have data and methods that work with that (and other data). Obviously by the principles of OOP, objects that are just data (like C structs) are considered an anti-pattern. (...) This is clearly against the principal idea of OOP.

Esta é uma pergunta difícil porque é baseada em algumas premissas com defeito:

  1. A ideia de que a OOP é a única maneira válida de escrever código.
  2. A ideia de que o OOP é um conceito bem definido. Tornou-se uma palavra de ordem que é difícil encontrar duas pessoas que possam concordar sobre o que é a OOP.
  3. A ideia de que a OOP envolve o agrupamento de dados e comportamento.
  4. A ideia de que tudo é / deveria ser uma abstração.

Eu não vou abordar muito # 1-3, porque cada um poderia gerar sua própria resposta, e isso convida a muita discussão baseada em opiniões. Mas acho que a ideia de "OOP é sobre acoplamento de dados e comportamento" é especialmente preocupante. Isso não só leva ao 4º, mas também leva à ideia de que tudo deve ser um método.

Há uma diferença entre as operações que definem um tipo e as maneiras de usar esse tipo. Ser capaz de recuperar o elemento i th é essencial para o conceito de uma matriz, mas a classificação é apenas uma das muitas coisas que posso escolher fazer com uma. Classificação não precisa ser um método mais do que "criar uma nova matriz contendo apenas os elementos pares" precisa ser.

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 em seu código, não um fim em si mesmo. Se a sua noção de uma carta é definida apenas pelo valor de sua suíte e classificação, é bom implementá-la como simples tupla ou registro. Não há detalhes não essenciais que qualquer outra parte do código possa formar uma dependência. Às vezes você simplesmente não tem nada a esconder.

Você não tornaria isVisible um método do tipo Card , porque a visibilidade provavelmente não é essencial para a sua noção de cartão (a menos que você tenha cartões muito especiais que podem se tornar translúcidos ou opacos ...). Deve ser um método do tipo Player ? Bem, isso provavelmente não é uma qualidade de definição dos jogadores. Deve ser parte de algum tipo Viewport ? Mais uma vez, isso depende do que você define como uma porta de visualização e se a noção de verificar a visibilidade das placas é essencial para definir uma janela de visualização.

É muito possível que isVisible seja apenas uma função gratuita.

    
por 02.04.2014 / 15:56
fonte
9

Obviously by the principles of OOP, objects that are just data (like C structs) are considered an anti-pattern.

Não, eles não são. Objetos Plain-Old-Data são um padrão perfeitamente válido, e eu os esperaria em qualquer programa que lide com dados que precisem ser persistidos ou comunicados entre áreas distintas de seu programa.

Enquanto sua camada de dados pode executar uma% da classe Player completa quando ela for ler da tabela Players , ela poderia ser uma biblioteca de dados geral que retorna um POD com a campos da tabela, que passa para outra área do programa que converte um jogador POD para sua classe Player concreta.

O uso de objetos de dados, digitados ou não, pode não fazer sentido em seu programa, mas isso não os torna um antipadrão. Se fizerem sentido, use-os e, se não fizerem, não use.

    
por 02.04.2014 / 14:37
fonte
8

Pessoalmente, acho que o Design Dirigido por Domínio ajuda a esclarecer esse problema. A pergunta que faço é: como descrevo o jogo de cartas para os seres humanos? Em outras palavras, o que estou modelando? Se a coisa que estou modelando realmente inclui a palavra "viewport" e um conceito que corresponde ao seu comportamento, então eu criaria o objeto viewport e faria o que ele deveria logicamente.

No entanto, se eu não tenho o conceito de viewport no meu jogo, e é algo que eu acho que preciso, porque senão o código "parece errado". Eu penso duas vezes em adicionar meu modelo de domínio.

A palavra modelo significa que você está construindo uma representação de algo. Preciso evitar colocar em uma classe que represente algo abstrato além do que você está representando.

Vou editar para adicionar que é possível que você precise do conceito de um Viewport em outra parte do seu código, se precisar interagir com um display. Mas, em termos DDD, isso seria uma preocupação de infraestrutura e existiria fora do modelo de domínio.

    
por 02.04.2014 / 18:46
fonte
5

Eu não costumo fazer auto-promoção, mas o fato é que eu escrevi muito sobre os problemas de design OOP no meu blog . Para resumir várias páginas: você não deve começar 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 as especificações e evitar inchaço de classes concretas com código não reutilizável.

Como isso se aplica a Card - Player problema: criar uma abstração ViewPort faz sentido se você considera Card e Player duas bibliotecas independentes (o que implicaria que Player às vezes é usado sem Card ). No entanto, estou inclinado a pensar que Player contém Cards e deve fornecer um Collection<Card> getVisibleCards () de acesso a eles. Ambas as soluções ( ViewPort e mine) são melhores do que fornecer isVisible como um método de Card ou Player , em termos de criação de relacionamentos de código compreensíveis.

Uma solução fora da classe é muito, muito melhor para o DocumentId . Há pouca motivação para fazer (basicamente, um inteiro) depende de uma biblioteca de banco de dados complexa.

    
por 02.04.2014 / 21:07
fonte
3

Não sei se a pergunta em questão está sendo respondida no nível certo. Eu havia pedido aos sábios do fórum que pensassem ativamente no núcleo da questão aqui.

U Mad está trazendo uma situação em que acredita que programar de acordo com seu entendimento de OOP geralmente resultaria em muitos nós de folha sendo detentores de dados, enquanto sua API de nível superior é composta da maior parte do comportamento.

Acho que o tópico foi um pouco tangencial para saber se isVisible seria definido no Card vs Player; foi um mero exemplo ilustrado, embora ingênuo.

Eu tinha empurrar o experiente aqui para olhar o problema em mãos. Eu acho que há uma boa pergunta que U Mad tem pressionado. Eu entendo que você empurraria as regras e a lógica em questão para um objeto próprio; mas como eu entendo a pergunta é

  1. Não há problema em ter construções de titulares de dados simples (classes / structs; não me importo com o que elas são modeladas para essa pergunta), que realmente não oferecem muita funcionalidade?
  2. Se sim, qual é a melhor ou a melhor maneira de modelá-los?
  3. Se não, como incorporamos essas partes do contador de dados em classes de API mais altas (incluindo o comportamento)

Minha opinião:

Acho que você está fazendo uma pergunta de granularidade que é difícil de acertar na programação orientada a objetos. Em minha pequena experiência, eu não incluiria uma entidade em meu modelo que não incluísse nenhum comportamento por si só. Se eu tiver que, provavelmente usei uma construção uma estrutura que é projetada para manter essa abstração diferente de uma classe que tem a idéia de encapsular dados e comportamento.

    
por 03.04.2014 / 06:35
fonte
2

Uma fonte comum de confusão na OOP decorre do fato de que muitos objetos encapsulam dois aspectos do estado: as coisas que eles conhecem e as coisas que sabem sobre eles. Discussões sobre o estado dos objetos freqüentemente ignoram o último aspecto, já que em frameworks onde as referências a objetos são promíscuas, não existe uma maneira geral de determinar o que as coisas podem saber sobre qualquer objeto cuja referência tenha sido exposta ao mundo externo.

Eu sugeriria que provavelmente seria útil ter um objeto CardEntity que encapsula os aspectos do cartão em componentes separados. Um componente se relacionaria com as marcações no cartão (por exemplo, "Diamond King" ou "Lava Blast; os jogadores têm AC-3 chance de se esquivar, ou então recebem 2D6 de dano"). Pode-se relacionar com um aspecto único do estado, como a posição (por exemplo, no convés, na mão de Joe ou na mesa na frente de Larry). Um terceiro pode estar relacionado com a possibilidade de vê-lo (talvez ninguém, talvez um jogador, ou talvez muitos jogadores). Para garantir que tudo seja mantido em sincronia, os locais onde um cartão pode estar não seriam encapsulados como campos simples, mas CardSpace objects; para mover um cartão para um espaço, ele forneceria uma referência ao objeto CardSpace adequado; então 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 novas placas surgirem e cartões antigos desaparecerem, é necessário garantir que as cartas que devem ser abandonadas não sejam perpetuamente anexadas a objetos de vida longa ) mas, se a existência de referências a um objeto formará uma parte relevante de seu estado, é inteiramente adequado que o próprio objeto encapsule explicitamente tal informação (mesmo que delegue a alguma outra classe o trabalho de gerenciá-la).

    
por 02.04.2014 / 19:16
fonte
0

However this approach robs both Card and Player classes of a possible member function.

E como isso é ruim / mal aconselhado?

Para usar uma analogia semelhante ao exemplo de cards, considere um Car , um Driver e será necessário determinar se o Driver pode direcionar o Car .

OK, você decidiu que não deseja que seu Car saiba se o Driver tem a chave do carro certa ou não e, por algum motivo desconhecido, você também decidiu que não deseja que seu Driver para saber sobre a classe Car (você também não especificou completamente a sua pergunta original). Portanto, você tem uma classe intermediária, algo semelhante a uma classe Utils , que contém o método com regras de negócios para retornar um valor boolean para a pergunta acima.

Eu acho que isso está bem. A classe intermediária só precisa verificar as chaves do carro agora, mas pode ser refatorada para considerar se o motorista tem uma carteira de motorista válida, sob a influência de álcool ou, em um futuro distópico, verificar a biometria do DNA. Por encapsulamento, realmente não há grandes problemas em ter essas três classes coexistindo juntas.

    
por 03.04.2014 / 05:31
fonte