Printable: La clase que no sabías que existía en Arduino y que no podrás dejar de usar

Printable: La clase que no sabías que existía en Arduino y que no podrás dejar de usar

Imagina que tienes una clase Clock (Reloj) que te funciona perfectamente, pero unas veces deberá imprimir al puerto serial (Serial), otras veces a un display LCD (lcd) y otras veces a hardware personalizado. ¿Cómo le harías?

  • ¿3 o más versiones de la misma clase?
  • ¿3 clases diferentes?
  • ¿Un enorme switch que cubra todos los casos?
  • ¿Usar de manera indiscriminada los métodos getter para repetir la misma tarea una y otra vez?

Si ya has enfrentado este problema, o estás trabajando en un proyecto similar, entonces estoy seguro que la clase Printable será de tu interés.

La clase Printable provee una forma fácil, rápida y segura de agregar la funcionalidad de imprimir a tus clases en diferentes escenarios, como los que establecí anteriormente, sin duplicar tu código.

Suponiendo que la clase Clock ya está funcionando, podrías hacer esto en un proyecto:

Clock serial_clock( 12, 0, 0 );
serial_clock.printTo( Serial ); // Output (in a console): 12:00:00

Mientras que en otro proyecto harías lo siguiente sin modificar ni una instrucción a la clase Clock y sin pasar argumentos raros que no escalan bien:

Clock lcd_clock( 12, 0, 0 );
lcd_clock.printTo( lcd ); // Output (in a LCD): 12:00:00

En ciertos casos, donde tengas habilitados tanto Serial, como lcd, podrías imprimir en ambos casos al mismo tiempo:

Clock a_clock( 12, 0, 0 );
a_clock.printTo( Serial ); // Output (in a console): 12:00:00
a_clock.printTo( lcd );    // Output (in a LCD): 12:00:00

Finalmente (un poco más avanzado), si escribiste una clase propia MyAmazingDisplay (la cual hereda de la clase Print), podrías hacer lo siguiente:

MyAmazingDisplay mad();
Clock my_clock( 12, 0, 0 );
my_clock.printTo( mad ); // Output (in your amazing display): 12:00:00

Como podrás observar, la magia se lleva a cabo en el método .printTo() de la clase Printable. Más adelante, cuando explique cómo se usa la clase, te darás cuenta que, además, estarás utilizando los métodos para imprimir de toda la vida: print(), println(), etc.

Por alguna razón que desconozco esta clase, a pesar de su utilidad, está poco documentada y por esa razón me decidí a escribir un poco sobre ella.

Así que sigue leyendo para que la conozcas y comiences a usarla en tus proyectos.

Tabla de contenidos

Principios de programación involucrados

(Si tienes prisa, o ya conoces los conceptos relacionados a esta clase, puedes brincar a la sección Ejemplos.)

En la clase Printable han sido usados un par de los principios más socorridos en la programación: el de responsabilidad única y el de inyección de dependencias.

Single Responsability Principle

El primero, Single Responsability Principle, establece que una clase debe hacer una sola cosa, y debe hacerla bien. En nuestro ejemplo, la clase Clock lleva a cabo únicamente la lógica de cualquier reloj, sin mezclar otras responsabilidades, como la de imprimir.

Visita este enlace para que conozcas más sobre este valioso principio de programación.

¿No se supone que en un reloj queremos ver la hora? No siempre, pero sí. Sin embargo, como apunté más arriba, uno querrá imprimir la hora en diferentes dispositivos en diferentes momentos y proyectos y es imposible que una clase pueda conocer por adelantado todos los tipos de pantallas.

Dependency Injection Principle

El segundo principio involucrado, Dependency injection, también conocido como el principio de Hollywood, aplica la siguiente metodología:

No nos llames, nosotros te llamamos.

Principio de Hollywood

En la instrucción

serial_clock.printTo( Serial );

estamos inyectando una dependencia (el “actor” Serial) que en su momento será llamado por el método printTo() (quien hace las veces del “productor de la película”). En este ejemplo, el productor de la película puede llamar a diferentes actores: Serial, lcd, etc, dependiendo de la película que esté produciendo.

Visita este enlace para que conozcas más sobre este otro valioso principio de programación.

En mi blog puedes visitar un artículo en el cual profundizo sobre este principio (un poco avanzado, pero vale la pena darle una leída). Para español da click aquí, for english click here.

