Functional thinking: Acoplamiento y composición, Parte 2

Bloques de construcción orientados por objeto vs. funcionales

Los programadores acostumbrados a usar los bloques de construcción de orientación de objetos (herencia, polimorfismo, etc.) pueden estar ciegos frente a sus fallas y a sus alternativas. La programación funcional usa diferentes bloques de construcción para lograr la reutilización, con base en conceptos de propósito general, como listar transformaciones y código portable. Esta entrega de Functional thinking compara al acoplamiento vía herencia con la composición como mecanismos de reutilización, y apunta a una de las diferencias clave entre la programación imperativa y la funcional.

Neal Ford, Application Architect, ThoughtWorks Inc.

Photo of Neal FordNeal Ford es arquitecto de software y Meme Wrangler en ThoughtWorks, una consultora global en TI. También diseña y desarrolla aplicaciones, materiales instructivos, artículos para revistas, courseware, y presentaciones en video/DVD, y es el autor o editor de libros que abarcan una variedad de tecnologías, incluyendo las más recientes The Productive Programmer. Él se enfoca en el diseño y construcción de aplicaciones corporativas a gran escala. También es un conferencista aclamado internacionalmente en conferencias de desarrolladores en todo el mundo. Visite su sitio web.



26-03-2012

Sobre esta serie

Esta serie tiene por objeto reorientar su perspectiva hacia una forma de pensar funcional, ayudándole a observar problemas comunes de nuevas formas y a encontrar formas para mejorar su codificación del día a día. Este explora conceptos de programación funcional, infraestructuras que permiten programación funcional dentro del lenguaje Java, lenguajes de programación que se ejecutan en la JVM, y algunas indicaciones de cara al futuro sobre diseño de lenguaje. Esta serie está articulada para desarrolladores que conozcan Java y cómo funcionan sus abstracciones, pero que tengan poca o ninguna experiencia usando un lenguaje funcional.

En la última entrega, ilustré las diferentes variedades de reutilización de código. En la versión orientada por objeto, extraje métodos duplicados, moviéndolos hacia una súper clase junto con un campo protegido . En la versión funcional, extraje las funciones puras (aquellas que no tienen efectos secundarios) en su propia clase, llamándolas suministrando valores de parámetros. Cambié el mecanismo de reutilización de campo protegido mediante herencia a parámetros de método. Los recursos (como la herencia) que comprenden los lenguajes orientados por objeto tienen beneficios claros, pero también pueden tener efectos secundarios inadvertidos. Como algunos lectores han comentado acertadamente, muchos desarrolladores OOP experimentados han aprendido a no compartir estado vía herencia por esa misma razón. Pero si su paradigma profundamente arraigado es la orientación de objeto, algunas veces las alternativas son difíciles de ver.

En esta entrega, contrastaré el acoplamiento mediante mecanismos de lenguaje con composición más código portable como una forma de extraer código reutilizable — lo cual también sirve para descubrir una diferencia filosófica sobre la reutilización de código. Primero, volveré a observar un problema clásico: cómo escribir un método equals() adecuado en la presencia de herencia.

El método equals() vuelto a observar

El libro de Joshua Bloch Effective Java incluye una sección sobre cómo esculpir métodos equals() y hashCode() (vea Recursos). La complicación surge de la interacción entre semántica de igualdad y herencia. El método equals() en Java debe adherirse a las características especificadas por el Javadoc para Object.equals():

  • Es reflexivo: para cualquier valor de referencia x no nulo, x.equals(x) debe retornar verdadero.
  • Es simétrico: para cualquier valor de referencia x e y no nulos, x.equals(y) debe retornar verdadero si y solo si y.equals(x) retorna verdadero.
  • Es transitivo: para cualquier valor de referencia x, y y z no nulos, si x.equals(y) retorna verdadero y y.equals(z) retorna verdadero, entonces x.equals(z) debe retornar verdadero.
  • Es consistente: para cualquier valor de referencia que no sea nulo, múltiples invocaciones de x.equals(y) retornan consistentemente verdadero o consistentemente falso, siempre y cuando no se modifique la información que se haya usado en comparaciones iguales sobre los objetos.
  • Para cualquier valor de referencia x, x.equals(null) debe retornar falso.

En su ejemplo, Bloch crea dos clases — una Point y una ColorPoint — e intenta crear un método equals() que funciona adecuadamente para ambos. Intentar ignorar el campo adicional en la clase heredada rompe la simetría, e intentar tenerlo en cuenta rompe la transitividad. Josh Bloch ofrece un diagnóstico grave para este problema:

