Relação entre objetos

8

Por algumas semanas eu estive pensando sobre a relação entre objetos - não especialmente sobre os objetos do OOP. Por exemplo, em C ++ , estamos acostumados a representá-lo colocando em camadas ponteiros ou contêineres de ponteiros na estrutura que precisa de acesso ao outro objeto. Se um objeto Aprecisa ter um acesso a B, não é raro encontrar um B *pBem A.

Mas não sou mais um programador de C ++ , escrevo programas usando linguagens funcionais, e mais especialmente em Haskell , que é uma linguagem funcional pura. É possível usar ponteiros, referências ou esse tipo de coisa, mas me sinto estranho com isso, como "fazer da maneira não-Haskell".

Então eu pensei um pouco mais sobre todas essas coisas de relação e cheguei ao ponto:

“Por que representamos essa relação por camadas?

Eu li algumas pessoas que já pensaram sobre isso ( aqui ). Do meu ponto de vista, representar relações através de gráficos explícitos é muito melhor, pois nos permite focar no núcleo do nosso tipo e expressar relações posteriormente através de combinadores (um pouco como o SQL ).

Por essência, quero dizer que, quando definimos A, esperamos definir do que Aé feito , e não do que depende . Por exemplo, em um jogo de vídeo, se temos um tipo Character, ele é legítimo para falar sobre Trait, Skillou esse tipo de coisa, mas é se falamos Weaponou Items? Não tenho mais tanta certeza. Então:

data Character = {
    chSkills :: [Skill]
  , chTraits :: [Traits]
  , chName   :: String
  , chWeapon :: IORef Weapon -- or STRef, or whatever
  , chItems  :: IORef [Item] -- ditto
  }

parece realmente errado em termos de design para mim. Prefiro algo como:

data Character = {
    chSkills :: [Skill]
  , chTraits :: [Traits]
  , chName   :: String
  }

-- link our character to a Weapon using a Graph Character Weapon
-- link our character to Items using a Graph Character [Item] or that kind of stuff

Além disso, quando chega o dia de adicionar novos recursos, podemos apenas criar novos tipos, novos gráficos e links. No primeiro design, teríamos que quebrar o Charactertipo ou usar algum tipo de trabalho para estendê-lo.

O que você acha dessa ideia? O que você acha melhor para lidar com esse tipo de problema no Haskell , uma linguagem funcional pura?

phaazon
fonte
2
A pesquisa de opiniões não é permitida aqui. Você deve reformular a pergunta para algo como "há algum inconveniente nessa abordagem?" em vez de "o que você acha disso?" ou "qual é a melhor maneira de ...?" Por tudo o que vale a pena, não faz sentido perguntar por que as coisas são feitas de certa maneira em C ++, e não em linguagens funcionais, porque o C ++ carece de coleta de lixo, algo análogo às classes de tipo (a menos que você use modelos, que abre realmente uma grande lata de worms) e muitos outros recursos que facilitam a programação funcional.
Doval 28/05
Uma coisa que não vale nada é que, na sua abordagem, não é possível dizer, usando o sistema de tipos, se um Character deve ser capaz de segurar armas. Quando você tem um mapa de Characterspara Weaponse um personagem está ausente no mapa, isso significa que o personagem atualmente não possui uma arma ou que o personagem não pode conter armas? Além disso, você faria pesquisas desnecessárias porque não sabe a priori se a pessoa Characterpode segurar uma arma ou não.
Doval 28/05
@Doval Se os personagens incapazes de portar armas fossem uma característica do seu jogo, não faria sentido atribuir uma canHoldWeapons :: Boolbandeira ao registro do personagem , que informa imediatamente se ele pode portar armas e se o seu personagem não é no gráfico, você pode dizer que o personagem não está segurando armas no momento. Faça giveCharacterWeapon :: WeaponGraph -> Character -> Weapon -> WeaponGraphapenas agir como idse essa bandeira fosse Falsepara o personagem fornecido ou use Maybepara representar falha.
Bhecklilr 28/05
Eu gosto disso e, francamente, acho que o que você está projetando se encaixa muito bem em um estilo de aplicativo - o que eu costumo achar comum ao analisar problemas de modelagem de OO em Haskell. Os aplicativos permitem que você componha de maneira agradável e fluente os comportamentos no estilo CRUD, de modo que a idéia de modelar bits de forma independente e, em seguida, tenha algumas implementações de aplicativos que permitem compor esses tipos juntamente com as operações que são inseridas entre as composições para itens como validação, persistência e evento reativo etc. Isso se encaixa muito com o que você sugere lá. Os gráficos funcionam muito bem com aplicativos.
Jimmy Hoffa 28/05
3
@Doval Especificamente com tipos de dados estilo de discos, se você tem vários construtores o seguinte código é perfeitamente legal e não emite avisos com -Wallon GHC: data Foo = Foo { foo :: Int } | Bar { bar :: String }. Em seguida, digite cheques para chamar foo (Bar "")ou bar (Foo 0), mas ambos geram exceções. Se você estiver usando construtores de dados de estilo posicional, não haverá problema porque o compilador não gera os acessadores para você, mas você não obtém a conveniência dos tipos de registro para estruturas grandes e é necessário mais clichê para usar bibliotecas como lente.
Bhecklilr 28/05

