Si camina como un pato y grazna como un pato, entonces debe ser un pato
Vimos en un artículo anterior que es mucho mejor depender de abstracciones que de clases concretas. Las entidades concretas son más propensas a cambiar que las abstracciones, y esos cambios se propagarán por nuestro código fuente afectando todas las clases que dependen de ellas.
También aprendimos que los lenguajes de tipado estático nos ofrecen una gran construcción para representar una abstracción: la interfaz. La mayoría de lenguajes también proporcionan clases abstractas, que son como clases regulares que no puedes instanciar.
¿Cómo podemos representar abstracciones en un lenguaje de tipado dinámico como Ruby? La respuesta es Duck Typing.
Los duck types son abstracciones encarnadas por una interfaz pública implementada a través de clases. Si esto suena un poco críptico, es porque la forma más fácil de entender los duck types es a través de un ejemplo. Imagina que tenemos una clase Rocket, con un método llamado upkeep, que recibe una colección de profesionales de mantenimiento para realizar tareas de mantenimiento en diferentes partes del cohete.
class Rocket
attr_reader :engines, :computers, :fuel_tank
# ...
def upkeep(maintainers)
maintainers.each do |maintainer|
case maintainer
when PropulsionEngineer
maintainer.verify_engines(engines)
when ComputerTechnician
maintainer.perform_computer_maintenance(computers)
when PropellantCrew
maintainer.fix_fuel_tank(fuel_tank)
end
end
end
# ...
end
class PropulsionEngineer
def verify_engines(engines)
engines.each {|engine| check_engine(engine)}
end
def check_engine(engine)
#...
end
end
class ComputerTechnician
def perform_computer_maintenance(computers)
computers.each do |computer|
verify_circuitry(computer)
update_software(computer)
end
end
def verify_circuitry(computer)
#...
end
def update_software(computer)
#...
end
end
class PropellantCrew
def fix_fuel_tank(fuel_tank)
#...
end
end
El código anterior funciona, pero la solución está lejos de ser ideal. Tal como está ahora, Rocket depende de PropulsionEngineer, ComputerTechnician y PropellantCrew. Cualquier cambio en la firma de los métodos usados en upkeep obligará a Rocket a cambiar. Además, la inclusión de pasos adicionales de mantenimiento requiere que abramos la clase Rocket y agregemos código extra.
Es importante darse cuenta de que aunque las clases de los otros objetos son importantes, todo lo que le importa a Rocket es el mantenimiento. Lo que estas clases carecen es una interfaz común en la que Rocket pueda confiar: un duck type de mantenedor.
Necesitamos dejar de pensar en términos de la clase a la que pertenece un objeto y enfocarnos más en su comportamiento: más importante que el tipo del objeto son los mensajes a los que puede responder.
Un duck type Maintainer
Ahora que descubrimos que necesitamos un duck, necesitamos saber a qué mensajes necesita responder. En nuestro caso, Rocket solo se preocupa porque otras clases realicen trabajo de mantenimiento, así que tiene sentido que respondan al mensaje perform_maintenance. Refactorizamos y obtenemos el siguiente código:
class Rocket
attr_reader :engines, :computers, :fuel_tank
# ...
def upkeep(maintainers)
maintainers.each do |maintainer|
maintainer.perform_maintenance(self)
end
end
# ...
end
class PropulsionEngineer
def perform_maintenance(rocket)
rocket.engines.each{|engine| check_engine(engine)}
end
#...
end
class ComputerTechnician
def perform_maintenance(rocket)
rocket.computers.each do |computer|
verify_circuitry(computer)
update_software(computer)
end
end
#...
end
class PropellantCrew
def perform_maintenance(rocket)
fix_fuel_tank(rocket.fuel_tank)
end
#...
end
Al crear una abstracción para el comportamiento del mantenedor, el nuevo método upkeep es mucho más simple. Más importante es el hecho de que ya no depende de clases concretas, mientras un objeto implemente perform_maintenance el cohete trabajará gustosamente con él. Es el Principio de Inversión de Dependencias aplicado en un lenguaje de tipado dinámico.
Encontrando ducks ocultos en tu código
Puede que haya patos (jeje) escondidos en tu código: interfaces públicas que necesitan ser definidas, pero de las que aún no te das cuenta. El code smell más común que apunta a la presencia de un duck type oculto es usar el mecanismo integrado de un lenguaje para identificar el tipo de un objeto para determinar qué método debería ser llamado.
En el ejemplo que acabamos de ver, la declaración case verificaba si el objeto era un PropulsionEnginner y llamaba al método correcto. Las declaraciones case en el tipo de un objeto no son el único escenario al que le debes prestar atención, otras alarmas comunes para ducks ocultos son:
En Ruby:
- kind_of?
- is_a?
- responds_to?
En Python:
- type/is
- isinstance
- hasattr
- callable
Aunque estas cosas no siempre significan que hay algo mal en tu código, es importante preguntarte si objetos con diferentes tipos realizan la misma acción con un nombre y especificaciones diferentes. Si ese es el caso, podrías querer definir una interfaz común para llamar esa acción.
La importancia de la documentación
Los lenguajes de tipado dinámico, a diferencia de sus contrapartes de tipado estático, usualmente no te ofrecen construcciones como interfaces o clases abstractas para representar tus abstracciones. Los duck types son otro tipo de abstracción: no viven en un único archivo que puedas abrir y leer, son una interfaz pública implementada a través de diferentes clases.
Aunque este enfoque tiene ventajas, no puedes contar con el compilador para encontrar errores de tipo. Es tu responsabilidad escribir documentación para respaldar estos tipos y escribir pruebas que sirvan como documentación adicional. Hará las cosas mucho más fáciles para ti y para otras personas trabajando en el mismo proyecto.
Qué hacer a continuación:
- Comparte este artículo con amigos y colegas. Gracias por ayudarme a llegar a personas que podrían encontrar útil esta información.
- Envíame un email con preguntas, comentarios o sugerencias (está en la página Autor)