¡Hazlo, no preguntes! O sobre cómo dejar de abusar de los getters

¡Hazlo, no preguntes! O sobre cómo dejar de abusar de los getters

A pesar de que la programación es una de mis dos más grandes pasiones no tuve formación académica en ésta, más allá de cursos introductorios mientras estudiaba Ingeniería en Electrónica.

En el camino, mientras me formaba de manera autodidacta como Ingeniero de Software, aprendí que en la Programación Orientada a Objetos cada atributo debía contar con un método para cambiar su valor y un método para leer su valor; los famosos setters y getters, respectivamente.

Lenguajes como C# (y Ruby, entre otros) incluso los han incorporado dentro del propio lenguaje (los properties de C# o los attr_reader de Ruby), lo cual refuerza la idea de que siempre deberíamos contar con el par set/get por cada atributo de nuestras clases.

Sin embargo, la idea de que cada atributo cuente con un método de lectura nos puede crear vicios de programación, como por ejemplo, usar a los getters por el simple hecho de que los tenemos disponibles moviendo hacia afuera de los objetos la lógica propia de ellos.

El principio de programación del cual quiero platicar hoy: “¡Hazlo, no preguntes! (cuyo nombre original en inglés es “Tell, Don’t Ask!”) nos sugiere evitar la utilización (irracional) de los getters y buscar formas alternativas de escribir software más limpio, más fácil de leer y más seguro.

Tabla de contenidos

Principio ¡Hazlo, no preguntes!

Y a todo esto, ¿qué es lo que nos dice este principio? Te lo pongo en español y a continuación la versión original en inglés:

El principio “Tell-Don’t-Ask” nos ayuda a recordar que la programación orientada a objetos se trata de agrupar datos con las funciones que operan sobre esos datos. Nos recuerda que en lugar de pedirle datos a un objeto y actuar sobre esos datos, deberíamos decirle al objeto qué hacer. Esto nos anima a mover el comportamiento a un objeto para que vaya con los datos.

Me tomé la libertad de resaltar lo que considero la parte fundamental de este principio.

La versión original, propuesta por Martin Fowler, dice así:

Tell-Don’t-Ask is a principle that helps people remember that object-orientation is about bundling data with the functions that operate on that data. It reminds us that rather than asking an object for data and acting on that data, we should instead tell an object what to do. This encourages to move behavior into an object to go with the data.

Martin Fowler

Un horno de microondas

Para comprender mejor a este principio quiero usar como ejemplo a mi horno de microondas: mi horno (y muy seguramente el tuyo también) me permite agregar o quitar tiempo mientras los alimentos están siendo procesados; esto es, puedo agregar o quitar 10 segundos cada vez que presiono el botón (+) o el botón () en el panel, respectivamente. 

Puedo agregar todo el tiempo que quiera, pero cuando se trata de descontarlo el horno no hace nada cuando quedan menos de 10 segundos. Vamos a ver cómo implementar esta lógica sin y con el principio.

Solución directa

La solución directa, SIN EL PRINCIPIO, queda más o menos así:

  • El usuario presiona el botón (+) mientras el horno está funcionando. El programa:
    • LEE el tiempo restante.
    • AGREGA 10 segundos.
    • ESCRIBE el nuevo tiempo.
  • El usuario presiona el botón (-) mientras el horno está funcionando. El programa:
    • LEE el tiempo restante.
    • Si el tiempo restante es mayor que 10 segundos, entonces QUITA 10 segundos; en caso contrario no hace nada.
    • ESCRIBE el nuevo tiempo.

En código se vería así:

   Device mw_oven;

   // ...

   if( key.read() == eKey::Button::Plus )
   {
      uint32_t remaining_time = mw_oven.get_rem_time(); // READ

      remaining_time += 10;                             // ADD SOME TIME

      mw_oven.set_new_time( remaining_time );           // WRITE
   }
   else if( key.read() == eKey::Button::Minus )
   {
      uint32_t remaining_time = mw_oven.get_rem_time(); // READ

      if( remaining_time > 10 )
      {
         remaining_time -= 10;                          // SUBSTRACT SOME TIME

         mw_oven.set_new_time( remaining_time );        // WRITE
      }
   }

