La historia olvidada de la POO

Nota: Esto es parte de la serie «Componiendo Software» (¡ahora un libro!) sobre el aprendizaje de la programación funcional y las técnicas de composición de software en JavaScript ES6+ desde cero. Manténgase en sintonía. ¡Hay mucho más de esto por venir!
Comprar el libro | Índice | < Anterior | Siguiente >

Los paradigmas de programación funcional e imperativa que utilizamos hoy en día fueron explorados por primera vez matemáticamente en la década de 1930 con el cálculo lambda y la máquina de Turing, que son formulaciones alternativas de la computación universal (sistemas formalizados que pueden realizar computación general). La Tesis de Church Turing demostró que el cálculo lambda y las máquinas de Turing son funcionalmente equivalentes, es decir, que todo lo que se puede calcular con una máquina de Turing se puede calcular con el cálculo lambda, y viceversa.

Nota: Hay una idea errónea de que las máquinas de Turing pueden calcular cualquier cosa computable. Hay clases de problemas (por ejemplo, el problema de detención) que pueden ser computables para algunos casos, pero no son generalmente computables para todos los casos utilizando máquinas de Turing. Cuando uso la palabra «computable» en este texto, me refiero a «computable por una máquina de Turing».

El cálculo Lambda representa un enfoque descendente, de aplicación de funciones a la computación, mientras que la formulación de la máquina de Turing representa un enfoque ascendente, imperativo (paso a paso) a la computación.

Los lenguajes de bajo nivel, como el código máquina y el ensamblador, aparecieron en la década de 1940 y, a finales de la década de 1950, aparecieron los primeros lenguajes populares de alto nivel. Los dialectos de Lisp siguen siendo de uso común hoy en día, incluyendo Clojure, Scheme, AutoLISP, etc. FORTRAN y COBOL aparecieron en la década de 1950 y son ejemplos de lenguajes imperativos de alto nivel que todavía se utilizan hoy en día, aunque los lenguajes de la familia C han sustituido tanto a COBOL como a FORTRAN para la mayoría de las aplicaciones.

Tanto la programación imperativa como la funcional tienen sus raíces en las matemáticas de la teoría de la computación, anteriores a los ordenadores digitales. La «Programación Orientada a Objetos» (POO) fue acuñada por Alan Kay alrededor de 1966 o 1967, mientras cursaba sus estudios de posgrado.

La aplicación seminal Sketchpad de Ivan Sutherland fue una de las primeras inspiraciones de la POO. Fue creada entre 1961 y 1962 y publicada en su Tesis de Sketchpad en 1963. Los objetos eran estructuras de datos que representaban imágenes gráficas mostradas en la pantalla de un osciloscopio, y presentaban herencia a través de delegados dinámicos, que Ivan Sutherland denominó «maestros» en su tesis. Cualquier objeto podía convertirse en un «maestro», y las instancias adicionales de los objetos se denominaban «ocurrencias». Los maestros de Sketchpad tienen mucho en común con la herencia prototípica de JavaScript.

Nota: El TX-2 del Laboratorio Lincoln del MIT fue uno de los primeros usos de un monitor gráfico de ordenador que empleaba la interacción directa con la pantalla mediante un lápiz óptico. El EDSAC, que estuvo operativo entre 1948 y 1958, podía mostrar gráficos en una pantalla. El Whirlwind del MIT tenía una pantalla de osciloscopio que funcionaba en 1949. La motivación del proyecto era crear un simulador de vuelo general capaz de simular la retroalimentación de los instrumentos de varios aviones. Esto condujo al desarrollo del sistema informático SAGE. El TX-2 fue un ordenador de prueba para SAGE.

El primer lenguaje de programación ampliamente reconocido como «orientado a objetos» fue Simula, especificado en 1965. Al igual que Sketchpad, Simula presentaba objetos, y con el tiempo introdujo las clases, la herencia de clases, las subclases y los métodos virtuales.

Nota: Un método virtual es un método definido en una clase que está diseñado para ser sobrescrito por subclases. Los métodos virtuales permiten a un programa llamar a métodos que pueden no existir en el momento en que se compila el código, empleando el envío dinámico para determinar qué método concreto invocar en tiempo de ejecución. JavaScript presenta tipos dinámicos y utiliza la cadena de delegación para determinar qué métodos invocar, por lo que no necesita exponer el concepto de métodos virtuales a los programadores. Dicho de otro modo, todos los métodos en JavaScript utilizan el envío de métodos en tiempo de ejecución, por lo que los métodos en JavaScript no necesitan ser declarados «virtuales» para soportar la característica.

