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 A
precisa ter um acesso a B
, não é raro encontrar um B *pB
em 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
, Skill
ou esse tipo de coisa, mas é se falamos Weapon
ou 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 Character
tipo 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?
fonte
Character
deve ser capaz de segurar armas. Quando você tem um mapa deCharacters
paraWeapons
e 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 pessoaCharacter
pode segurar uma arma ou não.canHoldWeapons :: Bool
bandeira 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çagiveCharacterWeapon :: WeaponGraph -> Character -> Weapon -> WeaponGraph
apenas agir comoid
se essa bandeira fosseFalse
para o personagem fornecido ou useMaybe
para representar falha.-Wall
on GHC:data Foo = Foo { foo :: Int } | Bar { bar :: String }
. Em seguida, digite cheques para chamarfoo (Bar "")
oubar (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.Respostas:
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.
fonte