Depende del comportamiento, no de los datos

Este es uno de los consejos más poderosos en programación OO que he recibido.

Significa que los objetos no deben ser vistos solo como una colección de estructuras de datos. En cambio, debes visualizarlos como entidades de software que pueden responder a mensajes y responder preguntas sobre sí mismos.

Logramos esto ocultando los detalles de implementación y solo exponiendo interfaces públicas bien definidas. Las partes de tu programa que usan un objeto no necesitan saber sobre el funcionamiento interno del objeto, solo necesitan saber cómo usar la interfaz del objeto. Esto te ayuda a crear diseños modulares que son más fáciles de extender y mantener.

El ocultamiento de información es una técnica poderosa utilizada para lograr este objetivo: previene que otras piezas de código accedan a detalles de implementación de la clase. Este es uno de los conceptos que más me tomó apreciar. Al principio, realmente no entendía cuál era el punto de los métodos y atributos privados. Después de entender mejor los principios detrás de la programación OO, entendí que todo se trata de manejar la complejidad.

Ocultando los detalles de pizza&piña

Las partes de tu programa no necesitan saber todos los detalles de las clases que usan. Usualmente, todo lo que necesitan saber es qué método llamar y qué argumentos pasar. Es mucho más fácil visualizarlo con una analogía:

Imagina que vas a un restaurante a comer pizza. Estás acostumbrado a una interfaz muy simple: un mesero toma tu orden y luego recibes la pizza después de algunos minutos. Ahora imagina que el mesero, en lugar de tomar tu orden, te lleva a la cocina y te pide que guíes al chef, paso a paso, en el proceso de preparación de la pizza.

Esta interfaz inconveniente es el resultado de no usar ocultamiento de información. Como cliente, no te importa (o ni siquiera sabes) cómo preparar una pizza, esa es la responsabilidad del chef. Todo lo que necesitas proporcionar es el nombre del tipo de pizza que quieres y ¡voilà! En lugar de usar esta interfaz:

    order_pizza(:hawaiian)
    # OR
    order_pizza(:hungarese)

Terminaste especificando todo el proceso:

    chef.ask_to_prepare_dough(:thin_dough)
    chef.ask_to_add_ingredient(:ham)
    chef.ask_to_add_ingredient(:cheese)
    chef.ask_to_add_ingredient(:chilli)
    chef.ask_to_add_ingredient(:pepperoni)
    chef.ask_to_put_pizza_in_the_oven(:toasty)
    chef.ask_for_final_pizza

Esto resulta en una pieza de código estrechamente acoplada. Cualquier cambio en los detalles de nuestra clase chef impactará fuertemente todo el código que la usa. Por esta razón, todos los detalles y datos utilizados en procedimientos complejos deben mantenerse ocultos dentro de nuestros objetos. Esto también aplica a las estructuras de datos que nuestros objetos usan.

Hola, soy Robobob y mi número de serie es XAWE-15915975357

No deberías acceder a los atributos de un objeto directamente.

Supongamos que tienes una clase Robot y quieres acceder a información sobre nuestro amigo artificial (la variable que mantiene el número de serie), puedes escribir la siguiente línea para acceder al número de serie:

    print "Este es el número de serie de mi robot: #{robbierob.serial_number}"
    #...
    #... serial_number es una variable de instancia
    robbierob.serial_number = "NAVE-48622684268

Este código tiene al menos 2 problemas:

1- ¿Qué pasa si la variable serial_number cambia en formato o tipo? necesitarías revisar todo el código que referenció las variables de instancia y cambiarlo para que funcione con el nuevo cambio.

2- Un problema más sutil es que estás viendo tu clase robot como una estructura de datos glorificada, no como un objeto que puede responder a mensajes (métodos) y darte información sobre sí mismo o alterar su estado.

La solución es que, en lugar de depender de los datos (el atributo en sí), puedes depender del comportamiento (funciones), esa es la razón por la que usamos ‘setters’ y ‘getters’ para acceder a atributos. Declaramos serial_number como privado, de esa manera estamos ocultando los datos detrás de una fachada de comportamiento.

Agregar setters y getters resuelve el primer problema listado arriba: Si por alguna razón necesitas hacer un cambio en los detalles, solo cambia esas dos funciones. El resto del código ni siquiera notará que algo es diferente siempre y cuando sigas honrando la interfaz.

Lamentablemente, solo agregar una capa de setters y getters no resuelve el segundo problema. Depender del comportamiento, no de los datos, es más que solo agregar una capa de métodos frente a tus datos, se trata de crear abstracciones apropiadas.

