tl;dr
Eu escrevo mais código funcional, evito a mutação de dados e efeitos colaterais em métodos, não tenho receio das primitivas de simultaneidade (modernas), explorei opções alternativas de implantação.
Introdução rápida
Eu escrevo código Ruby para viver desde 2007/2008. Então, você pode me chamar de veterano (ou de combatente, se isso lhe agrada). Ao longo do caminho, escrevi um pouco de Java, muitos CoffeeScript e JavaScript, alguns bits de Python e até mesmo PHP, pequenas quantidades de Clojure e Elm. Tenho notado, no entanto, que nenhuma dessas linguagens influenciou muito a maneira como escrevo código Ruby – até que comecei a escrever Elixir para viver também.
No momento, divido meu trabalho entre dois projetos. Um onde escrevo Ruby, outro onde escrevo Elixir. O meu estilo Ruby mudou, e não em um nível consciente no início. Isso só ficou claro para mim quando eu comecei a olhar para o meu antigo código Ruby e não o reconheci como algo que eu escreveria nos dias de hoje.
Evitando métodos encadeáveis
A influência óbvia é o uso de paradigmas de programação funcionais em Ruby. Ao contrário do que você pode pensar, não me refiro aos métodos encadeáveis (como em Enumerable ou classes próprias). Na verdade – se alguma coisa relacionada a isso – eu aprendi a evitar métodos encadeáveis juntos, e, em vez disso, segui com alternativas mais simples e melhores.
Os métodos encadeáveis podem ter algumas vantagens, especialmente para a legibilidade do código em aplicativos de estilo DSL, mas também podem levar a problemas de refatoração, tamanho de inchaço de classes/objetos e dados e depuração mais difícil.
Elixir fornece o operador pipe (|>), onde você pode encadear chamadas de função passando o valor de retorno da função anterior como primeiro argumento do próximo na cadeia.
Existem algumas implementações em Ruby, mas não as achei tão atraentes. Em vez disso, eu simplesmente uso uma variável local temporária para executar o método encadeando de maneira simples e morta:
def process_input(input) tmp_value = step1(input) tmp_value = step2(tmp_value) tmp_value = step3(tmp_value) step4(tmp_value) end
Este não é um exemplo bonito do que você pode chamar de “estilo”, mas é simples, legível e faz o trabalho sem efeitos colaterais.
Lógica encapsuladora em pequenos métodos
Eu percebi, que eu também escrevo métodos mais curtos do que antes. Por exemplo, sempre que eu tiver uma cláusula if no meu código Ruby, eu a extrairia para o método maybe_do_something_now (data), em vez de mantê-la em linha.
Isso tem efeitos indesejados sobre o tamanho das classes, mas melhora consideravelmente a legibilidade.
Isso também ajuda a encadear chamadas de função, para criar pipelines de processamento de dados. Se você tiver uma plataforma de comércio eletrônico, você poderia escrever um código como este:
def order_total() total = cart_summary() total = maybe_add_shipping_cost(total) total = maybe_apply_discount(total) total = maybe_subtract_credit(total) total end
Limitando a herança
Foi-nos dito para reutilizar o comportamento de nossas classes e objetos através da herança. Esta é uma armadilha da morte se você abusar do mecanismo. Dê uma olhada no infame ActiveRecord::Base, que tenta fazer tudo e, como resultado, muitas vezes é imprevisível e leva a problemas de desempenho da sua aplicação se você abusar dele.
Em Elixir, você realmente não pode copiar facilmente as funções de um módulo para outro. Isso é exatamente o que a herança e os módulos incluídos fazem no Ruby, no entanto. Você tem palavra-chave import, mas ela apenas apelida a função remota que pode ser referenciada como local, enquanto ainda é externa (ou seja, não tem acesso aos atributos do módulo do módulo importador, etc.).
Eu me vi extraindo comportamento comum para módulos ou classes externas em Ruby, mas eu não incluo ou herdo muito disso. Pelo contrário, não me importo de criar módulos de utilidade e chamar funções diretamente neles. Se eu compartilhei a função step3 entre as minhas clsses, muitas vezes simplesmente escrevi:
def process_input(input) tmp_value = step1(input) tmp_value = step2(tmp_value) tmp_value = UtilityModule.step3(tmp_value) step4(value) end
Isso resulta em classes e objetos menores, e também fornece um encapsulamento mais limpo da funcionalidade. Nós não precisamos mais criar ActiveRecord::Base com todas as funções que precisamos!
Limitação da mutabilidade
Em Elixir, todos os dados são imutáveis. Sempre que você deseja fornecer dados mutáveis, você precisa usar algo como o GenServer ou o Agent para manter o estado mutável.
Na maioria das linguagens de programação processuais e orientadas a objetos, é o contrário: seus dados são mutáveis por padrão. E Ruby não é exceção.
Enquanto Ruby permite freeze objetos (e depois descongelar;)), o padrão simples que uso é para:
- Criar novos objetos que levem seus argumentos iniciais no construtor
- Processar os argumentos no construtor e definir o atributo do objeto
- Nunca mais modificar esses atributos de objeto. Em vez disso, quando precisamos modificá-lo, fazemos isso em métodos de objeto ou devolvemos um novo objeto com dados modificados.
Um exemplo poderia ser:
class Counter attr_reader :value def initialize(payload) @value = payload.to_i end def increment Counter.new(@value + 1) end end
Então, em vez de modificar o mesmo contador, sempre retornamos um novo se precisarmos.
Mas descobri que, em muitos casos, você nunca precisa retornar um objeto modificado. Tome a resposta das chamadas da API como exemplo. Você obtém um corpo de resposta, processa-o inicialmente no construtor (de-serialize o JSON?), o estabelece um atributo de objeto, e tudo o que você precisa fazer mais tarde é fornecer um conjunto de getters, que modificaria os dados internamente em variáveis locais – mas nunca modifique os atributos do objeto novamente.
Escrevendo código concorrente
Os programadores de Ruby muitas vezes fogem de escrever código concorrente. Existem boas razões para isso, incluindo penalidades de desempenho e redução da legibilidade do código.
Elixir herda o modelo de concorrência de Erlang e você escreve um monte de código concorrente. Toda vez que você quer ter um estado mutável, você escreve código concorrente. Sempre que quiser fazer algo de forma assíncrona, você escreve código concorrente.
Em Ruby, muitas vezes procuramos trabalhos em segundo plano para tarefas semelhantes. Muitas vezes, prematura e desnecessariamente.
Ontem eu tive que escrever o client da API, com limitação de taxa e cache interno. Eu poderia escrever algum tipo de bloqueio, contagem de pedidos. Muitos provavelmente chegariam ao banco de dados para fornecer meios de sincronização e bloqueio entre as threads do web worker.
Acabei escrevendo GenServer – semelhante ao ator, gerado em um tópico separado, que armazena em cache a resposta da API no estado interno e garante que não excedamos os limites da API. A coisa toda é curta, legível e funciona bem por enquanto.
Nós temos ferramentas de concorrência modernas em Ruby agora, não devemos evitar usá-las. Celluloid é um começo incrível. Se você estiver usando o Rails > 5, você já possui uma dependência moderna e agradável na biblioteca concurrent-ruby também. Confira e use-a – ela pode simplificar muito o seu projeto.
Há uma boa razão para evitar primitivas básicas de threading fornecidas por Ruby, no entanto. Elas são mapeadas diretamente das chamadas do sistema UNIX subjacentes e não são nem fáceis nem seguras de ser usadas diretamente por um programador novato. Não use isso para prototipar coisas, pegar uma biblioteca mais agradável e iterar se precisar melhorar o desempenho mais tarde.
Opções alternativas de implantação
Ao trabalhar com o código Ruby, eu costumava usar o Heroku, e se isso provasse ser difícil ou caro mais tarde – eu mudaria para Capistrano e um servidor mais tradicional. Embora isto seja relativamente simples e funcione na maioria dos casos, a abordagem tem desvantagens significativas.
As opções de implantação em Elixir são muito mais complexas e esmagadoras no início. Evitei fortemente aprender a implantar aplicativos Elixir por um período de tempo embaraçoso.
Em princípio, as aplicações Elixir são compiladas para um binário que está sendo carregado no servidor e iniciado lá. Isso difere muito da implantação tradicional do Ruby, na qual você enviaria o código-fonte para o servidor, criaria suas dependências e o executaria lá.
Você pode dizer que a diferença está no fato de que Ruby é interpretado e Elixir é compilado – e você está certo. A questão é que nossos aplicativos não são mais unicamente Ruby. Há muito JavaScript, arquivos SCSS e arquivos gráficos que precisam ser processados por pipeline de recursos também. Isso não precisa ser feito no servidor.
Você precisa de todas as dependências de compilação no servidor ou na instância do Heroku se quiser seguir com a rota de implantação tradicional. Contrariamente a isso, se você usa algo como implementações do Docker para Rails, sua fase de compilação é executada em sua máquina local e a coisa que você envia para o Heroku ou um VPS já está pronta para ser executada. Você ainda precisará do node.js no servidor, uma vez que o Rails o usa internamente, mas você não precisa lidar com pacotes NPM, versões, dependências etc. Isso facilita a manutenção dos servidores com os aplicativos Rails em longo prazo.
Outro
Um desenvolvedor que está ativamente tentando melhorar suas habilidades, precisa chegar a outras linguagens, ambientes, comunidades. Você pode continuar usando a mesma stack que você usou há anos, mas isso não significa que a experiência de aprender novas tecnologias está completamente desperdiçada. Conscientemente ou não, você importará algumas das coisas novas que você aprendeu para o seu fluxo de trabalho de cada dia – e se tornará um desenvolvedor melhor no processo.
Eu fiz isso comigo mesmo e depois compartilhei o conhecimento com a equipe. Na verdade, organizamos uma série de oficinas internas sobre Elixir, e os desenvolvedores ensinaram a stack eles mesmos, com as próprias mãos. Provavelmente é um assunto para uma publicação separada – mas acredito que somos todos agora melhores Rubistas. Faça isso sozinho e também a sua equipe de desenvolvimento Ruby.
Agora pare de ler e vá ensinar você mesmo um pouco de Elixir. Isso fará de você um melhor Rubista.
***