Simplemente no hay forma de extender una clase instanciable y añadir un aspecto mientras se preserva el contrato de igualdad.

Implementar la equidad es mucho más simple cuando usted no necesita preocuparse por campos cambiantes heredados. Añadir mecanismos de acoplamiento como herencia crea matices y trampas sutiles. (Resulta que existe una forma para resolver este problema que retiene la herencia, pero bajo el costo de añadir un método dependiente adicional. Consulte la Inheritance y la barra lateral canEqual() ).

Inheritance y canEqual()

En Programming Scala, los autores proporcionan un mecanismo que permite la igualdad para trabajar incluso en la presencia de herencia (vea Recursos). La raíz del problema que Bloch discute es que las clases padres no "saben" lo suficiente sobre las subclases como para determinar si deben participar en una comparación de igualdad. Para resolver esto, usted añade un método canEqual() a la clase base y lo sobrescribe para clases hijas para las que desee comparaciones de igualdad. Esto permite a la clase actual (vía canEqual()) decidir si es razonable y sensible igualar dos tipos.

Este mecanismo resuelve el problema, pero con el costo de añadir incluso otro punto de acople entre clases padre e hijas mediante el método canEqual() .

Recuerde la cita de Michael Feathers que introdujo las dos entregas previas de esta serie:

La programación orientada por objetos hace que el código se pueda entender al encapsular partes móviles. La programación funcional hace que el código se pueda extender minimizando partes móviles.

La dificultad en la implementación de equals() ilustra la metáfora de partes móviles de Feathers. La herencia es un mecanismo de acople: reúne dos entidades con reglas bien definidas sobre visibilidad, despacho de método, etc. En lenguajes como Java, el polimorfismo también está atado a la herencia. Aquellos puntos de acople hacen de Java un lenguaje orientado por objeto. Pero permitir partes móviles tiene consecuencias, especialmente a nivel de lenguaje. Los helicópteros son notoriamente fáciles de volar porque existe un control para cada una de las cuatro extremidades del piloto. Mover un control afecta a los otros controles, por lo que el piloto debe convertirse en un experto para tratar con los efectos secundarios que cada control tiene sobre los otros. Las partes del lenguaje son como controles de un helicóptero: usted no puede añadirlos (o cambiarlos) fácilmente sin que afecten a todas las otras partes.

La herencia es una parte tan natural de los lenguajes orientados por objetos, que la mayoría de desarrolladores pierde de vista el hecho de que, en su esencia, es un mecanismo de acople. Cuando se presentan cosas extrañas o no funcionan, usted simplemente aprende las reglas (algunas veces misteriosas) para mitigar el problema y continuar. No obstante, esas reglas de acoplamiento implícitas afectan su forma de pensar sobre los aspectos fundamentales de su código, como la forma de lograr la reutilización, la extensibilidad y la equidad.

Effective Java probablemente no habría sido tan exitoso si Bloch hubiese dejado pendiente la cuestión de la igualdad. En lugar de ello, la utilizó como una oportunidad para volver a presentar el buen consejo de un punto previo en el libro: prefiera composición en lugar de herencia. La solución de Bloch del problema equals() usa composición en lugar de acople. Se abstiene completamente de la herencia, haciendo que ColorPoint posea una referencia hacia una instancia de Point en lugar de convertirse en un tipo de punto.


Composición y herencia

La composición — en la forma de parámetros pasados más funciones de primera clase — aparece frecuentemente en bibliotecas de programación funcional como un mecanismo de reutilización. Los lenguajes funcionales logran la reutilización a un nivel de grano más grueso que los lenguajes orientados por objeto, extrayendo maquinaria común con comportamiento parametrizado. Los sistemas orientados por objeto consisten en objetos que se comunican enviando mensajes a (o, más específicamente, ejecutando métodos en) otros objetos. La Figura 1 representa un sistema orientado por objeto:

Figura 1. Sistema orientado por objeto
Sistema orientado por objeto

Cuando usted descubre una colección útil de clases y sus mensajes correspondientes, usted extrae esa gráfica de clases para reutilización, como se muestra en la Figura 2:

Figura 2. Extrayendo piezas útiles de la gráfica
Extrayendo piezas útiles de la gráfica

