jueves, 19 de septiembre de 2013

Getters y setters automágicos en C++

Hablando con un amigo y colega hace unos días, me comentaba que en Haxe, un lenguaje con varias cosas tomadas (aparentemente) de actionscript y javascript, se pueden definir atributos con getters y setters en una linea como esta: "public var(A,B)", donde A y B pueden ser "null" si no queremos que se puedan ver o modificar desde fuera de la clase, "default" si queremos que se pueda como si fueran públicos, "get" o "set" si más abajo queremos implementar un getter o setter especial, y otras variantes. Algo interesante de esto es que desde el programa/función "cliente" de la clase (el que la usa para algo mediante su interfaz pública), el acceso a estos atributos no cambia de un caso a otro, sino que es siempre igual, como si fueran públicos. Pero en realidad se mete en medio (si queremos) de forma transparente el getter/setter propio.

A esta clase de cosas que parecen pero no son simples atributos públicos, muchos lenguajes las llaman propiedades. Inmediatamente me acordé de las clases para representar widgets en las ventanas con Borland Builder, donde podía hacer edit1->text="hola" en lugar de edit1->SetText("hola") sin problemas. Y dije: "esto se puede hacer igual de transparente en C++". En el caso de objetos de una interfaz gráfica, hay varias formas de que parezca transparente, pero mi idea era generalizarlo, y plantear algún mecanismo para lograr esto con cualquier atributo sin meter mucho ruido en el código de la clase.

Es interesante, además de por la "comodidad" sintáctica, también porque un código con clases mal diseñadas o diseñadas a las apuradas, donde no se había previsto un setter o getter, puede corregirse modificando la clase, pero sin tener que hacer ningún cambio en los programas clientes. Y a mi me divierten estos "ejercicios" donde tengo que exprimir la sintaxis de C++, o combinar trucos varios para lograr que el código aparente algo que no es. Entonces, cuando alguien dice "este lenguaje es mejor porque me deja hacer esto", yo digo "pero en C++ también lo puedo hacer", sin aclarar que para hacerlo tengo que utilizar templates y macros de preprocesador a montones, hacer sobrecargas extrañas, inventar clases auxiliares, y todo tipo de trucos que llevan al final a que ya no quiera usar esa solución en la práctica. Pero sí me quedo contento pensando que se puede.

Yendo al grano, mi primer problema para reproducir esta "característica" fue: ¿cómo hacer que el setter y el getter se llamen de forma automática y transparente? Es decir, quiero que al hacer "a.b=x" sea como hacer "a.set_b(x)", o que hacer "x=a.b" sea como hacer "x=a.get_b()". La solución que se me vino a la cabeza fue usar sobrecarga de operadores: sobrecargar el igual para que actúe de setter. Pero entonces, si el atributo era en realidad un int, tengo que hacer una clase a modo de wrapper que contenga un int y sobrecargue el igual, porque no se lo puedo sobrecargar al int. Y esto resuelve el setter, pero no el getter. Para el getter sobrecargo la conversión a int de esa clase. Entonces, tengo una clase como esta:
   class Propiedad {
      int el_atributo;
   public:
      Propiedad &operator=(int x) { ... } // setter
      operator int() { ... } // getter
   };
Ahora bien, usar una propiedad así es fácil y lindo desde el programa cliente, pero no al hacer la clase que lo contiene (de ahora en más digamos clase C). Imagínense lo horrible que sería hacer una clase de estas (Propiedad) por cada atributo de una clase como C, poniendo ahí el setter y getter. Entonces pensé en hacerla genérica de dos formas: templatizando el tipo del verdadero atributo, para que sirva para cualquier cosa en lugar de int; y utilizando punteros a funciones para llamar a setters y getters definidos fuera de esta clase Propiedad. En realidad, serían punteros a métodos de la clase C que contiene a la propiedad. Pero para declarar un puntero a método hay que poner esa clase C en la declaración (hay que decir método de cual clase), y entonces también templatizar eso. Pero no puedo templatizar el tipo de esa clase C en la propiedad antes de declararla, y no puedo declarar esa clase C sin poner dentro su propiedad. Hay un problema tipo huevo-gallina que me hace descartar esa parte del template. La solución fue declarar toda esta clase auxiliar dentro de la clase C, y de paso evito conflictos de nombres y visibilidad (queda oculto y encapsulado).

