
Compartilhar:
Ben é um desenvolvedor que mudou de carreira, tendo atuado anteriormente por uma década nas áreas de educação de adultos, organização comunitária e gestão de organizações sem fins lucrativos. Ele trabalhou como representante de desenvolvedores na Vonage. Escreve regularmente sobre a interseção entre desenvolvimento comunitário e tecnologia. Natural do sul da Califórnia e morador de longa data da cidade de Nova York, Ben reside atualmente perto de Tel Aviv, em Israel.
Conclusões sobre a incorporação da verificação de tipos estática no Ruby
A Nexmo oferece SDKs em diversas linguagens de programação para apoiar a comunidade de desenvolvedores no trabalho com nossas diversas ofertas de API. É perfeitamente possível interagir diretamente com cada API REST por meio de chamadas HTTP criadas sob medida pelo desenvolvedor. No entanto, aproveitar as vantagens de um SDK permite que o desenvolvedor alcance seus objetivos mais rapidamente e com menos esforço em seu trabalho.
Portanto, a tarefa de desenvolver e aprimorar cada SDK é algo que nós, como equipe, levamos muito a sério. Dezenas de milhões de chamadas de API são feitas mensalmente por meio dos SDKs por usuários de todo o mundo que trabalham em iniciativas que vão desde pequenos projetos de hobby até infraestruturas empresariais multinacionais.
Por isso, estamos sempre buscando maneiras de melhorar a experiência do desenvolvedor a cada SDK. Cada SDK se empenha em atingir as metas estabelecidas na Especificação da Biblioteca de Servidor , levando em consideração as restrições específicas de cada linguagem.
Um dos Princípios Gerais descritos na Especificação é o seguinte:
Nossas bibliotecas devem ser explícitas.
Entende-se que, idealmente, todas as classes, métodos, constantes e outros elementos devem ser definidos, e seus valores e parâmetros devem ser conhecidos pelos desenvolvedores que utilizam o SDK. Código explícito é mais fácil de incorporar, o que, por sua vez, resulta em código mais simples de depurar e de resolver os inevitáveis problemas quando eles surgirem.
A fim de trabalhar nesse sentido no SDK do Ruby , começamos a incorporar a verificação estática de tipos em nossa base de código usando o gem . A versão v6.3.0 do SDK inclui a instalação e a inicialização da gem, além das assinaturas de métodos para a classe SMS.
Houve momentos de aprendizado ao longo desse processo? Refatorar uma base de código tipada dinamicamente em uma linguagem que sempre foi tradicionalmente tipada dinamicamente traz algumas observações que valem a pena compartilhar. Durante o trabalho para o lançamento da versão v6.3.0, descobrimos as duas pérolas a seguir:
Refletindo sobre a interface
O SDK do Ruby aproveita o private e protected para distinguir entre os diferentes componentes da arquitetura da biblioteca. O código que não estiver definido dentro de uma das duas palavras-chave mencionadas acima faz parte da interface pública.
O que isso significa, na prática, para você como usuário do SDK? É importante esclarecer essas diferenças por várias razões.
Ou seja, o código definido dentro da public interface é aquele que você, como usuário, pode esperar que permaneça estável, e qualquer refatoração não deve causar alterações que quebrem a compatibilidade desse código sem o devido aviso aos usuários e uma mudança na versão semântica. As classes e métodos definidos na public interface do SDK são os mecanismos nos quais você confia diretamente para realizar seu trabalho em seus casos de uso. Esse é o código com o qual você irá interagir diretamente, invocando-o pelo nome em suas chamadas de método, ou seja, client.sms.send.
Quando analisamos nosso uso do private e protected , precisamos entender quando devemos usar uma ou outra.
Tradicionalmente, os programadores de Ruby não se preocupavam muito com essas distinções. Na verdade, para muitos, o próprio uso dessas definições era frequentemente visto mais como uma “boa prática” e não como uma “obrigação”, como ocorre em outras linguagens, como o Java. Afinal, utilizar o método #send permite que o desenvolvedor contorne a definição da interface de qualquer maneira e acesse diretamente os métodos nela definidos. No entanto, quando começamos a integrar a tipagem estática ao Ruby, essas definições de interface ganham mais importância e exigem de nós maior precisão em sua aplicação.
Em Ruby, a diferença entre private e protected está no fato de um método poder ou não ser acessado fora do escopo da classe em que foi definido. Vamos dar uma olhada em um exemplo usando a private palavra-chave:
class MyExample
def public_method
puts "This is public"
end
private
def private_method
puts "This is private"
end
endNo exemplo acima, posso chamar o #private_method de dentro da MyExample classe, mas se eu tivesse outra classe, mesmo que ela herdasse de MyExample, o método não estaria disponível para ela. Por exemplo, se eu tivesse uma classe definida da seguinte forma:
class MySecondExample < MyExample
end
O método privado MyExample.private_method não seria acessível no escopo da MySecondExample classe. Isso é verdade mesmo que a segunda classe seja uma subclasse da MyExample classe.
Considerando que os métodos definidos dentro da protected palavra-chave são acessíveis às subclasses que herdam da classe pai. Portanto, se a private palavra-chave no exemplo acima fosse reclassificada como protected, os métodos definidos nela ficariam acessíveis no MySecondExample escopo da classe.
Independentemente de o método estar dentro do protected ou private escopo, a mensagem transmitida aos desenvolvedores que utilizam o SDK é que esses métodos estão sujeitos a alterações sem aviso prévio ao mundo externo. Qualquer alteração neles não deve afetar o comportamento público do aplicativo. Caso isso ocorra, levanta-se a questão de se esse método realmente pertence a uma interface não pública.
Quando começamos a integrar a verificação de tipos estática por meio da gem Sorbet, um dos primeiros problemas que encontramos foi o verificador de tipos relatar erros indicando que não era possível acessar certos métodos.
Por exemplo, a SMS classe, assim como muitas outras classes no SDK, utiliza o Nexmo::Namespace#request método para enviar a solicitação à API. Devido à flexibilidade inerente e ao rigor das definições de interface no Ruby, o fato de esse método ter sido definido sob a private palavra-chave e o fato de estar sendo usado em uma subclasse não impediu que ele fosse executado da maneira como foi arquitetado. No entanto, de acordo com as melhores convenções de projeto de interface, como esse método estava sendo usado implicitamente em uma subclasse, ele deveria ter sido definido dentro da protected palavra-chave. Assim, antes de redefinirmos a interface para protected o verificador de tipos relatou o seguinte erro:
lib/nexmo/sms.rb:109: Method request does not exist on Nexmo::SMS https://srb.help/7003Uma das funcionalidades úteis do Sorbet é que cada erro vem acompanhado de um URL que remete à documentação referente a esse código de erro. Neste caso, a documentação sobre o erro 7003 afirma: This error indicates a call to a method we believe does not exist (a la Ruby’s NoMethodError exception). A documentação continua apresentando exemplos de código com problemas e maneiras de resolvê-los, além de explicações adicionais sobre o erro. No nosso caso, acredito que a segunda razão pela qual o Sorbet poderia gerar esse erro se aplicava ao nosso código:
Mesmo que o método exista no momento da execução, o Sorbet ainda pode relatar um erro, pois nem sempre ele estará disponível.
Um método definido dentro da private interface é invisível para qualquer elemento fora do escopo da classe em que foi definido. Embora seja possível aproveitar a flexibilidade do Ruby para ainda assim invocá-lo, isso não altera sua invisibilidade inerente. Por isso, o Sorbet exige que o código seja visível no local de onde é chamado. Isso garante uma base de código explícita.
Siga cada método até o fim
A segunda descoberta que fizemos ao integrar o Sorbet à base de código foi refletir profundamente sobre todas as implicações de cada método chamado e utilizado no código.
Muitas vezes, mesmo que uma aplicação tenha uma arquitetura bem projetada, alguns aspectos podem passar despercebidos. É importante dedicar um esforço significativo para testar tanto os caminhos de sucesso quanto os de falha da aplicação. O código é desenvolvido para lidar com a maioria dos casos extremos comuns que possam surgir; no entanto, ainda assim, podem ocorrer consequências indesejadas.
Uma área em que isso ficou evidente para nós foi o valor de retorno padrão ao recuperar um objeto de um hash de parâmetros. O código chamava #unicode?, um pequeno método que verificava se o valor do objeto estava no formato Unicode ou não:
if unicode?(params[:text]) && params[:type] != 'unicode'
...
private
def unicode?(text)
!GSM7.encoded?(text)
end
O #unicode? método retornaria um valor booleano dependendo do valor do parâmetro. Mas o que aconteceria se não houvesse :text objeto nos parâmetros? A possibilidade de isso acontecer é extremamente remota durante a implementação, mas, mesmo assim, do ponto de vista do código, isso afeta o valor de retorno do #unicode? método.
Se simularmos essa ação sem nenhum valor para params[:text] vamos ver o que ela retorna:
params[:text]
=> nil
O Ruby retorna nil quando a chave não pode ser encontrada dentro de um hash. Isso explicaria, portanto, por que o Sorbet retornou um erro quando esse método foi submetido à verificação de tipos. A assinatura do método criada para o #unicode? método afirma:
sig { params(text: String).returns(T::Boolean) }A assinatura acima declara que o método aceita uma entrada do String tipo e retorna um valor do Boolean tipo. No entanto, no caso em que o parâmetro nil a entrada passa a ser nil e não um String.
Neste momento, há algumas opções. Uma delas seria reescrever a assinatura do método para permitir que Nilable parâmetros como entrada do método. Isso eliminaria o problema técnico. No entanto, não eliminaria o problema arquitetônico subjacente que o Sorbet revelou.
O código não quer que ocorra uma situação em que a entrada seja nil nunca. Se o parâmetro for nil, então algo está errado. Nesse caso, o código deveria, na verdade, exibir uma mensagem de erro para o usuário, em vez de continuar funcionando normalmente. Esse erro melhorará a experiência de uso do SDK, pois ajudará quem está desenvolvendo com ele a detectar, diagnosticar e corrigir os erros no código mais rapidamente e em um estágio mais precoce do processo iterativo.
Como o objetivo aqui é resolver o problema arquitetônico subjacente e não o sintoma, a solução é utilizar um método que não retorne nil quando não houver nenhum valor fornecido. A chamada aos parâmetros data passa a ser refatorada para:
params.fetch(:text)O #fetch método, conforme discutido na documentação da API do Ruby gerará uma KeyError exceção se a chave do objeto não for encontrada:
KeyError (key not found: :text)Essa exceção, quando apresentada ao usuário, tem caráter informativo e pode ajudar a orientar o aprimoramento do código logo no início do seu desenvolvimento.
Próximos passos
Antes de iniciarmos o processo de incorporar a verificação de tipos estática ao nosso SDK de Ruby, tivemos muitas discussões sobre as vantagens e desvantagens dessa decisão. Uma incógnita antes de iniciarmos esse caminho era saber se isso traria benefícios concretos para o desenvolvimento do nosso SDK e quais seriam esses benefícios. Neste momento, a resposta a essa questão é um claro “sim”.
A introdução da tipagem estática em nossa base de código Ruby ajudou a direcionar nosso trabalho como desenvolvedores do SDK para uma reflexão profunda sobre as implicações de cada escolha de design, utilização de métodos e muito mais. Mantemos um processo de revisão minucioso de cada pull request em nossa equipe. Aproveitamos a execução de testes de integração automatizados e criamos testes que abrangem tanto os caminhos de sucesso quanto os de falha. A adição da tipagem estática é uma nova camada para garantir a qualidade do código e uma experiência positiva para os desenvolvedores.
A tipagem estática no Ruby — ou em qualquer outra linguagem de tipagem dinâmica — também traz mudanças paradigmáticas na forma como o código é escrito. Ela impõe padronização onde antes havia muito mais flexibilidade. Esse ponto é controverso na comunidade Ruby. Qual é a abordagem preferida? Talvez a resposta para essa controvérsia esteja em algum ponto intermediário entre os dois extremos. Um pouco de flexibilidade preserva a magia do Ruby, enquanto o aumento da padronização e das convenções reduz a possibilidade de bugs ou de casos extremos até então desconhecidos serem descobertos muito mais tarde no processo.
No que diz respeito ao SDK do Nexmo Ruby, continuaremos a implementar tipos na base de código de forma gradual ao longo dos próximos meses. O objetivo é alcançar uma base de código 100% tipada e fazê-lo de maneira incremental.
O Nexmo Ruby é um projeto de código aberto e aceitamos contribuições! Se você quiser participar, pode nos encontrar no GitHub
Compartilhar:
Ben é um desenvolvedor que mudou de carreira, tendo atuado anteriormente por uma década nas áreas de educação de adultos, organização comunitária e gestão de organizações sem fins lucrativos. Ele trabalhou como representante de desenvolvedores na Vonage. Escreve regularmente sobre a interseção entre desenvolvimento comunitário e tecnologia. Natural do sul da Califórnia e morador de longa data da cidade de Nova York, Ben reside atualmente perto de Tel Aviv, em Israel.