No es de sorprender que uno de los libros más populares en el mundo de la ingeniería de software es Design Patterns: Elements of Reusable Object-Oriented Software (vea Recursos), un catálogo de exactamente el tipo de extracción mostrada en la Figura 2. La reutilización mediante patrones es tan ubicua que muchos otros libros también la catalogan (y proporcionan nombres diferentes para) tales extracciones. El movimiento de patrones de diseño ha sido una gran ayuda para el mundo del desarrollo de software porque proporciona nomenclatura y ejemplares. Pero, fundamentalmente, la reutilización mediante patrones de diseño es de grano fino: una solución (el patrón Flyweight) es ortogonal a otro (el patrón Memento). Cada uno de los problemas resueltos por los patrones de diseño es altamente específico, lo cual hace que los patrones sean útiles porque con frecuencia usted puede encontrar un patrón que coincida con su problema actual — pero que es apenas útil por ser tan específico para el problema.

Los programadores funcionales también desean código reutilizable, pero utilizan diferentes bloques de construcción. En lugar de intentar crear relaciones bien conocidas (acoplamiento) entre estructuras, la programación funcional intenta extraer mecanismos de reutilización de grano grueso— en parte con base en la teoría de categoría, una rama de las matemáticas que define las relaciones (morfismo) entre tipos de objetos (vea Recursos). La mayoría de las aplicaciones hacen cosas con listas de elementos, por lo que se construye un enfoque funcional para reutilizar mecanismos en torno a la idea de listas más código contextualizado y portable. Los lenguajes funcionales dependen de funciones de primera clase (funciones que pueden aparecer en cualquier otro lugar en donde cualquier otra compilación de lenguaje pueda aparecer) como parámetros y valores de retorno. La Figura 3 ilustra este concepto:

Figura 3. Reutilización mediante mecanismos de grano grueso más código portátil
Reutilización mediante mecanismos de grano grueso más código portátil

En la Figura 3, La caja de cambios representa abstracciones que de forma genérica tienen que ver con alguna estructura fundamental de datos, y la caja amarilla representa código portable que encapsula datos dentro de sí.


Bloques de construcción comunes

En la segunda entrega de esta serie, construí un ejemplo de un clasificador de números usando la biblioteca Functional Java (vea Recursos). Ese ejemplo usa tres bloques de construcción diferentes, pero sin explicación. Investigaré esos bloques de construcción ahora.

Folds

Uno de los métodos en el clasificador de números realiza una suma a lo largo de todos los factores reunidos Ese método aparece en el Listado 1:

Listado 1. El método sum() del clasificador funcional de números
public int sum(List<Integer> factors) {
    return factors.foldLeft(fj.function.Integers.add, 0);
}

En principio, no es obvio cómo el cuerpo de una línea del Listado 1 realiza una operación de suma. Este ejemplo es un tipo específico en la familia general de transformaciones de lista llamado catamorfismos — transformaciones de una forma hacia otra (vea Recursos). En este caso, la operaciónfold se refiere a una transformación que combina cada elemento de la lista con el siguiente, acumulando un solo resultado para toda la lista. Un fold left colapsa la lista hacia la izquierda, comenzando con un valor semilla y combinando cada elemento de la lista en turno, para producir un resultado final. La Figura 4 ilustra una operación fold:

Figura 4. Operación fold
Operación fold

Como la adición es acumulativa, no importa si usted hace un foldLeft() o un foldRight(). Pero algunas operaciones (incluyendo la resta y la división) tienen en cuenta el orden, por lo que existe el método simétricofoldRight() para manejar esos casos.

El Listado 1 usa la numeración add suministrada con Functional Java; esta incluye las operaciones matemáticas más comunes para usted. Pero ¿qué sucede con los casos en los que usted necesita criterios más refinados? Considere el ejemplo del Listado 2:

Listado 2. foldLeft() con criterios suministrados por el usuario
static public int addOnlyOddNumbersIn(List<Integer> numbers) {
    return numbers.foldLeft(new F2<Integer, Integer, Integer>() {
        public Integer f(Integer i1, Integer i2) {
            return (!(i2 % 2 == 0)) ? i1 + i2 : i1;
        }
    }, 0);
}

Debido a que Java todavía no tiene funciones de primera clase en forma de bloques lambda (vea Recursos), Functional Java es forzado a improvisar con los genéricos. La clase F2 incorporada tiene la estructura correcta para una operación antigua: crea un método que acepta dos parámetros enteros (estos son los dos valores que se pliegan uno sobre otro) y el tipo de retorno. El ejemplo del Listado 2 suma números impares retornando la suma de ambos números solo si el segundo número es impar, en caso contrario solo retorna el primer número.