En resumen: una clase templatizada (Propiedad), metida dentro de otra clase (C), que recibe en su constructor punteros a métodos de esa otra clase (setters/getters en C), que son los que invocará al momento de "settear" o "gettear" el valor que representa (el verdadero atributo que quería para C). Una parte de la clase (la del getter) sería:
    template<class T>
    class Propiedad { 
        T el_atributo; // el verdadero atributo
        C *la_clase; // la clase que lo contiene
        const T &(C::*el_getter)(const T &); // el puntero al getter
    public:
        // método para cargar el puntero al getter
        void SetGetter(C *la_clase, const  T &(C::*el_getter)(const T &)) {
             this->la_clase=la_clase; this->el_getter=el_getter;
        }
        operator const T() { // la llamada automática al setter
            (la_clase->*el_getter)(el_atributo); 
        }
    };

Ahora, queda el tema de poner todo este código horrible dentro de cada clase C de forma fácil. Y ahí viene a mi ayuda el preprocesador. Hago entonces una macro que recibe el nombre de la clase C y define toda la clase genérica para la propiedad (con algunas mejoras, como comportamientos para setters y getters por defecto, cuando los punteros a métodos son NULL). Entonces, en un header "atributos_magicos.h" pondría las siguientes macros (esta es la versión final y completa para que copien y peguen):
    #define _usar_atributos_magicos(C)\
    template<class T> \
    class _atr { \
        T el_atributo; \
        C *la_clase;\
        bool (C::*el_setter)(const T &);\
        const T &(C::*el_getter)(const T &);\
    public:\
        _atr():la_clase(NULL),el_setter(NULL), el_getter(NULL){}\
        void SetGetter(C *la_clase, const  T &(C::*el_getter)(const T &)) { \
            this->la_clase=la_clase; this->el_getter=el_getter; \
        }\
        void SetSetter(C *la_clase, bool (C::*el_setter)(const T &)) { \
            this->la_clase=la_clase; this->el_setter=el_setter; \
        }\
        _atr operator=(const T &o) {\
            if (la_clase && el_setter) {\
                if ((la_clase->*el_setter)(o)) el_atributo=o;\
            } else el_atributo=o;\
            return *this;\
        }\
        operator const T(){ \
            if (la_clase && el_getter) \
                return (la_clase->*el_getter)(el_atributo); \
            return el_atributo; \
        } \
    }
    #define _setter(x,m) x.SetSetter(this,&m);
    #define _getter(x,m) x.SetGetter(this,&m);
La primera declara la clase wrapper genérica, y las otras asignan setters y getters. Para usarlo desde una clase cliente sería por ejemplo:
    #include<atributos_magicos.h>
    class Booga {
    public:
        _usar_atributos_magicos(Booga); // va una vez, para habilitar los _atr
        _atr<int> x; // propiedad entera x
        _atr<float> y; // propiedad flotante y
        _atr<double> z; // propiedad double z
        Booga() { // el ctor asigna setters y getters especiales
            _setter(x,Booga::SetX); // SetX actuará como setter de x
            _getter(y,Booga::GetY); // GetY actuará como getter de y
            _setter(z,Booga::SetZ); // SetZ actuará como setter de z
        }
        bool SetX(const int &x) {
            cout<<"Ejecutando el seter!!"<<endl;
            // la asignación la realiza la clase _atr si el setter dice "true"
            return true;
        }
        bool SetZ(const double &z) {
            cout<<"Ejecutando el seter!!"<<endl;
            return true;
        }
        const float &GetY(const float &y) {// el getter recibe el valor real
            cout<<"Ejecutando el getter!!"<<endl;
            return y;
        }
    };
Esta clase tiene tres atributos que simulan este tipo de propiedades. Y define dos setters y un getter propios, para el resto usa el comportamiento por defecto (se comportarán como si fueran atributos públicos). Como ven, el ruido necesario para usar las propiedades no es tanto: hay una linea obligatoria al principio, luego las propiedades se declaran fácilmente, y luego hay que asignar setters y getters en el constructor, pero también eso es simple con las macros. Pueden probarla con la siguiente función main:
    int main(int argc, char *argv[]) {
        Booga b;
        b.x=7; b.y=3.4; b.z=7.8;
        cout<<"El valor de x es: "<<b.x<<endl;
        cout<<"El valor de y es: "<<b.y<<endl;
        cout<<"El valor de z es: "<<b.z<<endl;
        return 0;
    }