Aplicando una política de acceso a través de abstracción

Como acabamos de mencionar, solo un par de métodos no es suficiente. Queremos tener objetos que respondan a mensajes significativos, no solo estructuras de datos glorificadas escondidas detrás de mutadores y accesores.

Supongamos que todos los robots tienen un nombre de línea y un número de serie. Los nombres de línea son 4 caracteres en mayúscula (como “XAWE” y “NAVE”), mientras que el número de serie es un valor numérico de 11 dígitos. De los requerimientos, sabes que es útil establecer estos dos valores individualmente y que los usuarios solo necesitan obtener el identificador completo en el formato LLLL-sssssssss.

Puedes aplicar esta política creando dos métodos para establecer esos valores individualmente, y uno para obtener el identificador completo. Podrías usar un objeto robot de esta manera:

    robbierob.set_product_line("SUHR")
    robbierob.set_serial(15975464682)
    #...
    print "El número de serie completo de nuestro robot es #{robbierob.get_identifier}"

¿Cómo se almacenan la línea de producto y el número de serie dentro de los objetos robot? ¿Tienen una variable para cada valor? ¿están agregados en una sola cadena? ¿hay una estructura especial o incluso un objeto identificador?

No lo sabemos, y no nos importa. Todo lo que necesitamos saber es la interfaz: qué métodos están ahí y cuáles son los requerimientos para llamarlos. Nota que los implementadores de Robot decidieron que no puedes obtener los valores individuales que componen el identificador. Esta forma de control de acceso es común, así como lo opuesto: prohibirte establecer valores individuales pero poder consultarlos individualmente.

Este es un enfoque más ‘objetual’. Recuerda que el poder de la programación OO yace en modelar la realidad a través de paquetes de comportamiento u objetos. Si construyes las abstracciones correctas, puedes crear algo que es más que una estructura de datos glorificada.

Esto, por supuesto, no aplica a objetos de transferencia de datos.

La Ley de Demeter

Finalmente, echemos un vistazo a una heurística útil para medir el nivel de acoplamiento en tu código, la Ley de Demeter (LoD). Este es un buen indicador de la calidad de tus abstracciones y qué tan ocultos están tus detalles de implementación.

Te dice que un objeto no debería llamar métodos en objetos que no están cerca de él. Puedes medir la ‘cercanía’ de un objeto contando la cantidad de puntos (‘.’) en la cadena de métodos. El siguiente es un ejemplo de llamar a un método muy lejos del llamador:

    spaceship.crew.get_captain().inventory.get_tool(:telephone).send_message(:mom, "Estoy bien mamá, salúdame a papá")

Este ejemplo anterior es una violación de la LoD, ya que spaceship está llamando a un método (send_message) en un objeto muy ‘lejos’ de sí mismo. Las llamadas a métodos con muchos puntos en la cadena usualmente se llaman trainwrecks.

Bien, ¿pero cuántos puntos están permitidos?

La Ley de Demeter no es una regla estricta, es una heurística. Si tienes llamadas a métodos largas, es probable que tengas problemas con la definición de tus interfaces. Si los objetos necesitan funcionalidad de otros objetos que viven muy lejos, podrías querer reestructurar tu programa para que estén más cerca. La LoD es un indicador de posibles problemas con la manera en que tus objetos están organizados en tu programa.

Bob Martin considera válidas las siguientes llamadas en una función F del objeto O:

  • Otros métodos en O.
  • Métodos en objetos creados dentro de F.
  • Métodos definidos en objetos pasados a F como argumentos.
  • Métodos definidos en objetos mantenidos en variables de instancia de O.

Cualquier cosa más allá de esto es una violación de la LoD y podría significar que tu código necesita reestructuración.

La diferencia está en los detalles

Esto puede parecer algo muy pequeño, pero mi experiencia me ha enseñado que en esta profesión, los detalles hacen toda la diferencia.

La diferencia entre código fácil de mantener y pesadillas heredadas yace en qué tan efectivamente puedes desacoplar tu código. Los diseños modulares con clases cohesivas y desacopladas son un placer para trabajar, y esta idea te acerca un paso más a esa meta.

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.
  • Puedes encontrar más información sobre este tema en el capítulo 2 de Practical Object-Oriented Design in Ruby.
  • Envíame un email con preguntas, comentarios o sugerencias (está en la página Autor).

Juan Luis Orozco Villalobos

¡Hola! Soy Juan, ingeniero de software y consultor en Budapest. Me especializo en computación en la nube e IA, y me encanta ayudar a otros a aprender sobre tecnología e ingeniería