«Yo inventé el término ‘orientado a objetos’, y puedo decir que no tenía a C++ en mente.» ~ Alan Kay, OOPSLA ’97

Alan Kay acuñó el término «programación orientada a objetos» en la escuela de posgrado en 1966 o 1967. La gran idea era utilizar minicomputadoras encapsuladas en el software que se comunicaban a través del paso de mensajes en lugar de compartir datos directamente – para dejar de dividir los programas en «estructuras de datos» y «procedimientos» separados.

«El principio básico del diseño recursivo es hacer que las partes tengan la misma potencia que el todo». ~ Bob Barton, el principal diseñador del B5000, un mainframe optimizado para ejecutar Algol-60.

Smalltalk fue desarrollado por Alan Kay, Dan Ingalls, Adele Goldberg y otros en Xerox PARC. Smalltalk estaba más orientado a objetos que Simula – todo en Smalltalk es un objeto, incluyendo clases, enteros y bloques (cierres). El Smalltalk-72 original no incluía subclases. Eso fue introducido en Smalltalk-76 por Dan Ingalls.

Si bien Smalltalk soportaba clases y eventualmente subclases, Smalltalk no se trataba de clases o subclases de cosas. Era un lenguaje funcional inspirado en Lisp y en Simula. Alan Kay considera que el enfoque de la industria en la subclasificación es una distracción de los verdaderos beneficios de la programación orientada a objetos.

«Siento haber acuñado hace tiempo el término «objetos» para este tema porque consigue que mucha gente se centre en la idea menor. La gran idea es la mensajería.»
~ Alan Kay

En un intercambio de correos electrónicos de 2003, Alan Kay aclaró lo que quería decir cuando llamaba a Smalltalk «orientado a objetos»:

«OOP para mí significa sólo mensajería, retención local y protección y ocultación del estado-proceso, y vinculación tardía extrema de todas las cosas.»
~ Alan Kay

En otras palabras, según Alan Kay, los ingredientes esenciales de la POO son:

  • Paso de mensajes
  • Encapsulación
  • Enlace dinámico

Notablemente, la herencia y el polimorfismo de subclase NO fueron considerados ingredientes esenciales de la POO por Alan Kay, el hombre que acuñó el término y llevó la POO a las masas.

La esencia de la POO