Filtrado

Otra operación común sobre listas es el filtrado: la creación de una lista más pequeña filtrando elementos de una lista con base en algunos criterios definidos por usuario. El filtrado se ilustra en la Figura 5:

Figura 5. Filtrando una lista
Filtrando una lista

Cuando está filtrando, usted produce otra lista (o colección) potencialmente más pequeña que la original, dependiendo del criterio de filtrado. En el ejemplo de clasificador de número, yo uso filtrado para determinar los factores de un número, como se muestra en el Listado 3:

Listado 3. Usando filtrado para determinar los factores
public boolean isFactor(int number, int potential_factor) {
    return number % potential_factor == 0;
}

public List<Integer> factorsOf(final int number) {
    return range(1, number + 1)
            .filter(new F<Integer, Boolean>() {
                public Boolean f(final Integer i) {
                    return isFactor(number, i);
                }
            });
}

El código del Listado 3 crea un rango de números (como una Lista) desde 1 hasta el número objetivo, luego aplica el método filter() , usando el método isFactor() (definido al comienzo del listado) para eliminar los números que no sean factores del número objetivo.

La misma funcionalidad mostrada en el Listado 3 puede lograrse de forma mucho más concisa en un lenguaje que tenga cierres. Una versión Groovy aparece en el Listado 4:

Listado 4. Versión Groovy de una operación de filtrado
def isFactor(number, potential) {
  number % potential == 0;
}

def factorsOf(number) {
  (1..number).findAll { i -> isFactor(number, i) }
}

La versión Groovy de filter() esfindAll(), la cual acepta un bloque de código que especifica sus criterios de filtro. La última línea del método es el valor de retorno del método, que en este caso es la lista de factores.

Correlacionamiento

La operación map transforma una colección en una colección nueva, aplicando una función para cada uno de los elementos, como se ilustra en la figura 6:

Figura 6. Correlacionando una función en una colección
Correlacionando una función en una colección

En el ejemplo de clasificador de número, yo uso el correlacionamiento de la versión optimizada del método factorsOf() , mostrado en el Listado 5:

Listado 5. Método map()
 de buscador de factores de Functional Java
public List<Integer> factorsOfOptimized(final int number) {
    final List<Integer> factors = range(1, (int) round(sqrt(number) + 1))
            .filter(new F<Integer, Boolean>() {
                public Boolean f(final Integer i) {
                    return isFactor(number, i);
                }
            });
    return factors.append(factors.map(new F<Integer, Integer>() {
        public Integer f(final Integer i) {
            return number / i;
        }
    }))
   .nub();
}

El código del Listado 5 primero reúne la lista de factores hasta la raíz cuadrada del número objetivo, guardándolo en la variable factors . Luego anexé una nueva colección a los factors — generados por la función map() de la lista factors lista— aplicando el código para generar la lista simétrica (el factor coincidente sobre la raíz cuadrada). El último método nub() asegura que en la lista no existen duplicados.

Como es usual, la versión Groovy es mucho más directa, como se mostró en el Listado 6, porque los tipos flexibles y los bloques de código son ciudadanos de primera clase:

Listado 6. Factores Groovy optimizados
def factorsOfOptimized(number) {
  def factors = (1..(Math.sqrt(number))).findAll { i -> isFactor(number, i) }
  factors + factors.collect({ i -> number / i})
}

Los nombres de los métodos difieren, pero el código del Listado 6 realiza la misma tarea que el código del Listado 5: obtiene un rango de números desde 1 hasta la raíz cuadrada, los filtra en factores, luego le adjunta una lista correlacionando cada uno de los valores de lista con la función que produce el factor simétrico.


Volviendo a observar la perfección funcional

Con la disponibilidad de funciones de orden superior, todo el problema de determinar si un número es perfecto o no se condensa en un par de líneas de código en Groovy, como muestra el Listado 7:

Listado 7. Buscador de número perfecto Groovy
def factorsOf(number) {
  (1..number).findAll { i -> isFactor(number, i) }
}

def isPerfect(number) {
    factorsOf(number).inject(0, {i, j -> i + j}) == 2 * number
}

Este es, desde luego, un ejemplo artificial relacionado con clasificación de números, por lo que es difícil generalizar diferentes tipos de código. Sin embargo, he notado un cambio significativo en estilo de codificación de proyectos usando lenguajes que soportan estas abstracciones (sean o no lenguajes funcionales). Donde primero noté esto fue en proyectos Ruby on Rails. Ruby tiene esta misma lista de métodos de manipulación de listas que usan bloques de cierre, y me impresionó la frecuencia con la que aparecen los métodos collect(), map() y inject() . En cuanto usted se acostumbra a tener estas herramientas en su caja de herramientas, se verá regresando a ellas una y otra vez.