Reuso de código

Los dos principios de programación anteriores, y en general, todos los principios de programación, promueven el reuso del código. Para el caso que nos ocupa, aunque en este momento no sea obvio, estarás reutilizando la infraestructura de Arduino para imprimir en los diferentes dispositivos. Por ejemplo, no tendrás que escribir funciones de conversión de enteros a cadenas de texto, sino simple y sencillamente vas a utilizar los métodos que ya conoces y que son parte tanto del objeto Serial, como lcd. Cuando más adelante desarrolle la idea verás de lo que estoy hablando.

Lo anterior viene a colación porque en la Internet he visto muchísimos ejemplos de programadores de Arduino (incluyéndome) que escriben una y otra vez funciones de conversión para sus dispositivos, como por ejemplo, las pantallas. ¿Por qué hacerlo cuando la plataforma ya nos provee de todas esas herramientas?

Si conociéramos un poco más a fondo la plataforma sabríamos que podemos usar los métodos print(), print(), etc, a nuestro favor, y a cambio sólo debemos escribir una función que escriba un carácter a la vez en nuestros dispositivos.

Hace unos días me puse a investigar cómo se usa el chip HT1622 (para controlar pantallas de cristal líquido) ya que la documentación china está en chino y deja mucho qué desear. Llegué a un código para Arduino en Github donde el driver del chip utiliza pines GPIO en lugar de la interfaz serial SPI; eso no es importante, dado que sí me funcionó como prueba de concepto. Lo interesante es que la capa superior, la que usa al driver basado en GPIOs, tenía funciones de conversión de números enteros a cadena de carácteres, debido a que el driver escribe letra por letra. Para ese momento yo ya conocía la función Printable y la jerarquía de la clase Print (de la cual escribiré un artículo más adelante) y me puse a pensar la razón por la cual ese programador estaba reinventando la rueda, y porqué yo lo había hecho antes de saber de la existencia de ambas clases. Y fue esta reflexión sobre el desconocimiento de la plataforma lo que me motivó a escribir este artículo, el cual espero que te sea de utilidad.

Para terminar esta sección te muestro un pequeño fragmento del código que me encontré para que te des una idea de los problemas en los que nos podemos meter por reinventar la rueda en cada proyecto que escribimos. ¿Te imaginas todo el tiempo que el programador perdió buscando y corrigiendo errores?

void HT1621::print(long num, const char* flags, int precision){
    if(num > 999999) // basic checks
        num = 999999; // clip into 999999
    if(num < -99999) // basic checks
        num = -99999; // clip into -99999

    char localbuffer[7]; //buffer to work within the function
    snprintf(localbuffer, 7, flags, num); // convert the decimal into string
    #ifdef _HTDEBUG
        Serial.begin(9600);
        Serial.print(localbuffer);
        Serial.print("\t");
    #endif

    // horrible handling but should get us working. needs refactor in next major
    if (precision > 0 && (num) < pow(10, precision)) {
        // we remove extra leading zeros
        for (int i = 0; i < (5 - precision); i++) {
            #ifdef _HTDEBUG
                Serial.print(localbuffer[1]);
            #endif // _HTDEBUG
            if(localbuffer[i+1] == '0' && localbuffer[i] != '-'){ // we remove only if there is another zero ahead AND if it's not a minus sign
                localbuffer[i] = ' ';

// etc ...

Clase Printable

La clase Printable es una clase utilitaria, esto es, no es una clase de la que puedas instanciar objetos, sino que agregará la funcionalidad para imprimir, ya sea en el puerto serial (Serial) o en un LCD 16×2 (lcd), o como lo mencioné, en objetos de clases que han heredado de la clase base Print.

Por su naturaleza no puedes hacer lo siguiente con la clase Printable:

Printable p; // Error
p.printTo();

Dado que Printable incluye al método público virtual puro printTo(), esta clase se convierte en una clase abstracta (y por eso no puedes instanciar objetos de ella):

virtual size_t printTo( Print& p ) const = 0;

Como cliente de la clase Printable deberás proveer el código de printTo() (la forma de hacerlo te la indicaré más adelante). En términos de la Programación Orientada a Objetos, POO, tú implementarás al método printTo().

El argumento p, el cual es una referencia a la clase Print, representa a cualquier objeto capaz de imprimir, como los objetos Serial y lcd.

A saber, la clase Print es la clase base de las diferentes clases que imprimen en Arduino. En otro artículo te mostraré cómo puedes tomar ventaja de esta clase para tus propios dispositivos. Por ejemplo, una clase myDisplay, que represente a un display que tú construiste y que tendría las mismas operaciones que Serial y lcd: print(), println(), etc, sin que tengas que reinventar la rueda.

El modificador const es una línea de defensa obligatoria que indica que el código que implemente al método printTo() no va a modificar al objeto p; esto es, dentro del método printTo() el objeto p es de sólo lectura. Si tu código de alguna forma pretendiera modificar a p, entonces el compilador te lo hará notar a través de un error de compilación.

Finalmente, printTo() devuelve el número de carácteres que se hayan escrito en el dispositivo. El cliente es responsable de usar o no a este valor (nunca lo usamos, aunque deberíamos).

La clase Printable es una clase mixin.

Clases mixin

Las clases mixin representan un patrón de diseño de software que promueven la reutilización de código.

Esto es, las clases mixin son clases base que agregan funcionalidad a otras clases a través del mecanismo de la herencia, pero que de ninguna manera forman parte de ellas en una jerarquía tradicional de generalización, ni tampoco puedes instanciar objetos de ellas.

En el caso que nos ocupa la clase Printable es una clase mixin que ya está programada, pero tú podrías escribir tus propias clases mixin para reutilizarlas en tus proyectos, o para compartirlas con la sociedad, o por simple diversión.

Usando a la clase Printable

Para usar a la clase Printable (y en general cualquier clase mixin) tus clases deberán heredar de ella e implementar el método printTo() (o, en general, todos los métodos virtuales puros que la clase mixin incluya).

El siguiente código te muestra lo que debes hacer para incorporar la clase Printable en la clase Clock:

class Clock : public Printable // (1)
{
   private:
      // igual que antes...

   public:
      // igual que antes...

      virtual size_t printTo( Print& p ) const override; // (2)
};

size_t Clock::printTo( Print& p ) const // (3)
{
   if( this->hrs < 10 ) p.print( '0' );
   p.print( this->hrs );
   p.print( ":" );

   if( this->min < 10 ) p.print( '0' );
   p.print( this->min );
   p.print( ":" );

   if( this->sec < 10 ) p.print( '0' );
   p.println( this->sec );
}

Del código anterior puedes observar 3 cosas:

  1. La clase Clock hereda de la clase Printable.
  2. Indicas (override) que vas a proporcionar el código para el método printTo().
  3. Proporcionas el código para el método printTo() (implementas el método printTo(), en términos de la POO).

Dentro del método printTo() el objeto p es una referencia a cualquier instancia (objeto) de alguna clase que haya heredado de Print, como los objetos Serial y lcd. En otras palabras, p representa a un dispositivo capaz de imprimir.

Como feliz consecuencia de lo anterior, el objeto p contiene todas las funciones de impresión a las que estamos acostumbrados: print(), println(), etc, las cuales puedes usar para imprimir lo que tú quieras sin ningún esfuerzo extra.

Reutilización de código

Es importante que notes que utilizando la clase Printable vas a poder imprimir en dispositivos conocidos sin escribir una sola línea de código que tenga que ver con la impresión, por ejemplo, no tendrás que convertir de cadena a entero, o de entero a cadena, o de número real a cadena, etc. La clase Printable, a través de la herencia con la clase Print lo hace por tí.

La terminación able y los nombres curiosos

La terminación ble ( able en inglés, y able e ible en español) significa “otorgar la capacidad de”:

  • “Imprimible”, que tiene la capacidad de imprimir.
  • “Serializable”, que tiene la capacidad de tomar datos binarios y convertirlos a un flujo de texto para ser transmitido por algún canal serial.
  • “Switchable”, que tiene la capacidad de estar en uno de dos estados (encendido o apagado).
  • “Movible”, que se puede mover.
  • Etc.

Oficialmente, según la Real Academia de la Lengua Española (la RAE), la terminación “-ble” significa:

suf. Forma adjetivos casi siempre deverbales. Indica posibilidad pasiva, es decir, capacidad o aptitud para recibir la acción del verbo. Si el verbo es de la primera conjugación, el sufijo toma la forma -able. Prorrogable. Si es de la segunda o tercera, toma la forma -ible. Reconocible, distinguible. Los derivados de verbos intransitivos o de sustantivos suelen tener valor activo. Agradable, servible.

RAE

En nuestro ejemplo, a la clase cliente Clock le estamos agregando la capacidad de imprimir. De esta relación pueden resultar nombres muy curiosos, como ClockPrintable, ya que la costumbre es agregar la partícula able a los nombres de nuestras clases cliente:

  • Print -> Printable
  • Switch -> Switchable
  • Time -> Timeable (tengo una clase mixin sencilla Timeable que le otorga a los clientes la capacidad de funcionar por tiempo con el mínimo de tu intervención: un relevador se desactivará después de un cierto tiempo. Si te interesa, házmelo saber en los comentarios).

Una vez abordados los conceptos más importantes alrededor de la clase Printable es momento de ver un ejemplo completo.

Ejemplo

El siguiente es el código (casi) completo de la clase Clock. Observa, además, que si usas un sketch de Arduino (lo cual seguramente estarás haciendo), entonces no tienes necesidad de agregar el archivo de encabezado <Printable.h>, sin embargo lo voy a incluir por completitud.

#include "Printable.h"

class Clock : public Printable
{
   private:
      uint8_t hrs;
      uint8_t min;
      uint8_t sec;

   public:
      Clock( uint8_t _hrs, uint8_t _min, uint8_t _sec )
         : hrs{_hrs}, min{_min}, sec{_sec}
      { /* nothing*/ }

      // clock logic here...

      // magic happens here:
      virtual size_t printTo( Print& p ) const override
      {
         if( this->hrs < 10 ) p.print( '0' );
         p.print( this->hrs );
         p.print( ":" );

         if( this->min < 10 ) p.print( '0' );
         p.print( this->min );
         p.print( ":" );

         if( this->sec < 10 ) p.print( '0' );
         p.println( this->sec );
      }
};

Clock my_clock( 9, 30, 5 );

void setup()
{
   Serial.begin( 115200 );

   // initialize the lcd here (if it's ever used)
}

void loop()
{
   // each second:
   my_clock.printTo( Serial );

   // if lcd has been properly initialized:
   // my_clock.printTo( lcd );
}

Para mantener el ejemplo simple y manejable omití algunas partes que tú muy fácilmente podrás completar. No obstante esas omisiones, el código contiene lo necesario de lo que platicamos hoy y compila.

Por otro lado, con toda seguridad puedes eliminar el código que hace referencia a un LCD, ya que la finalidad de haberlo incluído es para que vieras la flexibilidad y utilidad de esta clase.

NOTA: Si has seguido mis artículos sabrás que los sketches no me gustan y que prefiero programar en Arduino desde la función main(). Si haces lo mismo, entonces sí que deberás incluir al encabezado <Printable.h> en el archivo de declaraciones de la clase cliente (esto es, en el archivo “.h”).

¿Qué nos llevamos y qué sigue?

Hoy hemos visto una forma de sacar provecho de la infraestructura de Arduino y de evitar duplicar el código utilizando una clase muy útil.

Así mismo, también vimos cómo algunos principios de programación universales son utilizados en la plataforma de Arduino y cómo también nosotros los podemos incorporar a nuestros propios códigos. Te recomiendo que leas sobre estos principios y comiences a ponerlos en práctica, tu vida como programador va a dar un giro de 180 grados.

Te llevas como tarea incorporar la clase Printable en tus proyectos y yo me llevo, también como tarea, escribir un artículo sobre la clase Print (y Stream), que como mencioné, permitirán que tu hardware use las herramientas que la plataforma Arduino provee para que evites descubrir la rueda en cada proyecto que inicies.

Adendum

No me podía ir sin mostrarte la simplicidad de la clase Printable. La puedes encontrar en tu/instalación/de/arduino/hardware/arduino/avr/cores/arduino:

#include <stdlib.h>

class Print;

/** The Printable class provides a way for new classes to allow themselves to be printed.
    By deriving from Printable and implementing the printTo method, it will then be possible
    for users to print out instances of this class by passing them into the usual
    Print::print and Print::println methods.
*/

class Printable
{
  public:
    virtual size_t printTo(Print& p) const = 0;
};

Tengo un pequeño curso gratuito sobre el sistema operativo en tiempo real FreeRTOS donde utilizo a Arduino como conejillo de indias. Puedes echarle una mirada en el siguiente enlace:

Í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