La combinación del paso de mensajes y la encapsulación sirven para algunos propósitos importantes:

  • Evitar el estado mutable compartido encapsulando el estado y aislando a otros objetos de los cambios de estado locales. La única forma de afectar al estado de otro objeto es pedir (no ordenar) a ese objeto que lo cambie enviando un mensaje. Los cambios de estado se controlan a nivel local y celular, en lugar de estar expuestos al acceso compartido.
  • Desacoplamiento de los objetos entre sí: el emisor de mensajes sólo está débilmente acoplado al receptor de mensajes, a través de la API de mensajería.
  • Adaptabilidad y resistencia a los cambios en tiempo de ejecución a través de la vinculación tardía. La adaptabilidad en tiempo de ejecución proporciona muchas grandes ventajas que Alan Kay consideraba esenciales para la POO.
  • Estas ideas se inspiraron en las células biológicas y/o en los ordenadores individuales de una red a través de la formación de Alan Kay en biología y la influencia del diseño de Arpanet (una versión temprana de Internet). Incluso en ese momento, Alan Kay imaginó un software que se ejecutaba en un ordenador gigante y distribuido (Internet), en el que los ordenadores individuales actuaban como células biológicas, operando de forma independiente en su propio estado aislado, y comunicándose mediante el paso de mensajes.

    «Me di cuenta de que la metáfora de la célula/ordenador completo se desharía de los datos»
    ~ Alan Kay

    Al decir «deshacerse de los datos», Alan Kay seguramente era consciente de los problemas de estado mutable compartido y del estrecho acoplamiento provocado por los datos compartidos, temas comunes hoy en día.

    Pero a finales de la década de 1960, los programadores de ARPA se sentían frustrados por la necesidad de elegir una representación del modelo de datos para sus programas antes de construir el software. Los procedimientos que estaban demasiado acoplados a estructuras de datos particulares no eran resistentes al cambio. Querían un tratamiento más homogéneo de los datos.

    «el objetivo de la POO es no tener que preocuparse por lo que hay dentro de un objeto. Los objetos hechos en diferentes máquinas y con diferentes lenguajes deben ser capaces de hablar entre sí » ~ Alan Kay

    Los objetos pueden abstraer y ocultar las implementaciones de las estructuras de datos. La implementación interna de un objeto podría cambiar sin romper otras partes del sistema de software. De hecho, con una vinculación tardía extrema, un sistema informático completamente diferente podría asumir las responsabilidades de un objeto, y el software podría seguir funcionando. Los objetos, por su parte, podrían exponer una interfaz estándar que funcionara con cualquier estructura de datos que el objeto utilizara internamente. La misma interfaz podría funcionar con una lista enlazada, un árbol, un flujo, y así sucesivamente.

    Alan Kay también vio los objetos como estructuras algebraicas, que hacen ciertas garantías matemáticamente demostrables sobre sus comportamientos:

    «Mi formación matemática me hizo darme cuenta de que cada objeto podría tener varias álgebras asociadas a él, y que podría haber familias de éstas, y que éstas serían muy muy útiles.»
    ~ Alan Kay

    Esto ha demostrado ser cierto, y constituye la base de objetos como las promesas y las lentes, ambos inspirados en la teoría de categorías.

    La naturaleza algebraica de la visión de Alan Kay para los objetos permitiría que los objetos permitieran verificaciones formales, un comportamiento determinista y una mejor comprobabilidad, porque las álgebras son esencialmente operaciones que obedecen a unas pocas reglas en forma de ecuaciones.

    En la jerga de los programadores, las álgebras son como abstracciones formadas por funciones (operaciones) acompañadas de leyes específicas impuestas por pruebas unitarias que esas funciones deben pasar (axiomas/ecuaciones).

    Estas ideas fueron olvidadas durante décadas en la mayoría de los lenguajes OO de la familia C, incluyendo C++, Java, C#, etc., pero están empezando a encontrar su camino de vuelta en las versiones recientes de los lenguajes OO más utilizados.

    Se podría decir que el mundo de la programación está redescubriendo los beneficios de la programación funcional y el pensamiento razonado en el contexto de los lenguajes OO.

    Al igual que JavaScript y Smalltalk antes, la mayoría de los lenguajes OO modernos se están convirtiendo cada vez más en «lenguajes multiparadigma». No hay ninguna razón para elegir entre la programación funcional y la POO. Cuando miramos la esencia histórica de cada uno, no sólo son compatibles, sino que son ideas complementarias.

    Debido a que comparten tantas características en común, me gusta decir que JavaScript es la venganza de Smalltalk contra la incomprensión del mundo de la POO. Both Smalltalk and JavaScript support:

    • Objects
    • First-class functions and closures
    • Dynamic types
    • Late binding (functions/methods changeable at runtime)
    • OOP without class inheritance

    What is essential to OOP (according to Alan Kay)?

    • Encapsulation
    • Message passing
    • Dynamic binding (the ability for the program to evolve/adapt at runtime)

    What is non-essential?

    • Classes
    • Class inheritance
    • Special treatment for objects/functions/data
    • The new keyword
    • Polymorphism
    • Static types
    • Recognizing a class as a «type»

    If your background is Java or C#, you may be thinking static types and Polymorphism are essential ingredients, but Alan Kay preferred dealing with generic behaviors in algebraic form. For example, from Haskell:

fmap :: (a -> b) -> f a -> f b

This is the functor map signature, which acts generically over unspecified types a and b, applying a function from a to b in the context of a functor of a to produce a functor of b. Functor is math jargon that essentially means «supporting the map operation». If you’re familiar with .map() in JavaScript, you already know what that means.

Here are two examples in JavaScript:

// isEven = Number => Boolean
const isEven = n => n % 2 === 0;const nums = ;// map takes a function `a => b` and an array of `a`s (via `this`)
// and returns an array of `b`s.
// in this case, `a` is `Number` and `b` is `Boolean`
const results = nums.map(isEven);console.log(results);
//

The .map() method is generic in the sense that a and b can be any type, and .map() handles it just fine because arrays are data structures that implement the algebraic functor laws. Los tipos no le importan a .map() porque no intenta manipularlos directamente, sino que aplica una función que espera y devuelve los tipos correctos para la aplicación.