En resumen, me divierten estos "desafíos sintácticos" aunque el resultado no siempre sea al final especialmente útil. Y en este caso se dio combinar varios trucos. Hay veces en estos trucos dan pié a cosas realmente útiles en la vida real (como las expression templates), y veces que no. Insisto en que si bien el resultado es interesante, le faltan detalles que no he analizado del todo (como las implicancias de ese casteo sobrecargado, o del acceder a los atributos dentro de la clase C, etc). Pero creo que igual puede abrir un poco la cabeza, o inspirarle a alguien alguna solución más particular a su problema.

Por último, quiero hacer una advertencia respecto al abuso de getters y setters, desde el punto de vista del diseño de clases, sin importar ya la sintaxis: una clase no debería ser simplemente un conjunto de atributos envueltos en setters y getters, no pasa por ahí la programación orientada a objetos. Pueden encontrar muchas discusiones interesantes al respecto (como esta).

2 comentarios:

  1. Interesantísimo esto!. Estuve buscando en internet mucho sobre este tema para hacerlo en C++, particularmente porque es muy cómodo trabajar de esta forma con getters y setters. En C# se hace realmente cómodo trabajar con las propiedades tanto autoimplementadas como totales. Sirve también para poder crear atributos que sean de solo lectura (solo get), solo escritura (solo set) o ambos. Cuento algo acerca del tema de las propiedades en C# por si alguien lee el post y quiere ir mas allá del código de Pablo:

    Las propiedades en C# se definen como si fueran métodos en los que se puede acceder a un miembro privado de la clase.

    class Clase{

    private 'tipo' _nombre_miembro; //Esta es la variable privada

    public 'tipo' Propiedad //Propiedad full o completa.
    {
    get { return _nombre_miembro; }
    set { _nombre_miembro = value;}
    }
    }

    Esa es la sintaxis de una propiedad en C#. Notar que cada propiedad tiene implícita una variable llamada value, en la cual viene el nuevo valor hacia la propiedad, valor que finalmente se asigna a la variable privada asociada... el uso es el siguiente:

    objClass.Propiedad = _algun_valor; //Aquí se asigna value y dentro de la propiedad value es asignado a la variable asociada.

    Console.Write(objClass.Propiedad); // La propiedad devuelve el valor de la variable asociada.

    Notar que si dentro de la propiedad se elimina el get o el set, se da lo que dije al principio, se convierte en solo escritura, o solo lectura. También se puede ver que dentro del set o del get, se puede hacer algún tipo de validación u otra cosa que sea pertinente a la asignación o devolución de la variable.

    Vemos que la forma de uso es la misma que finalmente se puede usar con el codigo de Pablo, que obviamente me voy a llevar para mis proyectos! =D Gracias Pablo!

    - nota al margen - yo lo habia solucionado creando métodos que devuelven por referencia la variable en cuestión:

    string& Nombre() { return _nombre; }

    lo usaba así objClass.Nombre()="seba"; algo totalmente raro jaja.

    Propiedades Autoimplementadas:

    Esto es algo que salio en C# 3.0 creo... Poder crear una propiedad dejando la creación de la variable privada implícita:

    public 'tipo' Propiedad { get; set; }

    al declararlo de esta forma creamos una variable privada (implícita) y su propiedad adjunta. Ventajas? escribir menos, desventajas? perdemos el poder implementar algún código dentro de get o de set. No se usa mucho y menos con el sistema de notificación de cambios de variables de C# para avisar a la interfaz cuando debe actualizarse. Ese sistema solo es viable con propiedades full.

    ResponderEliminar
    Respuestas
    1. Interesante el comentario. Pero esta implementación que presenté ahora me parece más un experimento teórico antes que algo que se justifique usar. A esta altura el post ya es algo viejo, creo que con C++11/14 se podría hacer algo mucho mejor... Tal vez usando funciones lambda para especificar más fácilmente los getters y setters, y algo de lo nuevo de templates para no abusar tanto del preprocesador... En algún momento voy a repensarlo y escribir una segunda versión de este post si encuentro algo útil.

      Eliminar