Solución usando al principio ¡Haz, no preguntes!

La solución, CON EL PRINCIPIO, queda así:

  • El usuario presiona el botón (+) mientras el horno está funcionando. El programa:
    • INDICA que quiere agregar 10 segundos utilizando una función especial para ello.
  • El usuario presiona el botón (-) mientras el horno está funcionando. El programa:
    • INDICA que quiere quitar 10 segundos utilizando una función especial para ello.

En código se vería así:

   Device mw_oven;

   // ...

   if( key.read() == eKey::Button::Plus )
   {
      mw_oven.add_time( 10_s );              // Tell, don't ask!
   }
   else if( key.read() == eKey::Button::Minus )
   {
      mw_oven.sub_time( 10_s );              // Tell, don't ask!
   }

En el primer caso le preguntas al horno por su estado, luego calculas el nuevo tiempo (¡por fuera del objeto!), y finalmente escribes ese nuevo valor de vuelta al horno. ¿Porqué tienes que ser tú, el cliente, el que realice esa lógica? ¿Qué pasa si haces algo mal?

En el segundo caso el horno está provisto de operaciones que intentarán llevar a cabo tu petición (.add_time() y .sub_time()), sin que tú, como cliente, te enteres de cómo lo hace.

El horno, sin tu intervención, está haciendo lo que se supone que debe hacer. ¿Quién más que el propio horno de microondas sabe cómo comportarse como horno de microondas?

En caso de que agregar tiempo también tenga sus propias limitaciones, entonces podrías hacer algo similar al método .sub_time().

¿Notaste que la versión que usa al principio es más clara, más sucinta, más fácil de leer y mantener, no expone el interior del objeto y, muy importante, es más segura? En un momento tocaré el tema del código seguro.

Ahora, probar que hay tiempo suficiente para descontar la cantidad que ha indicado el cliente es responsabilidad del objeto y no tuya. La parte de la clase Device correspondiente al método .sub_time() podría verse así:

class Device
{
   // ...
   uint32_t _remaining_time{0};

public:
   // ...

   bool sub_time( uint32_t time )
   {
      bool ret_val = false;

      if( _remaining_time > time ) // This logic belongs here!
      {
         _remaining_time -= time;

         ret_val = true;
      }

      return ret_val;
   }
};

(Quizás te fijaste que usé 10_s, en lugar de simplemente el valor 10. C++11 y posteriores incluyen un mecanismo llamado User-defined literals que nos permite ser más explícitos en cuanto a las unidades de medición que usamos en nuestros programas. Hablé sobre ellas en esta entrada en el punto 4.)

Getters y sus cosas

Los getters exponen el interior de los objetos

Los getters tienen varios problemas: observa que en el primer ejemplo el horno (o mejor dicho, el objeto mw_oven) expone su interior al devolverte el tiempo restante. ¿En qué formato ha sido devuelto el tiempo? ¿mm:ss? ¿segundos? La exposición de las partes internas de un objeto es una clara violación al principio de ocultamiento de la información

Imagina que una primera versión de la clase Device devuelve el total de segundos restantes y, en consecuencia, tú basas toda tu lógica en ello. Pero más adelante, en una siguiente versión, el autor del código cambia de parecer y devuelve el tiempo como mm:ss, ¿qué vas a hacer?

Los getters podrían hacerte escribir código inseguro

Si el escenario anterior ya es malo, imagina el desastre que podrías causar si en la operación de descontar tiempo incondicionalmente quitas 10 segundos: ¿Cómo interpretaría el sistema el resultado de una operación aritmética tal como:  5-10?

Por eso es importante en este tipo de sistemas que sea el propio objeto el responsable de agregar o quitar tiempo, y en su caso, tomar las medidas necesarias cuando el usuario cometa un error.

La operación podría devolver el nuevo tiempo para que tú lo imprimas en la pantalla, o quizás devolver un valor booleano para indicar que la operación se realizó correctamente (o no y que el cliente emita un pitido), o quizás lanzar una excepción (y que el horno se apague). 