Respostas:

2

Você realmente respondeu sua própria pergunta, mas ainda não a conhece. A pergunta que você está fazendo não é sobre Haskell, mas sobre programação em geral. Você está realmente se perguntando muito bem (parabéns).

Minha abordagem para o problema que você tem em mãos divide-se basicamente em dois aspectos principais: o design do modelo de domínio e as compensações.

Deixe-me explicar.

Design de modelo de domínio : é assim que você decide organizar o modelo principal do seu aplicativo. Eu posso ver que você já está fazendo isso pensando em Personagens, Armas e assim por diante. Além disso, você precisa definir as relações entre eles. Sua abordagem de definir objetos pelo que eles são e não pelo que eles dependem é totalmente válida. Tenha cuidado, pois não é uma bala de prata para todas as decisões relacionadas ao seu design.

Você definitivamente está fazendo a coisa certa pensando nisso com antecedência, mas em algum momento você precisa parar de pensar e começar a escrever algum código. Você verá se suas decisões iniciais foram as corretas ou não. Provavelmente não, pois você ainda não tem conhecimento completo de seu aplicativo. Então não tenha medo de refatorar quando perceber que certas decisões estão se tornando um problema, não uma solução. É importante escrever código ao pensar nessas decisões para validá-las e evitar a necessidade de reescrever tudo.

Existem vários bons princípios que você pode seguir, basta pesquisar no Google por "princípios de software" e você encontrará vários deles.

Trade-offs : Tudo é um trade-off. Por um lado, ter muitas dependências é ruim; no entanto, você terá que lidar com uma complexidade extra de gerenciamento de dependências em outro lugar, e não nos objetos do domínio. Não há decisão certa. Se você tem um pressentimento, vá em frente. Você aprenderá muito seguindo esse caminho e vendo o resultado.

Como eu disse, você está fazendo as perguntas certas e isso é a coisa mais importante. Pessoalmente, concordo com o seu raciocínio, mas parece que você já dedicou muito tempo pensando nisso. Apenas pare de ter medo de cometer um erro e cometer erros . Eles são realmente valiosos e você sempre pode refatorar seu código sempre que achar necessário.

Alex
fonte
Obrigado pela sua resposta. Desde que publiquei isso, segui muitos caminhos. Eu tentei AST, vinculei AST, mônadas gratuitas, agora estou tentando representar a coisa toda através de classes tipográficas . De fato, não há solução absoluta. Apenas sinto a necessidade de desafiar a maneira comum de fazer relacionamento, que está longe de ser robusta e flexível.
Phaazon
@phaazon Isso é ótimo e eu gostaria que mais desenvolvedores tivessem uma abordagem semelhante. Experimentar é a melhor ferramenta de aprendizado. Boa sorte com seu projeto.
Alex