
Compartir:
Ben es un desarrollador de segunda carrera que anteriormente pasó una década en los campos de la educación de adultos, la organización comunitaria y la gestión de organizaciones sin ánimo de lucro. Trabajó como defensor de los desarrolladores para Vonage. Escribe regularmente sobre la intersección entre el desarrollo comunitario y la tecnología. Originario del sur de California y residente durante mucho tiempo en Nueva York, Ben reside ahora cerca de Tel Aviv (Israel).
Conclusiones de la incorporación de la comprobación estática de tipos en Ruby
Nexmo ofrece SDK en varios idiomas para ayudar a la comunidad de desarrolladores a trabajar con nuestras diversas ofertas de API. Es muy posible interactuar directamente con cada API REST a través de llamadas HTTP que un desarrollador construye a medida. Sin embargo, aprovechar un SDK permite a un desarrollador alcanzar sus objetivos más rápidamente y con menos sobrecarga en su trabajo.
Por lo tanto, nos tomamos muy en serio la tarea de crear e iterar cada SDK. Cada mes se realizan decenas de millones de llamadas a la API a través de los SDK por parte de usuarios de todo el mundo que trabajan en iniciativas que van desde pequeños proyectos de aficionados hasta infraestructuras empresariales multinacionales.
Por ello, buscamos continuamente formas de mejorar la experiencia de los desarrolladores con cada SDK. Cada SDK se esfuerza por cumplir los objetivos establecidos en la Especificación de la biblioteca de servidores teniendo en cuenta las limitaciones propias de cada lenguaje.
Uno de los principios generales del pliego de condiciones es el siguiente:
Nuestras bibliotecas deben ser explícitas.
Esto se entiende como que, idealmente, cada clase, método, constante y demás debería estar definido y su valor y sus parámetros conocidos por los desarrolladores que confían en el SDK. El código explícito es más fácil de incorporar, lo que a su vez conduce a un código más sencillo de depurar y de resolver los inevitables problemas cuando surgen.
Para trabajar con ese objetivo en el SDK de Ruby hemos comenzado a incorporar la comprobación estática de tipos en nuestro código base utilizando la herramienta Sorbete comprobador de tipos. La versión versión v6.3.0 del SDK incluye la instalación e inicialización de la gema y las firmas de métodos para la clase SMS.
¿Hubo momentos educativos a lo largo de este proceso? Refactorizar una base de código de tipado dinámico en un lenguaje que siempre ha sido tradicionalmente de tipado dinámico produce algunas observaciones que merece la pena compartir. Durante el trabajo para la versión v6.3.0 descubrimos las dos joyas siguientes:
Pensar a través de la interfaz
El SDK de Ruby aprovecha las funciones private y protected para distinguir entre los distintos componentes de la arquitectura de la biblioteca. El código no definido dentro de una de las dos palabras clave mencionadas forma parte de la interfaz pública.
¿Qué significa esto en la práctica para usted como usuario del SDK? Es importante enmarcar estas diferenciaciones por varias razones.
Es decir, el código definido dentro de la interfaz public es el código que usted, como usuario, puede esperar que permanezca estable y cualquier refactorización no debe provocar cambios de última hora en ese código sin el debido aviso a los usuarios y el cambio de versión semántica. Las clases y métodos definidos en la public del SDK son los mecanismos en los que confías directamente para realizar tu trabajo en tus casos de uso. Se trata de código con el que interactuará directamente invocándolo por su nombre en sus llamadas a métodos, es decir client.sms.send.
Cuando pasemos a examinar nuestro uso de los private y protected debemos saber cuándo debemos utilizar una u otra.
Clásicamente, los rubyistas no dedicaban mucho tiempo a preocuparse por estas distinciones. De hecho, para muchos su uso se consideraba más una "buena práctica" que una "obligación", como ocurre en otros lenguajes, como Java. Después de todo, utilizar el método #send permite al desarrollador eludir la definición de la interfaz y acceder directamente a los métodos definidos en ella. Sin embargo, cuando empezamos a integrar el tipado estático en Ruby, estas definiciones de interfaz adquieren más importancia y nos exigen más exactitud en su aplicación.
En Ruby, la diferencia entre private y protected es si se puede acceder a un método fuera del ámbito de la clase en la que se definió. Veamos un ejemplo en el que se utiliza la palabra clave private palabra clave:
class MyExample
def public_method
puts "This is public"
end
private
def private_method
puts "This is private"
end
endEn el ejemplo anterior, puedo llamar a la función #private_method desde dentro de la clase MyExample pero si tuviera otra clase, aunque heredara de MyExampleel método no estaría disponible para ella. Por ejemplo, si tuviera una clase definida como sigue:
class MySecondExample < MyExample
end
El método privado MyExample.private_method no sería accesible al ámbito de la MySecondExample clase. Esto es así aunque la segunda clase sea una subclase de la clase MyExample clase.
Mientras que los métodos definidos dentro de la palabra clave protected son accesibles a las subclases que heredan de la clase padre. Por lo tanto, si la palabra clave private del ejemplo anterior se reclasificara como protectedlos métodos escritos en ella serían accesibles en el ámbito de la clase MySecondExample ámbito de la clase.
Independientemente de si el método se encuentra dentro de la sección protected o private el mensaje que transmite a los desarrolladores que utilizan el SDK es que estos métodos están sujetos a cambios sin previo aviso. Cualquier cambio en ellos no debería afectar al comportamiento público de la aplicación. Si lo hace, plantea preguntas sobre si este método realmente pertenece a una interfaz no pública.
Cuando empezamos a integrar la comprobación de tipos estática a través de la gema Sorbet, uno de los primeros problemas que encontramos fue que el comprobador de tipos informaba de errores de que no se podía acceder a los métodos.
Por ejemplo, la clase SMS como muchas otras clases del SDK, aprovechan el método Nexmo::Namespace#request para enviar la solicitud a la API. Debido a la flexibilidad inherente a la rigurosidad de las definiciones de interfaz en Ruby, el hecho de que este método estuviera definido bajo la palabra clave private y se utilizara en una subclase no impedía que se ejecutara tal y como estaba diseñado. Sin embargo, en las mejores convenciones de diseño de interfaces, dado que este método estaba siendo usado en una subclase, implícitamente debería ser definido dentro de la palabra clave protected palabra clave. Antes de redefinir la interfaz a protected el verificador de tipos reportó el siguiente error:
lib/nexmo/sms.rb:109: Method request does not exist on Nexmo::SMS https://srb.help/7003Una de las características útiles de Sorbet es que cada error viene con una URL adjunta que hace referencia a la documentación de ese código de error. En este caso, la documentación sobre el error 7003 dice: This error indicates a call to a method we believe does not exist (a la Ruby’s NoMethodError exception). La documentación continúa proporcionando ejemplos de código con problemas y formas de solucionarlos, junto con una explicación más detallada del error. En nuestro caso, creo que la segunda razón por la que Sorbet podría lanzar este error se aplica a nuestro código:
Incluso si el método existe cuando se ejecuta, Sorbet todavía podría informar de un error porque el método no siempre estará allí.
Un método definido dentro de la interfaz private es invisible para cualquier cosa fuera del ámbito de la clase en la que se definió. Aunque es posible aprovechar la flexibilidad de Ruby para invocarlo, eso no mejora su invisibilidad inherente. Como tal, Sorbet insiste en que el código sea visible en el lugar desde el que se llama. Esto asegura una base de código explícita.
Siga cada método hasta el final
La segunda joya que descubrimos en el proceso de introducir Sorbet en la base de código fue pensar profundamente en todas las implicaciones de cada método que se llama y se utiliza en el código.
A menudo, aunque una aplicación tenga una buena arquitectura, algunos elementos se nos escapan. Se puede intentar seriamente probar tanto las rutas de éxito como de fracaso de la aplicación. El código está diseñado para manejar la mayoría de los casos extremos que pueden surgir, pero aún así pueden producirse consecuencias no deseadas.
Un área en la que esto surgió para nosotros fue el valor de retorno por defecto para recuperar un objeto de un hash de parámetros. El código invocaba #unicode?un pequeño método que comprobaba si el valor del objeto estaba en formato Unicode o no:
if unicode?(params[:text]) && params[:type] != 'unicode'
...
private
def unicode?(text)
!GSM7.encoded?(text)
end
El método #unicode? devolvería un booleano en función del valor del parámetro. ¿Qué ocurre si no hay ningún :text dentro de los parámetros? La posibilidad de que eso ocurra es increíblemente pequeña durante la implementación, pero sin embargo, desde la perspectiva del código, afecta al valor de retorno del método #unicode? del método.
Si simulamos esa acción sin ningún valor para params[:text] veamos qué nos devuelve:
params[:text]
=> nil
Ruby devuelve nil cuando la clave no se encuentra dentro de un hash. Esto explicaría, por tanto, por qué Sorbet devolvía un error cuando se comprobaba el tipo de este método. La firma de método creada para el método #unicode? indica:
sig { params(text: String).returns(T::Boolean) }La firma anterior declara que el método acepta una entrada del tipo String y devuelve un valor de tipo Boolean tipo. Sin embargo, en el caso de que el parámetro sea nil la entrada pasa a ser nil y no un String.
En este punto, hay un par de opciones. Una opción sería reescribir la firma del método para permitir Nilable como entrada del método. Esto eliminaría el problema técnico. Sin embargo, no eliminaría el problema arquitectónico subyacente que Sorbet descubrió.
El código no quiere una situación en la que la entrada es nil nunca. Si el parámetro es nilentonces algo va mal. En ese caso, el código debería mostrar un error al usuario en lugar de seguir funcionando normalmente. Ese error mejorará la experiencia de uso del SDK porque ayudará a quienes desarrollen con él a detectar, diagnosticar y tratar los errores en su código más rápido y antes en su proceso iterativo.
Dado que el objetivo aquí es abordar el problema arquitectónico subyacente y no el síntoma, la solución es utilizar un método que no devuelva nil cuando no se proporciona ningún valor. La llamada a los datos de los parámetros se refactoriza a:
params.fetch(:text)El método #fetch tal y como se explica en los documentos de la Documentación de la API de Ruby lanzará una excepción KeyError si no se encuentra la clave del objeto:
KeyError (key not found: :text)Esa excepción, cuando se devuelve al usuario, es informativa y puede ayudar a orientar la mejora de su código en una fase temprana de su desarrollo.
Próximos pasos
Antes de embarcarnos en el proceso de incorporar la comprobación estática de tipos a nuestro SDK de Ruby, tuvimos muchas conversaciones sobre los méritos y deméritos de hacerlo. Una incógnita antes de empezar a recorrer el camino era saber si conllevaría beneficios concretos en el desarrollo de nuestro SDK, y cuáles serían. En este momento, la resolución a esa incógnita es un claro sí afirmativo.
La introducción del tipado estático en nuestra base de código Ruby ha ayudado a centrar nuestro trabajo como desarrolladores del SDK en una reflexión profunda sobre las implicaciones de cada elección de diseño, utilización de métodos y mucho más. Hemos mantenido un exhaustivo proceso de revisión de cada pull request en nuestro equipo. Aprovechamos la ejecución de pruebas de integración automatizadas y construimos pruebas que cubren rutas de éxito y fracaso. La adición de tipado estático es una nueva capa de garantizar la calidad del código y la experiencia positiva de los desarrolladores.
El tipado estático en Ruby o en cualquier otro lenguaje de tipado dinámico, también provoca cambios paradigmáticos en la forma de escribir el código. Impone la estandarización donde antes había mucha más flexibilidad. Este punto es controvertido en la comunidad Ruby. ¿Cuál es el enfoque preferido? Quizás la respuesta a esta controversia se encuentre en algún punto intermedio entre los dos extremos. Cierta flexibilidad preserva la magia de Ruby, mientras que el aumento de la estandarización y la convención reduce la posibilidad de que se descubran errores o casos extremos no descubiertos hasta ahora mucho más tarde en el proceso.
En lo que respecta al SDK de Nexmo Ruby, seguiremos implementando tipos gradualmente en el código base durante los próximos meses. El objetivo es conseguir un código 100% tipado y hacerlo de forma gradual.
Nexmo Ruby es de código abierto y aceptamos contribuciones. Si quieres participar puedes encontrarnos en GitHub
Compartir:
Ben es un desarrollador de segunda carrera que anteriormente pasó una década en los campos de la educación de adultos, la organización comunitaria y la gestión de organizaciones sin ánimo de lucro. Trabajó como defensor de los desarrolladores para Vonage. Escribe regularmente sobre la intersección entre el desarrollo comunitario y la tecnología. Originario del sur de California y residente durante mucho tiempo en Nueva York, Ben reside ahora cerca de Tel Aviv (Israel).