Eu assisti a palestra de Stuart Sierra " Thinking In Data " e peguei uma das idéias como um princípio de design neste jogo que estou criando. A diferença é que ele está trabalhando em Clojure e eu estou trabalhando em JavaScript. Vejo algumas diferenças importantes entre nossos idiomas:
- Clojure é uma programação funcional linguística
- A maioria dos estados é imutável
Tirei a ideia do slide "Tudo é um mapa" (de 11 minutos, 6 segundos a> 29 minutos). Algumas coisas que ele diz são:
- Sempre que você vê uma função que recebe de 2 a 3 argumentos, é possível transformá-la em um mapa e apenas passar um mapa. Há muitas vantagens nisso:
- Você não precisa se preocupar com a ordem dos argumentos
- Você não precisa se preocupar com informações adicionais. Se houver chaves extras, essa não é nossa preocupação. Eles simplesmente fluem, não interferem.
- Você não precisa definir um esquema
- Ao contrário de passar um Objeto, não há ocultação de dados. Mas ele argumenta que a ocultação de dados pode causar problemas e é superestimada:
- atuação
- Facilidade de implementação
- Assim que você se comunica pela rede ou pelos processos, é necessário que ambos os lados concordem com a representação dos dados. Isso é trabalho extra que você pode pular se apenas trabalhar com dados.
Mais relevante para a minha pergunta. São 29 minutos em: "Torne suas funções compostáveis". Aqui está o exemplo de código que ele usa para explicar o conceito:
;; Bad (defn complex-process [] (let [a (get-component @global-state) b (subprocess-one a) c (subprocess-two a b) d (subprocess-three a b c)] (reset! global-state d))) ;; Good (defn complex-process [state] (-> state subprocess-one subprocess-two subprocess-three))
Entendo que a maioria dos programadores não conhece o Clojure, então reescreverei isso no estilo imperativo:
;; Good def complex-process(State state) state = subprocess-one(state) state = subprocess-two(state) state = subprocess-three(state) return state
Aqui estão as vantagens:
- Fácil de testar
- Fácil de olhar para essas funções isoladamente
- É fácil comentar uma linha disso e ver qual é o resultado removendo uma única etapa
- Cada subprocesso pode adicionar mais informações ao estado. Se o subprocesso um precisa comunicar algo para o subprocesso três, é tão simples quanto adicionar uma chave / valor.
- Nenhum clichê para extrair os dados de que você precisa fora do estado, apenas para que você possa salvá-los novamente. Apenas passe o estado inteiro e deixe o subprocesso atribuir o que precisa.
Agora, voltando à minha situação: peguei esta lição e a apliquei no meu jogo. Ou seja, quase todas as minhas funções de alto nível pegam e retornam um gameState
objeto. Este objeto contém todos os dados do jogo. EG: Uma lista de badGuys, uma lista de menus, os itens no chão, etc. Aqui está um exemplo da minha função de atualização:
update(gameState)
...
gameState = handleUnitCollision(gameState)
...
gameState = handleLoot(gameState)
...
O que estou aqui para perguntar é: criei alguma abominação que perverteu uma idéia que só é prática em uma linguagem de programação funcional? O JavaScript não é funcionalmente idioma (embora possa ser escrito dessa maneira) e é realmente desafiador escrever estruturas de dados imutáveis. Uma coisa que me preocupa é que ele assume que cada um desses subprocessos é puro. Por que essa suposição precisa ser feita? É raro que qualquer uma das minhas funções seja pura (com isso, quero dizer, elas frequentemente modificam o gameState
. Não tenho outros efeitos colaterais complicados além disso). Essas idéias desmoronam se você não possui dados imutáveis?
Estou preocupado que um dia eu acorde e perceba que todo esse design é uma farsa e realmente acabei de implementar o anti-padrão Big Ball Of Mud .
Honestamente, eu tenho trabalhado nesse código há meses e tem sido ótimo. Sinto que estou recebendo todas as vantagens que ele reivindicou. Meu código é super fácil para eu raciocinar. Mas eu sou uma equipe de um homem só, por isso tenho a maldição do conhecimento.
Atualizar
Eu tenho codificado mais de 6 meses com esse padrão. Normalmente, a essa altura, esqueço o que fiz e é aí que "escrevi isso de maneira limpa?" entra em jogo. Se não tivesse, realmente lutaria. Até agora, não estou lutando.
Entendo como outro conjunto de olhos seria necessário para validar sua manutenção. Tudo o que posso dizer é que me preocupo com a manutenção em primeiro lugar. Eu sou sempre o evangelista mais alto em código limpo, não importa onde eu trabalho.
Quero responder diretamente àqueles que já têm uma experiência pessoal ruim com essa maneira de codificar. Na época eu não sabia, mas acho que estamos realmente falando de duas maneiras diferentes de escrever código. O jeito que eu fiz isso parece ser mais estruturado do que o que os outros experimentaram. Quando alguém tem uma experiência pessoal ruim com "Tudo é um mapa", eles falam sobre como é difícil manter, porque:
- Você nunca sabe a estrutura do mapa que a função requer
- Qualquer função pode alterar a entrada da maneira que você nunca esperaria. Você deve procurar por toda a base de código para descobrir como uma chave específica entrou no mapa ou por que ela desapareceu.
Para aqueles com essa experiência, talvez a base de código fosse "Tudo leva 1 de N tipos de mapas". O meu é: "Tudo leva 1 de 1 tipo de mapa". Se você conhece a estrutura desse tipo 1, conhece a estrutura de tudo. Obviamente, essa estrutura geralmente cresce com o tempo. É por isso...
Há um lugar para procurar a implementação de referência (ou seja: o esquema). Essa implementação de referência é o código que o jogo usa para não ficar desatualizado.
Quanto ao segundo ponto, não adiciono / removo chaves do mapa fora da implementação de referência, apenas modifico o que já existe. Eu também tenho um grande conjunto de testes automatizados.
Se essa arquitetura eventualmente entrar em colapso com seu próprio peso, adicionarei uma segunda atualização. Caso contrário, suponha que tudo está indo bem :)