Programación orientada a objetos (POO): el desastre del billón de dólares
¿Por qué es hora de pasar de la programación orientada a objetos?
Programación orientada a objetos
Muchos consideran que la programación orientada a objetos es la joya de la corona de la informática. La solución definitiva para la organización del código. El fin de todos nuestros problemas. La única forma verdadera de escribir nuestros programas. Otorgado sobre nosotros por el único Dios verdadero de la programación a sí mismo… o algo así.
Hasta que… ya no lo es, y la gente comienza a sucumbir bajo el peso de las abstracciones y el complejo gráfico de objetos mutables compartidos de manera promiscua. Se dedica un tiempo precioso y una capacidad intelectual a pensar en “abstracciones” y “patrones de diseño” en lugar de resolver problemas del mundo real.
Mucha gente ha criticado la programación orientada a objetos, incluidos ingenieros de software muy destacados. ¡Diablos, incluso el inventor de la programación orientada a objetos es un conocido crítico de la programación orientada a objetos moderna!
El objetivo final de todo desarrollador de software debería ser escribir código confiable. Nada más importa si el código tiene errores y no es confiable. ¿Y cuál es la mejor manera de escribir código que sea confiable? Sencillez. La simplicidad es lo opuesto a la complejidad. Por lo tanto, nuestra primera y principal responsabilidad como desarrolladores de software debería ser reducir la complejidad del código.
Descargo de responsabilidad
Seré honesto, no soy un fanático de la orientación a objetos. Por supuesto, este artículo estará sesgado. Sin embargo, tengo buenas razones para que no me guste la programación orientada a objetos.
También entiendo que la crítica a la programación orientada a objetos es un tema muy delicado; probablemente ofenderé a muchos lectores. Sin embargo, estoy haciendo lo que creo que es correcto. Mi objetivo no es ofender, sino crear conciencia sobre los problemas que presenta la POO.
No estoy criticando la programación orientada a objetos de Alan Kay, es un genio. Ojalá la programación orientada a objetos se implementara de la forma en que lo diseñó. Critico el enfoque moderno de Java / C# para la programación orientada a objetos.
Creo que no es correcto que la programación orientada a objetos sea considerada el estándar de facto para la organización de códigos por muchas personas, incluidas las que ocupan puestos técnicos de alto nivel. También es inaceptable que muchos lenguajes convencionales no ofrezcan ninguna otra alternativa a la organización del código que no sea programación orientada a objetos.
Demonios, yo solía luchar mucho mientras trabajaba en proyectos de programación orientada a objetos. Y no tenía ni idea de por qué estaba luchando tanto. ¿Quizás no fui lo suficientemente bueno? ¡Tenía que aprender un par de patrones de diseño más (pensé)! Finalmente, me quedé completamente agotado.
Esta publicación resume mi viaje de primera mano de una década desde la programación orientada a objetos a la programación funcional. Desafortunadamente, no importa cuánto lo intente, ya no puedo encontrar casos de uso para la programación orientada a objetos. Personalmente, he visto fallar proyectos de programación orientada a objetos porque se vuelven demasiado complejos para mantenerlos.
TLDR
Los programas orientados a objetos se ofrecen como alternativas a los correctos …
– Edsger W. Dijkstra, pionero de la informática
La programación orientada a objetos se ha creado con un objetivo en mente: gestionar la complejidad de las bases de código de procedimientos. En otras palabras, se suponía que mejoraría la organización del código. No hay evidencia objetiva y abierta de que la programación orientada a objetos sea mejor que la programación simple de procedimientos.
La amarga verdad es que la programación orientada a objetos falla en la única tarea que se pretendía abordar. Se ve bien en papel: tenemos jerarquías limpias de animales, perros, humanos, etc. Sin embargo, se desmorona una vez que la complejidad de la aplicación comienza a aumentar. En lugar de reducir la complejidad, fomenta el intercambio promiscuo del estado mutable e introduce una complejidad adicional con sus numerosos patrones de diseño. La programación orientada a objetos hace que las prácticas de desarrollo comunes, como la refactorización y las pruebas, sean innecesariamente difíciles.
Algunos pueden no estar de acuerdo conmigo, pero la verdad es que la programación orientada a objetos Java / C# moderno nunca se ha diseñado correctamente. Nunca salió de una institución de investigación adecuada (a diferencia de Haskell / FP). El cálculo lambda ofrece una base teórica completa para la programación funcional. OOP no tiene nada que ver con eso.
El uso de programación orientada a objetos es aparentemente inocente a corto plazo, especialmente en proyectos totalmente nuevos. Pero, ¿cuáles son las consecuencias a largo plazo de usar la programación orientada a objetos? la POO es una bomba de tiempo, lista para explotar en el futuro cuando el código base sea lo suficientemente grande.
Los proyectos se retrasan, los plazos se pierden, los desarrolladores se agotan y agregar nuevas funciones se vuelve casi imposible. La organización etiqueta la base de código como la “base de código heredada” y el equipo de desarrollo planea una reescritura.
La POO no es natural para el cerebro humano, nuestro proceso de pensamiento se centra en “hacer” cosas: salir a caminar, hablar con un amigo, comer pizza. Nuestros cerebros han evolucionado para hacer cosas, no para organizar el mundo en jerarquías complejas de objetos abstractos.
El código POO no es determinista; a diferencia de la programación funcional, no tenemos la garantía de obtener la misma salida con las mismas entradas. Esto dificulta mucho razonar sobre el programa. Como ejemplo muy simplificado, la salida de 2 + 2 o calculator. Add (2, 2) es mayoritariamente igual a cuatro, pero a veces puede llegar a ser igual a tres, cinco e incluso 1004. Las dependencias del objeto Calculator pueden cambiar el resultado del cálculo de formas sutiles pero profundas.
La necesidad de un marco resiliente
Los buenos programadores escriben buen código, los malos programadores escriben mal código, sin importar el paradigma de programación. Sin embargo, el paradigma de la programación debería impedir que los malos programadores hagan demasiado daño. Por supuesto, este no eres tú, ya que estás leyendo este artículo y te esfuerzas por aprender. Los malos programadores nunca tienen tiempo para aprender, solo presionan botones aleatorios en el teclado como locos. Te guste o no, estarás trabajando con malos programadores, algunos de ellos serán realmente malos. Y, desafortunadamente, la programación orientada a objetos no tiene suficientes restricciones para evitar que los programadores malos hagan demasiado daño.
No me considero un mal programador, pero ni siquiera yo soy capaz de escribir un buen código sin un marco sólido en el que basar mi trabajo. Sí, hay marcos que se preocupan por algunos problemas muy particulares (por ejemplo, Angular o ASP.Net).
No me refiero a los marcos de software. Me refiero a la definición de diccionario más abstracta de un marco: “una estructura de soporte esencial” – marcos que se preocupan por las cosas más abstractas como la organización del código y abordar la complejidad del código. Aunque la programación funcional y la orientada a objetos son paradigmas de programación, también son marcos de muy alto nivel.
Limitando nuestras opciones
C ++ es un lenguaje [orientado a objetos] horrible … Y limitar su proyecto a C significa que la gente no arruina las cosas con ningún “modelo de objeto” idiota c & @ p.
– Linus Torvalds, el creador de Linux
Linus Torvalds es ampliamente conocido por sus críticas abiertas a C++ y POO. Una cosa en la que tenía toda la razón es en limitar a los programadores en las elecciones que pueden hacer. De hecho, cuantas menos opciones tengan los programadores, más resistente se vuelve su código. En la cita anterior, Linus Torvalds recomienda encarecidamente tener un buen marco en el que basar nuestro código.
A muchos no les gustan los límites de velocidad en las carreteras, pero son esenciales para ayudar a evitar que las personas mueran por accidentes. Del mismo modo, un buen marco de programación debería proporcionar mecanismos que nos impidan hacer cosas estúpidas.
Un buen marco de programación nos ayuda a escribir código confiable. En primer lugar, debería ayudar a reducir la complejidad al proporcionar lo siguiente:
- Modularidad y reutilización
- Aislamiento de estado adecuado
- Alta relación señal-ruido
Desafortunadamente, la programación orientada a objetos proporciona a los desarrolladores demasiadas herramientas y opciones, sin imponer los tipos adecuados de limitaciones. Aunque OOP promete abordar la modularidad y mejorar la reutilización, no cumple sus promesas (más sobre esto más adelante). El código POO fomenta el uso del estado mutable compartido, que se ha demostrado una y otra vez que no es seguro. La programación orientada a objetos normalmente requiere una gran cantidad de código repetitivo (baja relación señal / ruido).
Programación funcional
¿Qué es exactamente la programación funcional? Algunas personas lo consideran un paradigma de programación muy complicado que solo es aplicable en la academia y no es adecuado para el “mundo real”. ¡Esto no podría estar más lejos de la verdad!
Sí, la programación funcional tiene una sólida base matemática y tiene sus raíces en el cálculo lambda. Sin embargo, la mayoría de sus ideas surgieron como respuesta a las debilidades de los lenguajes de programación más convencionales. Las funciones son la abstracción central de la programación funcional. Cuando se utilizan correctamente, las funciones proporcionan un nivel de modularidad y reutilización de código nunca visto en la programación orientada a objetos. Incluso presenta patrones de diseño que abordan los problemas de nulabilidad y proporciona una forma superior de manejo de errores.
Lo único que hace realmente bien la programación funcional es que nos ayuda a escribir software confiable. La necesidad de un depurador casi desaparece por completo. Sí, no es necesario revisar el código y ver las variables. Personalmente, no he tocado un depurador en mucho tiempo.
¿La mejor parte? Si ya sabe cómo utilizar las funciones, entonces ya es un programador funcional. ¡Solo necesita aprender a aprovechar al máximo esas funciones!
No estoy predicando Programación funcional, realmente no me importa qué paradigma de programación uses para escribir tu código. Simplemente estoy tratando de transmitir los mecanismos que proporciona la programación funcional para abordar los problemas inherentes a la programación POO / imperativa.
No entendemos POO del todo bien
Lamento haber acuñado hace mucho tiempo el término “objetos” para este tema porque hace que mucha gente se centre en la idea menor. La gran idea es la mensajería.
– Alan Kay, el inventor de POO
Erlang generalmente no se considera un lenguaje orientado a objetos. Pero probablemente Erlang sea el único lenguaje convencional orientado a objetos que existe. Sí, por supuesto, Smalltalk es un lenguaje de programación orientado a objetos adecuado; sin embargo, no se usa ampliamente. Tanto Smalltalk como Erlang hacen uso de la programación orientada a objetos de la forma en que fue concebido originalmente por su inventor, Alan Kay.
Mensajería
Alan Kay acuñó el término “Programación Orientada a Objetos” en la década de 1960. Tenía experiencia en biología y estaba intentando hacer que los programas de computadora se comunicaran de la misma manera que lo hacen las células vivas.
La gran idea de Alan Kay era que los programas independientes (células) se comunicaran enviándose mensajes entre sí. El estado de los programas independientes nunca se compartiría con el mundo exterior (encapsulación).
Eso es. La POO nunca tuvo la intención de tener cosas como herencia, polimorfismo, la palabra clave “nueva” y la miríada de patrones de diseño.
Programación Orientada a Objetos en su forma más pura
Erlang es POO en su forma más pura. A diferencia de los lenguajes más convencionales, se centra en la idea central de la programación orientada a objetos: la mensajería. En Erlang, los objetos se comunican pasando mensajes inmutables entre objetos.
¿Hay pruebas de que los mensajes inmutables sean un enfoque superior en comparación con las llamadas a métodos?
¡Oh sí! Erlang es probablemente el idioma más confiable del mundo. Impulsa la mayor parte de la infraestructura de telecomunicaciones del mundo (y, por tanto, de Internet). Algunos de los sistemas escritos en Erlang tienen una confiabilidad del 99,9999999% (lo leíste bien, nueve nueves).
Complejidad del código
Con los lenguajes de programación con inflexión OOP, el software de computadora se vuelve más detallado, menos legible, menos descriptivo y más difícil de modificar y mantener.
– Richard Mansfield
El aspecto más importante del desarrollo de software es mantener baja la complejidad del código. Período. Ninguna de las características sofisticadas importa si el código base se vuelve imposible de mantener. Incluso una cobertura de prueba del 100% no vale nada si la base de código se vuelve demasiado compleja y no se puede mantener.
¿Qué hace que la base de código sea compleja? Hay muchas cosas a considerar, pero en mi opinión, los principales infractores son: estado mutable compartido, abstracciones erróneas y baja relación señal-ruido (a menudo causada por código repetitivo). Todos ellos son frecuentes en la Programación Orientada a Objetos.
Los problemas del estado
¿Qué es el estado? En pocas palabras, el estado es cualquier dato temporal almacenado en la memoria. Piense en variables o campos / propiedades en POO. La programación imperativa (incluida la programación orientada a objetos) describe el cálculo en términos del estado del programa y los cambios a ese estado. En cambio, la programación declarativa (funcional) describe los resultados deseados y no especifica los cambios en el estado de forma explícita.
Estado mutable: el acto de hacer malabares mental
Creo que los grandes programas orientados a objetos luchan con una complejidad creciente a medida que construye este gran gráfico de objetos de objetos mutables. Ya sabes, tratando de entender y recordar qué pasará cuando llames a un método y cuáles serán los efectos secundarios.
– Rich Hickey, creador de Clojure
El estado en sí mismo es bastante inofensivo. Sin embargo, el estado mutable es el gran infractor. Especialmente si se comparte. ¿Qué es exactamente el estado mutable? Cualquier estado que pueda cambiar. Piense en variables o campos en OOP.
¡Ejemplo del mundo real, por favor!
Tiene una hoja de papel en blanco, escribe una nota en ella y terminas con la misma hoja de papel en un estado diferente (texto). Usted, efectivamente, ha mutado el estado de esa hoja de papel.
Eso está completamente bien en el mundo real, ya que a nadie más probablemente le importa ese trozo de papel. A menos que esta hoja de papel sea la pintura original de Mona Lisa.
Limitaciones del cerebro humano
¿Por qué el estado mutable es un problema tan grande? El cerebro humano es la máquina más poderosa del universo conocido. Sin embargo, nuestros cerebros son realmente malos para trabajar con el estado, ya que solo podemos almacenar alrededor de 5 elementos a la vez en nuestra memoria de trabajo. Es mucho más fácil razonar sobre un fragmento de código si solo piensa en lo que hace el código, no en las variables que cambia alrededor del código base.
Programar con estado mutable es un acto de malabarismo mental️. No sé ustedes, pero probablemente podría hacer malabarismos con dos pelotas. Deme tres o más bolas y ciertamente las dejaré caer todas. ¿Por qué entonces estamos tratando de realizar este acto de malabarismo mental todos los días en el trabajo?
Desafortunadamente, el malabarismo mental del estado mutable es el núcleo mismo de la POO. El único propósito de la existencia de métodos en un objeto es mutar ese mismo objeto.
Estado disperso
La Programación Orientada a Objetos empeora aún más el problema de la organización del código al dispersar el estado por todo el programa. El estado disperso se comparte promiscuamente entre varios objetos.
¡Ejemplo del mundo real, por favor!
Olvidemos por un segundo que todos somos adultos y finjamos que estamos tratando de armar una pieza genial de Lego.
Sin embargo, hay un problema: todas las partes de la pieza se mezclan al azar con partes de tus otros juguetes Lego. Y se han colocado en 50 cajas diferentes, nuevamente al azar. Y no se le permite agrupar las piezas de su pieza; debe recordar dónde están las diversas piezas y solo puede sacarlas una por una.
Sí, eventualmente ensamblará esta pieza, pero ¿cuánto tiempo le llevará?
¿Cómo se relaciona esto con la programación?
En la programación funcional, el estado suele estar aislado. Siempre sabes de dónde viene algún estado. El estado nunca se dispersa entre sus diferentes funciones. En POO, cada objeto tiene su propio estado y, al crear un programa, debe tener en cuenta el estado de todos los objetos con los que está trabajando actualmente.
Para hacernos la vida más fácil, es mejor que solo una pequeña parte del código base se ocupe del estado. Deje que las partes principales de su aplicación sean sin estado y puras. En realidad, esta es la razón principal del gran éxito del patrón de flujo en la interfaz (también conocido como Redux).
Estado promiscuamente compartido
Como si nuestras vidas no fueran lo suficientemente difíciles por tener un estado mutable disperso, ¡La programación orientada a objetos va un paso más allá!
Ejemplo del mundo real, ¡por favor!
El estado mutable en el mundo real casi nunca es un problema, ya que las cosas se mantienen privadas y nunca se comparten. Esta es la “encapsulación adecuada” en el trabajo. Imagínese a un pintor que está trabajando en la próxima pintura de Mona Lisa. Está trabajando solo en la pintura, termina y luego vende su obra maestra por millones.
Ahora, está aburrido de todo ese dinero y decide hacer las cosas de manera un poco diferente. Piensa que sería una buena idea hacer una fiesta de pintura. Invita a sus amigos elfos, Gandalf, el policía y un zombi para que lo ayuden. ¡Trabajo en equipo! Todos comienzan a pintar en el mismo lienzo al mismo tiempo. Por supuesto, no sale nada bueno: ¡la pintura es un completo desastre!
El estado mutable compartido no tiene sentido en el mundo real. Sin embargo, esto es exactamente lo que sucede en los programas de programación orientada a objetos: el estado se comparte de manera promiscua entre varios objetos y lo mutan de la forma que mejor les parezca. Esto, a su vez, hace que razonar sobre el programa sea cada vez más difícil a medida que el código base sigue creciendo.
Problemas de concurrencia
El intercambio promiscuo del estado mutable en el código POO hace que la paralelización de dicho código sea casi imposible. Se han inventado mecanismos complejos para abordar este problema. Se han inventado el bloqueo de subprocesos, la exclusión mutua y muchos otros mecanismos. Por supuesto, estos enfoques complejos tienen sus propios inconvenientes: puntos muertos, falta de componibilidad, depurar código de subprocesos múltiples es muy difícil y requiere mucho tiempo. Ni siquiera me refiero a la mayor complejidad causada por el uso de tales mecanismos de concurrencia.
No todo el estado es malvado
¿Todo el estado es malo? No, ¡el estado de Alan Kay probablemente no sea malo! La mutación de estado probablemente esté bien si está verdaderamente aislada (no la “forma OOP” aislada).
También está bien tener objetos de transferencia de datos inmutables. La clave aquí es “inmutable”. Luego, estos objetos se utilizan para pasar datos entre funciones.
Sin embargo, estos objetos también harían que los métodos y propiedades de POO sean completamente redundantes. ¿De qué sirve tener métodos y propiedades en un objeto si no se puede mutar?
La mutabilidad es inherente a la programación orientada a objetos
Algunos podrían argumentar que el estado mutable es una elección de diseño en POO, no una obligación. Hay un problema con esa declaración. No es una elección de diseño, pero es prácticamente la única opción. Sí, se pueden pasar objetos inmutables a métodos en Java / C #, pero esto rara vez se hace ya que la mayoría de los desarrolladores utilizan la mutación de datos por defecto. Incluso si los desarrolladores intentan hacer un uso adecuado de la inmutabilidad en sus programas de programación orientada a objetos, los lenguajes no proporcionan mecanismos integrados para la inmutabilidad y para trabajar eficazmente con datos inmutables (es decir, estructuras de datos persistentes).
Sí, podemos asegurarnos de que los objetos se comuniquen solo pasando mensajes inmutables y nunca pasen referencias (lo que rara vez se hace). Tales programas serían más confiables que la programación orientada a objetos convencional. Sin embargo, los objetos todavía tienen que mutar su propio estado una vez que se ha recibido un mensaje. Un mensaje es un efecto secundario y su único propósito es provocar cambios. Los mensajes serían inútiles si no pudieran cambiar el estado de otros objetos.
Es imposible hacer uso de POO sin causar mutaciones de estado.