Conclusión

Unos de los retos principales de aprender un nuevo paradigma como la programación funcional es el aprendizaje de nuevos bloques de construcción y de "verlos" observar los problemas como una solución potencial. En la programación funcional usted tiene muchas menos abstracciones, pero cada una de ellas es genérica (con lo específico añadido mediante funciones de primera clase). Como la programación funcional descansa en gran medida en parámetros pasados y composición, usted tiene menos reglas que aprender sobre las interacciones entre mover partes, facilitando su trabajo.

La programación funcional logra la reutilización de código al extraer piezas de maquinaria genéricas, personalizables mediante funciones de orden superior. Este artículo resaltó algunas de las dificultades introducidas por los mecanismos de acople inherentes a lenguajes orientados por objeto, lo cual condujo a una discusión de las formas comunes en que las gráficas de clases son cosechadas para producir código reutilizable. Este es el dominio de los patrones de diseño. Luego mostré cómo mecanismos de grano grueso, basados en teoría de categoría, que le permiten aprovechar el código escrito (y depurado) por el diseñador de lenguaje para resolver problemas. En cada caso, la solución es sucinta y declarativa. Esto ilustra la reutilización de código mediante la composición de parámetros y funcionalidad para crear comportamiento genérico.

En la siguiente entrega, ahondaré más en los recursos funcionales de un par de lenguajes dinámicos en la JVM: Groovy y JRuby.

Recursos

Aprender

  • Effective Java (Joshua Bloch, Addison-Wesley, 2001): este libro de Bloch es un trabajo fundamental sobre cómo usar el lenguaje Java correctamente.
  • Programming in Scala, 1ª ed. (Martin Odersky, Lex Spoon y Bill Venners): Este libro está disponible online. La excelente segunda edición está disponible en librerías en muchos sitios.
  • Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma et al., Addison-Wesley, 1994): El trabajo clásico de la 'Pandilla de Cuatro' sobre patrones de diseño.
  • Category theory: La teoría de categoría es una rama de las matemáticas que cubre de manera abstracta las propiedades de conceptos matemáticos particulares.
  • Catamorphism: Un catamorfismo denota un correlacionamiento único de un álgebra a otra.
  • "Language designer's notebook: First, do no harm" (Brian Goetz, developerWorks, julio del 2011): Lea sobre las consideraciones de diseño detrás de expresiones lambda, un nuevo recurso de lenguaje en los trabajos para Java SE 8. Las expresiones lambda son literales de funciones — expresiones que encierran una computación diferida que puede ser tratada como un valor e invocada después.
  • Consulte la librería tecnológica para libros sobre estos y otros temas técnicos.
  • zona de tecnología Java developerWorks: Encuentre cientos de artículos sobre cada aspecto de la programación Java.

Obtener los productos y tecnologías

Comentar

Comentarios

developerWorks: Ingrese

Los campos obligatorios están marcados con un asterisco (*).


¿Necesita un IBM ID?
¿Olvidó su IBM ID?


¿Olvidó su Password?
Cambie su Password

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


La primera vez que inicie sesión en developerWorks, se creará un perfil para usted. La información en su propio perfil (nombre, país/región y nombre de la empresa) se muestra al público y acompañará a cualquier contenido que publique, a menos que opte por la opción de ocultar el nombre de su empresa. Puede actualizar su cuenta de IBM en cualquier momento.

Toda la información enviada es segura.

Elija su nombre para mostrar



La primera vez que inicia sesión en developerWorks se crea un perfil para usted, teniendo que elegir un nombre para mostrar en el mismo. Este nombre acompañará el contenido que usted publique en developerWorks.

Por favor elija un nombre de 3 - 31 caracteres. Su nombre de usuario debe ser único en la comunidad developerWorks y debe ser distinto a su dirección de email por motivos de privacidad.

Los campos obligatorios están marcados con un asterisco (*).

(Por favor elija un nombre de 3 - 31 caracteres.)

Al hacer clic en Enviar, usted está de acuerdo con los términos y condiciones de developerWorks.

 


Toda la información enviada es segura.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=90
Zone=tecnologia Java
ArticleID=806604
ArticleTitle=Functional thinking: Acoplamiento y composición, Parte 2
publish-date=03262012