Anécdota

En la clase de Estructuras de Datos y Algoritmos que imparto en la universidad (en lenguaje C), a mis alumnos les enseño que las funciones destructoras, además de devolver la memoria que el objeto pidió, deben poner a un estado seguro a los apuntadores para que no sean utilizado de forma errónea una vez que los objetos dejaron de existir:

void List_Delete( List** pThis )
{
   assert( *pThis );

   // ...

   free( *pThis );
   *pThis = NULL;    // Avoid future problems!
}

En todas las API de todas las estructuras de datos que escribo con mis estudiantes, las funciones destructoras son las únicas que requieren que el argumento sea doble apuntador (y los alumnos odian a los dobles apuntadores). La razón es que es el propio objeto el que deja en un estado seguro al sistema después de devolver la memoria. Si por error el objeto es utilizado después de haber sido destruído, entonces el sistema se detendrá, bajo control. La única forma de lograr que el apuntador siga valiendo NULL saliendo de la función es pasándole su dirección; esto es; un doble apuntador. Lo importante es que el objeto no confía en el programador =)

Insisto, confiar en que el cliente hará lo correcto no es correcto.

I’ve been there!

¿Los getter son malos?

¡No, para nada! En nuestro ejemplo del horno por supuesto que necesitamos conocer el tiempo restante para poderlo imprimir en una pantalla, pero en este caso no estaríamos llevando a cabo ninguna lógica extra.

Otros ejemplos donde preguntarle al objeto por su estado es fundamental:

  • Una clase para un teclado Keyboard. Aquí queremos saber cuál tecla presionó el usuario. De hecho, lo vimos en los ejemplos previos. Si no tuviésemos la oportunidad de preguntar sobre las teclas presionadas, entonces la clase sería inútil.
  • Una clase para sensores Sensor. Si estamos midiendo, lo más natural es querer conocer la magnitud de la medición; en caso contrario, la clase también sería inútil. La clase Sensor debería ser capaz de devolvernos la medición en las unidades más convenientes para evitar que nosotros realicemos las conversiones por fuera, lo cual nos llevaría de regreso al problema: LEER->PROCESAR->ESCRIBIR.

En este par de ejemplos sólo preguntamos, no realizamos ningún tipo de lógica propia del objeto, por lo cual es completamente válido tener y usar getters.

Finalmente, el punto no es evitarlos a toda costa como la plaga, sino utilizarlos con responsabilidad y donde tenga sentido usarlos (al igual que con las variables globales, las cuales a veces son inevitables y muy útiles).

Antes de terminar

  • Recuerda: No se trata de jamás volver a usar los getters; se trata de usarlos con responsabilidad y buen juicio, y mantener las partes privadas de una clase privadas.
  • En tus proyectos actuales y anteriores observa si no seguiste el principio ¡Haz, no preguntes!
  • Cuando estés diseñando una clase pon especial atención a aquellas operaciones cuya responsabilidad es del propio objeto y no del cliente, y aplica el principio.
  • Considera utilizar al principio en futuros proyectos.
  • ¿Qué otros principios has usado? Platícamelo en los comentarios.

¿Haz escuchado del sistema operativo en tiempo real, FreeRTOS? Escribí un pequeño curso sobre él, quizás quieras revisarlo:

Índice del curso

Espero que esta entrada haya sido de tu interés. Si fue así podrías suscribirte a mi blog o comparte esta entrada con alguien que consideres que puede serle de ayuda.


Fco. Javier Rodríguez
Escrito por:

Fco. Javier Rodríguez

Soy Ingeniero Electrónico con 20+ años de experiencia en el diseño y desarrollo de productos electrónicos de consumo y a medida, y 12+ años como profesor. Egresado de la UNAM, también tengo el grado de Maestro en Ingeniería por la misma universidad. Mi perfil completo lo puede encontrar en: https://www.linkedin.com/in/fjrg76-dot-com/

Ver todas las entradas