Con la cantidad de recursos gratuitos, libros, clases en línea y campamentos de codificación disponibles en la actualidad, todo el mundo puede aprender a programar. Sin embargo, sigue habiendo una brecha de calidad entre la codificación y la ingeniería de software. ¿Debe haber una?
Escribí mi primer "Hola mundo" hace más de veinte años. Ésa es la respuesta que doy si alguien me pregunta cuánto tiempo hace que programo. Desde hace diez años, disfruto de una carrera que me hace tocar código casi todos los días, es la respuesta que doy si me preguntan cuánto tiempo llevo como programador profesional.
¿Desde cuándo soy ingeniero de software? Yo diría que unos cinco años. Un momento, estas cifras no me cuadran. Entonces, ¿qué ha cambiado? ¿A quién consideraría un ingeniero de software y a quién "simplemente" un programador?
Definición de ingeniero informático
Codificar es relativamente fácil. Ya no todo son mnemotécnicos en ensamblador en sistemas ridículamente limitados. Y si usas algo tan expresivo y potente como Ruby, es aún más fácil.
Simplemente coges un ticket, encuentras dónde tienes que insertar tu código, averiguas la lógica que tienes que poner ahí, y boom - listo. Si eres un poco más avanzado, te aseguras de que tu código sea bonito. Lógicamente dividido en métodos. Tiene especificaciones decentes que no sólo prueban el camino feliz. Eso es lo que hace un buen programador.
Un ingeniero de software ya no piensa en métodos y clases, al menos no principalmente. En mi experiencia, un ingeniero de software piensa en flujos. Lo primero que ven es el atronador y embravecido río de datos e interacciones que atraviesa el sistema. Piensan en lo que tienen que hacer para desviar o alterar este flujo. El código bonito, los métodos lógicos y las especificaciones geniales son casi una ocurrencia tardía.
Es tortugas todo el camino hacia abajo
La gente suele pensar de una determinada manera sobre la mayoría de las interacciones con la realidad. A falta de un término mejor, llamémoslo perspectiva "descendente". Si mi cerebro está trabajando en prepararme una taza de té, primero averiguará los pasos generales: ir a la cocina, poner la tetera, preparar la taza, verter agua, volver al escritorio.
No pensará primero en qué taza usar, mientras estoy desconectada en mi escritorio; esa desconexión vendrá después, cuando esté delante del armario. No tendrá en cuenta que puede que nos hayamos quedado sin té (o, al menos, sin la taza de té). bien cosas). Es amplio, reactivo y propenso a errores. En resumen, muy humano en la naturaleza.
A medida que el ingeniero de software considere cambios en el flujo de datos, algo alucinante, naturalmente lo hará de forma similar. Consideremos este ejemplo de historia de usuario:
Un cliente encarga un widget. A la hora de fijar el precio del pedido, hay que tener en cuenta lo siguiente:
- Precio base del widget en la localidad del usuario
- Forma del widget (modificador de precio)
- Si se trata de un pedido urgente (modificador de precio)
- Si la entrega del pedido tiene lugar en un día festivo en la localidad del usuario (modificador de precio)
Todo esto puede parecer artificioso (y, obviamente, lo es), pero no dista mucho de algunas historias reales de usuarios que he tenido el placer de aplastar recientemente.
Veamos ahora el proceso de reflexión que podría emplear un ingeniero de software para abordar esta cuestión:
"Bueno, tenemos que obtener el usuario y su pedido. Luego empezamos a calcular el total. Empezaremos en cero. Luego aplicaremos el modificador de forma del widget. Luego el cargo de urgencia. Luego vemos si es un día festivo, boom, ¡hecho antes de comer!"
Ah, el subidón que puede provocar una simple historia de usuario. Pero el ingeniero de software es humano, no una máquina multihilo perfecta, y la receta anterior es a grandes rasgos. Entonces, el ingeniero sigue pensando en profundidad:
"El modificador de forma del widget es... oh, eso depende mucho del widget, ¿no? Y pueden ser diferentes por localización, incluso si no ahora entonces en el futuro," piensan, quemados anteriormente por los cambiantes requisitos empresariales, "y el recargo de apremio también puede serlo. Y los días festivos también son muy específicos de la zona, ¡y las zonas horarias también tienen que ver! Tenía un artículo aquí sobre cómo tratar con horas en diferentes zonas horarias en Rails... ¡oh, me pregunto si la hora del pedido se almacena con la zona en la base de datos! Mejor comprobar el esquema".
Muy bien, ingeniero de software. Para. Se supone que deberías estar preparando una taza de té, pero estás absorto delante del armario pensando si la taza de flores es siquiera aplicable a tu problema del té.
Widget para preparar la taza perfecta
Pero eso es fácilmente lo que puede ocurrir cuando se intenta hacer algo tan poco natural para el cerebro humano como pensar con varios niveles de detalle. simultáneamente.
Tras rebuscar brevemente en su amplio arsenal de enlaces sobre el manejo de la zona horaria, nuestro ingeniero se recompone y empieza a descomponerlo en código real. Si intentaran el enfoque ingenuo, podría ser algo como esto:
def calcular_precio(usuario, pedido)
precio.pedido = 0
order.price = WidgetPrices.find_by(widget_type: order.widget.type).price
order.price = WidgetShapes.find_by(widget_shape: order.widget.shape).modifier
...
fin
Y seguían y seguían, de esta manera deliciosamente procedimental, sólo para ser fuertemente cerrados en la primera revisión del código. Porque si lo piensas bien, es perfectamente normal pensar de esta manera: primero las grandes líneas y luego los detalles. Ni siquiera pensaste que te habías quedado sin el té bueno al principio, ¿verdad?
Nuestro ingeniero, sin embargo, está bien entrenado y no es ajeno al Objeto de Servicio, así que esto es lo que empieza a suceder en su lugar:
clase BaseOrderService
def self.call(usuario, pedido)
new(usuario, pedido).call
fin
def initialize(usuario, pedido)
@usuario = usuario
@pedido = pedido
end
def call
puts "[WARN] ¡Implementar llamada no predeterminada para #{self.class.name}!"
usuario, orden
end
end
class WidgetPriceService < BaseOrderService; end
class ShapePriceModifier < BaseOrderService; end
class RushPriceModifier < BaseOrderService; end
class HolidayDeliveryPriceModifier < BaseOrderService; end
clase OrderPriceCalculator < BaseOrderService
def call
user, order = WidgetPriceService.call(user, order)
usuario, pedido = ShapePriceModifier.call(usuario, pedido)
user, order = RushPriceModifier.call(user, order)
usuario, pedido = HolidayDeliveryPriceModifier.call(usuario, pedido)
usuario, pedido
end
end
```
Bien. Ahora podemos emplear un buen TDD, escribir un caso de prueba para él y desarrollar las clases hasta que todas las piezas encajen. Y será hermoso, también.
Además de completamente imposible de razonar.
El enemigo es el Estado
Claro, todos estos son objetos bien separados con responsabilidades únicas. Pero aquí está el problema: siguen siendo objetos. El patrón de objetos de servicio con su "fingir forzosamente que este objeto es una función" es realmente una muleta. No hay nada que impida a nadie llamar a HolidayDeliveryPriceModifier.new(usuario, pedido).algo_más_entero
. Nada impide que la gente añada estado interno a estos objetos.
Por no hablar de usuario
y pedir
también son objetos, y jugar con ellos es tan fácil como que alguien se cuele en un rápido ordenar.guardar
en algún lugar de estos objetos funcionales "puros", cambiando la fuente subyacente del estado de la verdad, es decir, una base de datos. En este ejemplo artificial no es un gran problema, pero seguro que puede volverse en tu contra si este sistema crece en complejidad y se expande en partes adicionales, a menudo asíncronas.
El ingeniero tenía la idea correcta. Y utilizó una forma muy natural de expresar esta idea. Pero saber cómo expresar esta idea -de una forma bella y fácil de razonar- estaba casi impedido por el paradigma de programación orientada a objetos subyacente. Y si alguien que todavía no ha dado el salto a expresar sus pensamientos como desviaciones del flujo de datos intenta alterar con menos habilidad el código subyacente, pasarán cosas malas.
Pureza funcional
Ojalá existiera un paradigma en el que expresar tus ideas en términos de flujos de datos no sólo fuera fácil, sino necesario. Si el razonamiento pudiera simplificarse, sin posibilidad de introducir efectos secundarios no deseados. Si los datos pudieran ser inmutables, como la florida taza en la que te preparas el té.
Sí, estoy bromeando, por supuesto. Ese paradigma existe, y se llama programación funcional.
Veamos cómo quedaría el ejemplo anterior en uno de nuestros favoritos, Elixir.
defmodule WidgetPrecios do
def preciopedido([usuario, pedido]) do
[usuario, pedido]
|> widgetprecio
|> modificador de precio de forma
|> modificador precio rush
|> modificador precio vacaciones
fin
defp widgetprecio([usuario, pedido]) do
%{widget: widget} = pedido
precio = WidgetRepo.getbase_price(widget)
[usuario, %{pedido | precio: precio }]
end
defp shapepricemodifier([usuario, pedido]) do
%{widget: widget, precio: precio actual} = pedido
modificador = WidgetRepo.getshapeprice(widget)
[usuario, %{pedido | precio: precio actual * modificador} ]
end
defp rushpricemodifier([usuario, pedido]) do
%{prisa: prisa, precio: precio actual} = pedido
if rush do
[usuario, %{pedido | precio: precio actual * 1,75} ]
else
[usuario, %{pedido|precio: precio_actual} ]
end
end
defp modificadorpreciosvacaciones([usuario, pedido]) do
%{fecha: fecha, precio: precio_actual} = pedido
modificador = HolidayRepo.getholidaymodifier(usuario, fecha)
[usuario, %{pedido | precio: precio actual * modificador}].
end
end
```
Puedes notar que es un ejemplo completo de cómo la historia de usuario se puede lograr realmente. Esto se debe a que es menos complicado que en Ruby. Estamos utilizando algunas características clave exclusivas de Elixir (pero generalmente disponibles en lenguajes funcionales):
Funciones puras. En realidad no cambiamos la entrada pedir
en absoluto, sólo estamos creando nuevas copias - nuevas iteraciones sobre el estado inicial. Tampoco saltamos a un lado para cambiar nada. E incluso si quisiéramos, pedir
es sólo un mapa "tonto", no podemos llamar a ordenar.guardar
en cualquier punto aquí porque simplemente no sabe lo que es.
Coincidencia de patrones. De forma bastante similar a la desestructuración de ES6, esto nos permite arrancar precio
y widget
de la orden y pasarla, en lugar de forzar a nuestros compañeros WidgetRepo
y HolidayRepo
para saber cómo hacer frente a una pedir
.
Operador de tuberías. Visto en orden_precio
nos permite pasar datos a través de funciones en una especie de "pipeline" - un concepto inmediatamente familiar para cualquiera que haya ejecutado alguna vez ps aux | grep postgres
para comprobar si la maldita cosa seguía funcionando.
Así es como piensas
Los efectos secundarios no son realmente una parte básica de nuestro proceso de pensamiento. Después de verter agua en la taza, no solemos preocuparnos de que un error en la tetera pueda hacer que se sobrecaliente y explote, al menos no tanto como para ir a hurgar en sus entrañas para comprobar si alguien no ha dejado inadvertidamente... explotar_después_de_verter
en alto.
El camino que lleva de programador a ingeniero de software -pasar de preocuparse por objetos y estados a preocuparse por flujos de datos- puede llevar años en algunos casos. A mí, por ejemplo, me llevó años. Con los lenguajes funcionales, empiezas a pensar en flujos en tu primera noche.
Hemos hecho ingeniería de software complicado para nosotros y para todos los recién llegados al campo. La programación no tiene por qué ser difícil y complicada. Puede ser fácil y natural.
No nos compliquemos y vayamos ya a lo funcional. Porque así es como pensamos.
Lea también: