10 de enero de 2018

Patrones de Diseño - Singleton

Motivado por los artículos de Andy Kramek, que ha traducido Ana María Bisbé York, intenté adaptar los patrones de diseño a mis desarrollos en VFP. La verdad, me ha dado buen resultado. Los patrones de diseño, aunque generan cierta complejidad agregada (que no es tal cuando se hace costumbre), hacen maravillas respecto a modificabilidad, rendimiento, disponibilidad, entre otros atributos de calidad. Ahora quisiera compartir mi propia experiencia luego de una investigación sobre un patrón en particular.

En determinado momento, me di cuenta que una de mis clases desarrolladas se adaptaba a una forma de patrón de diseño: el patrón Singleton. Puse manos a la obra para buscar la forma más eficiente de implementarlo (empezando por los artículos de Andy Kramek, obviamente), pero no encontré demasiado sobre éste en particular.

El patrón de diseño Singleton es usado para implementar el concepto matemático de Singleton (Conjunto unitario o con un solo elemento), restringiendo la instanciación de clases a un único objeto con un punto de acceso global. El carácter restrictivo respecto a la instanciación lo ubica dentro de la categoría de Patrones Creacionales. Es un patrón útil de aplicar para mejorar el rendimiento, por permitir solamente una instancia, y para unificar determinados procesos. Algunos ejemplos de componentes que podrían verse como Singleton serían: el Administrador de memoria del sistema; la cola de impresión del sistema, que debería ser única, aún cuando existan varias impresoras conectadas; los componentes de acceso a determinado dispositivo; los componentes de acceso a archivos compartidos; entre otros.

Es fácil de decir, pero no de hacer, sobre todo en VFP. Se puede buscar en la web y aparecerán infinidad de implementaciones, pero, la mayoría, con lenguajes de programación como C++, Java, Visual Basic, C#, entre otros, que soportan la definición de clases con miembros estáticos (o miembros de clase, propiamente dicho). Este tipo de miembro hace referencia a atributos o métodos que dependen de la propia clase, no de las instancias (objetos) que se creen de la misma. Es decir, un miembro estático de una clase A es común a todos los objetos de tipo A.

Las premisas más comunes para la implementación son las siguientes:

  • Mantener una referencia privada (un atributo de clase) que apunte hacia una instancia global única.
  • Ocultar el método constructor (el que crea una instancia de una clase) para que los objetos no puedan crearse explícitamente.
  • Publicar un método de clase para la obtención de referencias, el cual debe invocarse en vez del constructor, y que debe devolver referencias de la misma instancia para todas las invocaciones.

Algunas referencias para quienes no acostumbren a interpretar UML:

  • Los miembros subrayados son los miembros de clase o estáticos;
  • El caracter a la izquierda de cada miembro ("+" o "-") indica la visibilidad del mismo (pública u oculta, respectivamente);
  • Luego de ":" se indica el "tipo" de datos del atributo o del valor devuelto por un método;

Ahora bien, VFP no soporta alguna de estas características, como ya dijimos. Por lo tanto, para implementar Singleton, habría que buscar alguna estrategia que lo simule. Lo primero que se me ocurrió fue definir desde dentro de la clase una variable pública que tuviera la instancia única de la clase y se utilice esa instancia cuando sea necesario. En esa etapa de la investigación fue cuando me encontré con un artículo de Martín Salias (en inglés), el cual se puede consultar en: Design Patterns 6 - Singleton

Básicamente, lo que propone el artículo es utilizar una propiedad de _Screen para almacenar la referencia global a la instancia Singleton. Cada vez que se invoca al evento Init, se verifica si: 1) esa propiedad existe, 2) es un objeto y 3) es una instancia de la clase que debe ser Singleton. Si eso ocurre, retorna Falso (.F.) en el evento Init y NO crea una nueva instancia (lo que simula el ocultamiento del constructor). Para solucionar la creación del objeto, se proporciona un método GetInstance() para invocar desde _Screen.oSingleton (la referencia global) que devuelve una nueva referencia a la misma instancia de la clase Singleton. El objeto en memoria sigue siendo único, aunque se accede a él a través de referencias distintas.

Este artículo me ayudó a definir ciertas cosas, como la forma de almacenar la referencia global (como una propiedad de _Screen). Sin embargo, todavía existían dos inconvenientes:

  • El alto acoplamiento. Desde la aplicación cliente se debe conocer la referencia global _Screen.oSingleton.
  • La interoperabilidad. Mi intención era compilar la clase como una librería DLL y utilizarla posiblemente en otros lenguajes de programación. Esto dificulta la posibilidad de devolver .F. en Init (ya que los lenguajes fuertemente tipados no aceptarían que el constructor no les devuelva un objeto) y que no podría acceder a la referencia global (no quedaría disponible la referencia _Screen), lo cual es salvable definiendo una propiedad pública con una referencia a _Screen.

Para resolver estas cuestiones, se me ocurrió utilizar otro patrón de diseño: El Envoltorio. El mismo se basa en implementar una clase que envuelva a determinado componente, es decir, el envoltorio controla el contenido y publica la interfaz disponible, pero la implementación de los métodos publicados la realiza el componente cubierto. Para más información sobre el patrón de diseño Envoltorio, se puede revisar el siguiente artículo de Andy Kramek (traducido por Ana María Bisbé York): Patrones de diseño - Adaptadores y Envoltorios

En este punto, contamos con dos clases a definir:

  1. La clase a implementar como Singleton, que no debería instanciarse explícitamente desde las aplicaciones cliente.
  2. y la clase Envoltorio que sí estaría disponible para instanciar y debería encargarse de dos aspectos:
    • Crear la instancia Singleton (y almacenar la referencia en una propiedad agregada a _Screen).
    • Responder a las invocaciones de métodos. En realidad, va a obtener referencias a la clase Singleton y va a pasar la llamada al método para que lo resuelva la instancia única.

A continuación se muestra un ejemplo de definición de una clase envoltorio (ClaseUnica) y una clase Singleton (ClaseUnicaSingleton):

DEFINE CLASS ClaseUnica as Session OLEPUBLIC
* Envoltorio de la clase que debe ser implementada como Singleton.

 HIDDEN FUNCTION obtenerInstanciaSingleton
 * Descripción: Método que devuelve una referencia a la instancia única (Singleton) de tipo ClaseUnicaSingleton.

 * Se chequea si existe la propiedad que almacena la referencia Singleton, si la misma es de tipo objeto
 * y si el tipo de objeto concuerda con el Singleton (ClaseUnicaSingleton.
 IF !(PEMSTATUS( _Screen, "oClaseUnicaSingleton", 5 ) ;
  and Vartype( _Screen.oClaseUnicaSingleton ) == "O" ;
  and ALLTRIM(UPPER(_Screen.oClaseUnicaSingleton.Class)) == "CLASEUNICASINGLETON")

  _Screen.AddProperty( "oClaseUnicaSingleton", CREATEOBJECT("ClaseUnicaSingleton") )
 ENDIF

 * Se verifica el posible retorno. Si se pudo crear una instancia correcta, se la retorna, sino se arroja una excepción.
 IF Vartype( _Screen.oClaseUnicaSingleton ) == "O" AND ALLTRIM(UPPER(_Screen.oClaseUnicaSingleton.Class)) == "CLASEUNICASINGLETON"
  RETURN _Screen.oClaseUnicaSingleton
 ELSE
  THROW "Error de instanciación de la Clase única"
 ENDIF

 * Función que envuelve a la función getPropiedad de ClaseUnicaSingleton.
 PROCEDURE getPropiedad() as String
  LOCAL loSingle as ClaseUnicaSingleton
  loSingle = this.obtenerInstanciaSingleton()

  RETURN loSingle.getPropiedad()
 ENDPROC
 
 * Función que envuelve a la función setPropiedad de ClaseUnicaSingleton.
 PROCEDURE setPropiedad(cProp as String)
  LOCAL loSingle as ClaseUnicaSingleton
  loSingle = this.obtenerInstanciaSingleton()

  loSingle.setPropiedad(cProp)
 ENDPROC

ENDDEFINE


DEFINE CLASS ClaseUnicaSingleton as Custom
* Clase que debe implementarse como Singleton (debe existir una única instancia).

 HIDDEN propiedad
 
 PROCEDURE getPropiedad() as String
  RETURN this.Propiedad
 
 PROCEDURE setPropiedad(cProp as String)
  this.Propiedad = cProp

ENDDEFINE

Algunas conclusiones respecto a esta implementación:

  • Ventaja: Es posible instanciar varias veces a ClaseUnica, pero ClaseUnicaSingleton será instanciada una única vez. Esto se consigue producto del encapsulamiento de ClaseUnicaSingleton por parte de ClaseUnica.
  • Ventaja: Se logra desacoplar al cliente de la clase Singleton, porque ya no necesita conocer cuál es la referencia global a acceder (_Screen.oClaseUnicaSingleton, en este caso), sino que siempre utiliza su propia instancia del envoltorio de forma transparente. Este es otro caso de encapsulamiento.
  • Ventaja: Esta es una estructura compatible con otros lenguajes en caso de que se quiera compilar como DLL, ya que los procesos de construcción terminan siendo transparentes. La construcción del envoltorio siempre devuelve un objeto y la construcción del Singleton queda encapsulada por el envoltorio.
  • Desventaja: Es necesario escribir más código que con las demás formas de implementación: Definir dos clases y definir dos veces cada método de la interfaz (una vez en el envoltorio y otra en la clase Singleton).
  • Salvedad: Se debe ser cuidadoso con la definición del nombre de la propiedad agregada a _Screen, para que no sea sobrescrita.
  • Salvedad: Existe un límite para la implementación de este patrón: tiene alcance de sesión. Es decir, aunque el componente sea externo a la aplicación cliente, como en el caso de una DLL, la instancia será única solamente dentro del espacio de memoria de esa aplicación. Si se intenta instanciar desde dos ejecutables distintos (o desde dos entornos de Fox distintos, por ejemplo) se obtendrían dos instancias Singleton distintas, cada una única solo en su entorno.

Si quiere probar esta implementación, se podría armar un PRG con las siguientes líneas, agregando las definiciones de clases mostradas anteriormente:

LOCAL oRef1 as ClaseUnica, oRef2 as ClaseUnica

oRef1 = CREATEOBJECT("ClaseUnica")
oRef2 = CREATEOBJECT("ClaseUnica")

oRef1.setPropiedad("El valor debería ser igual para ambas instancias.")

MESSAGEBOX(oRef1.getPropiedad(), 0, "Referencia 1")
MESSAGEBOX(oRef2.getPropiedad(), 0, "Referencia 2")

***** Agregar las definiciones de ClaseUnica y ClaseUnicaSingleton *****

Pablo Lissa

No hay comentarios. :

Publicar un comentario

Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.