// matches = a => Boolean
// here, `a` can be any comparable type
const matches = control => input => input === control;const strings = ;const results = strings.map(matches('bar'));console.log(results);
//

Esta relación de tipos genéricos es difícil de expresar correctamente y a fondo en un lenguaje como TypeScript, pero era bastante fácil de expresar en los tipos Hindley Milner de Haskell con soporte para tipos de tipo superior (tipos de tipos).

La mayoría de los sistemas de tipos han sido demasiado restrictivos para permitir la libre expresión de ideas dinámicas y funcionales, como la composición de funciones, la composición libre de objetos, la extensión de objetos en tiempo de ejecución, los combinadores, los objetivos, etc. En otras palabras, los tipos estáticos con frecuencia dificultan la escritura de software componible.

Si tu sistema de tipos es demasiado restrictivo (por ejemplo, TypeScript, Java), te ves obligado a escribir un código más enrevesado para lograr los mismos objetivos. Eso no significa que los tipos estáticos sean una mala idea, o que todas las implementaciones de tipos estáticos sean igualmente restrictivas. He encontrado muchos menos problemas con el sistema de tipos de Haskell.

Si eres un fanático de los tipos estáticos y no te importan las restricciones, más poder para ti, pero si encuentras algunos de los consejos de este texto difíciles porque es difícil tipar funciones compuestas y estructuras algebraicas compuestas, culpa al sistema de tipos, no a las ideas. A la gente le encanta la comodidad de sus todoterrenos, pero nadie se queja de que no le permitan volar. Para eso, necesitas un vehículo con más grados de libertad.

Si las restricciones hacen que tu código sea más sencillo, ¡genial! Pero si las restricciones te obligan a escribir un código más complicado, tal vez las restricciones sean erróneas.

¿Qué es un objeto?

Está claro que los objetos han adquirido muchas connotaciones a lo largo de los años. Lo que llamamos «objetos» en JavaScript son simplemente tipos de datos compuestos, sin ninguna de las implicaciones de la programación basada en clases o el paso de mensajes de Alan Kay.

En JavaScript, esos objetos pueden y frecuentemente soportan la encapsulación, el paso de mensajes, la compartición de comportamientos a través de métodos, incluso el polimorfismo de subclases (aunque utilizando una cadena de delegación en lugar de un envío basado en tipos). Puedes asignar cualquier función a cualquier propiedad. Puedes construir comportamientos de objetos dinámicamente y cambiar el significado de un objeto en tiempo de ejecución. JavaScript también admite la encapsulación mediante cierres para la privacidad de la implementación. Pero todo eso es un comportamiento opcional.

Nuestra idea actual de un objeto es simplemente una estructura de datos compuesta, y no requiere nada más para ser considerado un objeto. Pero programar usando este tipo de objetos no hace que tu código sea «orientado a objetos» más de lo que programar con funciones hace que tu código sea «funcional».

La POO ya no es POO real

Debido a que «objeto» en los lenguajes de programación modernos significa mucho menos de lo que significaba para Alan Kay, estoy usando «componente» en lugar de «objeto» para describir las reglas de la POO real. Muchos objetos son poseídos y manipulados directamente por otro código en JavaScript, pero los componentes deben encapsular y controlar su propio estado.

La verdadera POO significa:

  • Programar con componentes (el «objeto» de Alan Kay)
  • El estado de los componentes debe estar encapsulado
  • Usar el paso de mensajes para la comunicación entre objetos
  • Los componentes pueden ser añadidos/cambiados/reemplazados en tiempo de ejecución
  • La mayoría de los comportamientos de los componentes pueden ser especificados genéricamente usando estructuras de datos algebraicas. La herencia no es necesaria aquí. Los componentes pueden reutilizar los comportamientos de las funciones compartidas y las importaciones modulares sin compartir sus datos.

    Manipular objetos o utilizar la herencia de clases en JavaScript no significa que estés «haciendo POO». Usar componentes de esta manera sí. Pero el uso popular es la forma en que se definen las palabras, así que tal vez deberíamos abandonar la POO y llamar a esto «Programación Orientada a Mensajes (POM)» en lugar de «Programación Orientada a Objetos (POO)»?

    ¿Es una coincidencia que las fregonas se utilicen para limpiar el desorden?

    Cómo es una buena POM

    En la mayoría del software moderno, hay alguna interfaz de usuario responsable de la gestión de las interacciones del usuario, algún código que gestiona el estado de la aplicación (datos del usuario), y el código que gestiona el sistema o la red de E/S.

    Cada uno de estos sistemas puede requerir procesos de larga duración, como escuchadores de eventos, estado para realizar un seguimiento de cosas como la conexión de red, el estado de los elementos de la UI y el propio estado de la aplicación.

    Una buena MOP significa que en lugar de que todos estos sistemas lleguen y manipulen directamente el estado de los demás, el sistema se comunica con otros componentes a través del envío de mensajes. Cuando el usuario hace clic en un botón de guardar, un mensaje "SAVE" podría ser enviado, que un componente de estado de la aplicación podría interpretar y transmitir a un controlador de actualización de estado (como una función reductora pura). Tal vez después de que el estado se haya actualizado, el componente de estado podría enviar un mensaje "STATE_UPDATED" a un componente de interfaz de usuario, que a su vez interpretará el estado, reconciliará qué partes de la interfaz de usuario necesitan ser actualizadas, y transmitirá el estado actualizado a los subcomponentes que manejan esas partes de la interfaz de usuario.

    Mientras tanto, el componente de conexión de red podría estar monitoreando la conexión del usuario a otra máquina en la red, escuchando mensajes, y enviando representaciones de estado actualizadas para guardar los datos en una máquina remota. Internamente está manteniendo un seguimiento de un temporizador de latido de la red, si la conexión está actualmente en línea o fuera de línea, y así sucesivamente.

    Estos sistemas no necesitan saber acerca de los detalles de las otras partes del sistema. Sólo sobre sus preocupaciones individuales y modulares. Los componentes del sistema son descomponibles y recomponibles. Implementan interfaces estandarizadas para poder interoperar. Siempre que se satisfaga la interfaz, se pueden sustituir por otros que hagan lo mismo de forma diferente, o cosas completamente distintas con los mismos mensajes. Incluso puede hacerlo en tiempo de ejecución, y todo seguiría funcionando correctamente.

    Los componentes del mismo sistema de software ni siquiera necesitan estar ubicados en la misma máquina. El sistema podría estar descentralizado. El almacenamiento en red podría repartir los datos a través de un sistema de almacenamiento descentralizado como IPFS, de modo que el usuario no dependa de la salud de ninguna máquina en particular para garantizar que sus datos estén respaldados de forma segura, y a salvo de los hackers que podrían querer robarlos.

    OOP se inspiró parcialmente en Arpanet, y uno de los objetivos de Arpanet era construir una red descentralizada que pudiera ser resistente a ataques como las bombas atómicas. Según el director de DARPA durante el desarrollo de Arpanet, Stephen J. Lukasik («Why the Arpanet Was Built»):

    «El objetivo era explotar las nuevas tecnologías informáticas para satisfacer las necesidades de mando y control militar contra las amenazas nucleares, lograr un control de supervivencia de las fuerzas nucleares estadounidenses y mejorar la toma de decisiones tácticas y de gestión militar.»

    Nota: El principal impulso de Arpanet fue la comodidad y no la amenaza nuclear, y sus evidentes ventajas en materia de defensa surgieron más tarde. ARPA utilizaba tres terminales informáticos distintos para comunicarse con tres proyectos de investigación informática distintos. Bob Taylor quería una única red informática que conectara cada proyecto con los demás.

    Un buen sistema MOP podría compartir la robustez de Internet utilizando componentes que fueran intercambiables en caliente mientras la aplicación estuviera funcionando. Podría seguir funcionando si el usuario está en un móvil y se desconecta porque ha entrado en un túnel. Podría seguir funcionando si un huracán deja sin electricidad a uno de los centros de datos donde se encuentran los servidores.

    Es hora de que el mundo del software deje de lado el fallido experimento de la herencia de clases, y adopte los principios matemáticos y científicos que originalmente definieron el espíritu de la POO.

    Es hora de que empecemos a construir un software más flexible, más resistente y mejor compuesto, con la POM y la programación funcional trabajando en armonía.

    Nota: El acrónimo MOP ya se utiliza para describir la «programación orientada a la monitorización» y es poco probable que la POO vaya a desaparecer tranquilamente.

    No te enfades si MOP no se pone de moda como jerga de programación.
    Haz MOP en tus OOPs.

    Aprende más en EricElliottJS.com

    Las lecciones de vídeo sobre programación funcional están disponibles para los miembros de EricElliottJS.com. Si no eres miembro, regístrate hoy.