4 de abril de 2018

Interfaces para objetos COM

En ciertas ocasiones, cuando se diseña con Orientación a Objetos, es útil separar las responsabilidades de una clase en dos o más clases distintas. Con esto se logra que una entidad exponga los lineamientos a seguir, pero, pueda desligarse de la forma de llevarlos a cabo, delegando esas responsabilidades en otras clases. Es más, existe un Patrón de Diseño que así lo aconseja: El Puente.

Otro caso típico, por ejemplo, se da con los servidores COM. Un objeto compilado en una DLL puede realizar sus funciones básicas o capturar sus eventos, pero, es útil que otro objeto encapsule las implementaciones de su funcionamiento para adaptar esa funcionalidad a los proyectos clientes de ese servicio, a la plataforma donde corre, etc. Luego, el objeto COM debe permitir delegar cierta funcionalidad a un objeto externo (no compilado en la misma DLL).

Para llevar esto adelante es necesario que se cumplan ciertas condiciones: Un contrato entre los objetos en cuestión. Esto se logra mediante las interfaces. En Programación Orientada a Objetos (POO), las interfaces son tipos de datos abstractos (clases abstractas), que exponen o publican los miembros públicos que deben contener otras clases para implementar dicha interfaz. Es decir, las interfaces son como listas de mensajes a los cuales debe responder un objeto de determinada clase, sin indicar cómo responder (no implementan funcionalidad).

Por ejemplo, se puede contar con una clase que encapsule un Array de objetos, a la cual podríamos llamar Arreglo, y con una interfaz llamada Pila que expone los métodos push (apilar) y pop (retirar o desapilar). Luego, podríamos querer definir una nueva clase basada en Arreglo: ArregloPila. Si a esta nueva clase se le definen los métodos push y pop, luego, podemos decir que ArregloPila "implementa" la interfaz Pila.

En el ejemplo anterior, se muestra como una de las funcionalidades de las interfaces es la de simular herencia múltiple, aspecto que algunos lenguajes, entre ellos VFP, no soportan. ArregloPila pareciera ser una clase heredera de Arreglo y de Pila, y en realidad es solamente heredera de la primera. Por otro lado, las interfaces sirven para validar que un objeto de una clase implemente lo necesario para cumplir con determinada funcionalidad. Por ejemplo, si se desea administrar las acciones que se vayan realizando en un programa para dar la posibilidad de deshacer esas acciones (siempre comenzando desde la última realizada), es conveniente que la estructura donde se almacenen esas acciones sea una pila. Es decir, será necesario verificar que un posible objeto llamado oAcciones sea de una clase que "implemente" la interfaz Pila.

VFP no soporta el uso de interfaces de forma nativa. Es un buen momento para aclarar que esta premisa se refiere a las interfaces puras de la POO, definidas con la misma categoría que las clases. En realidad, cada vez que se está definiendo una clase, todos aquellos miembros públicos son la interfaz de ella. Sin embargo, las interfaces declaradas como clases abstractas pueden ser útiles a la hora de documentar para especificar determinado comportamiento o clasificación de clases.

El uso de interfaces en VFP se puede simular mediante el análisis de la estructura interna de las clases, como lo ha hecho Víctor Espina, en un artículo publicado recientemente: Interfaces en VFP

Existe una excepción a estas restricciones que estamos nombrando: Los objetos COM. VFP incluye una serie de métodos y cláusulas que permiten implementar interfaces definidas en objetos COM, dependiendo de la forma en que se haya diseñado. Un ejemplo de esto se encuentra en Visual FoxPro Callback Design (Microsoft Visual FoxPro and Advanced COM)

La idea es definir 3 clases:

  • La clase principal del objeto COM.
  • Una clase que funcione como interfaz de los métodos delegados por la clase principal. Debe definir, sin implementar, los métodos que se delegarán.
  • Una clase externa que implemente los métodos que publica la interfaz.

A continuación se muestra un ejemplo implementado como dos PRG, de los cuales, el primero debe incluirse en un proyecto y compilarlo como DLL, mientras que el segundo PRG puede ejecutarse de cualquier forma posible. El alcance de este artículo no contempla la compilación de proyectos como DLL, por lo que si necesita ayuda con ese paso, se recomienda el siguiente artículo: Crear Servidores de Automatización

*------- Primer PRG: Incluir en un proyecto y compilarlo como DLL -------*
*************************
******* Clase COM *******
*************************

DEFINE CLASS ClaseOLE AS Session OLEPUBLIC
HIDDEN oImplementador
HIDDEN cPropiedad as String

PROCEDURE Init()
 this.oImplementador = .Null.
ENDPROC

*** Métodos para setear y recuperar la propiedad ***
PROCEDURE setPropiedad(cProp as Variant)
 IF ISNULL(this.oImplementador) ;
  OR (!ISNULL(this.oImplementador) AND this.oImplementador.ValidarAntesDeSetear(cProp))

  this.cPropiedad = cProp
 ELSE
  COMRETURNERROR("Valor incorrecto", "El valor ingresado no cumple con la validación")
 ENDIF
ENDPROC

PROCEDURE getPropiedad() as Variant
 IF !ISNULL(this.oImplementador)
  this.oImplementador.mostrar(this.cPropiedad)
 ENDIF

 RETURN this.cPropiedad
ENDPROC

*** Asociación del objeto COM con un objeto externo que implementa la interfaz InterfazImplementador ***
PROCEDURE setImplementador(oImplementadorExterno as Variant)
this.oImplementador = GETINTERFACE(oImplementadorExterno, "iInterfazImplementador", "interfacesCOM.ClaseOLE")
ENDPROC

ENDDEFINE


************************************************
******* Clase Interfaz con el objeto COM *******
************************************************
DEFINE CLASS InterfazImplementador as session OLEPUBLIC

PROCEDURE mostrar(cProp as String)
ENDPROC

PROCEDURE validarAntesDeSetear(cProp as Variant) as Boolean
ENDPROC

ENDDEFINE

*------- Segundo PRG: Cliente del objeto COM -------*
* Verificar, de ser necesario, las rutas.

LOCAL loImp as ImplementadorExterno
LOCAL loObjetoCOM as ClaseOLE

loImp = CREATEOBJECT("ImplementadorExterno")
loObjetoCOM = CREATEOBJECT("interfacesCOM.ClaseOLE")
loObjetoCOM.setImplementador(loImp)

TRY
 * Ejecución exitosa. Muestra "Hola Mundo"
 loObjetoCOM.setPropiedad("Hola Mundo")
 loObjetoCOM.getPropiedad()

 * Ejecución errónea. Muestra un error.
 loObjetoCOM.setPropiedad(DATE())
 loObjetoCOM.getPropiedad()
 
CATCH TO loError
 MESSAGEBOX(loError.message, 16, "Error")
ENDTRY

RETURN


********************************************************************************************************************************
******* Objeto que implementa parte de la funcionalidad de la clase ClaseOLE (Implementa la interfaz InterfazImplementador) *******
********************************************************************************************************************************
DEFINE CLASS ImplementadorExterno AS Custom
IMPLEMENTS iInterfazImplementador IN "interfacesCOM.dll"

*** Método validarAntesDeSetear ***
PROCEDURE iInterfazImplementador_validarAntesDeSetear(cProp as Variant) as Boolean
 RETURN VARTYPE(cProp) $ "CN"  && La propiedad debe ser caracter o numérica.
ENDPROC

*** Método mostrar ***
PROCEDURE iInterfazImplementador_mostrar(cProp as Variant)
 MESSAGEBOX(cProp)
ENDPROC

ENDDEFINE

Como se puede ver, la clase ClaseOLE está diseñada para delegar parte de su funcionamiento a un objeto externo, de la clase ImplementadorExterno en este caso, que implementa los métodos definidos por la interfaz InterfazImplementador. Con todo esto se logran ciertas ventajas:

  • Es fácil extender la funcionalidad y utilización de ClaseOLE. En el ejemplo, el procedimiento validarAntesDeSetear() permite cambiar las validaciones sin tener que recompilar la DLL. Además, los objetos externos pueden ser escritos en cualquier lenguaje, lo que permitiría que dichas validaciones se ajusten al lenguaje.
  • Se logra adaptar el funcionamiento del objeto COM a la plataforma que lo consume. En el caso del ejemplo, la función MESSAGEBOX() generaría un error si se intentara ejecutar desde la DLL. Luego, esa parte se delega.
  • Es útil este modelo para procesar eventos que captura el objeto COM.

Ahora, algunas preguntas y respuestas rápidas sobre el modelo:

  1. ¿Se puede omitir la escritura de alguno de los métodos que expone la interfaz en la clase que la implementa? No, deben escribirse todos los métodos publicados (aunque se dejen vacíos en su implementación). La cláusula IMPLEMENTS obliga a cumplir con el protocolo de la interfaz. Si alguno de los métodos se omite, al intentar realizar la creación del objeto externo (CREATEOBJECT("ImplementadorExterno") en este caso), se lanzaría un error indicando los métodos faltantes.
  2. ¿Qué pasa si no se incluye la cláusula IMPLEMENTS en la definición de la clase externa? La invocación a loObjetoCOM.setImplementador() arrojaría error (al llegar a la función GETINTERFACE()) ya que se valida que el objeto implemente la interfaz solicitada de forma explícita.
  3. Luego, ¿se podría omitir el uso de IMPLEMENTS y GETINTERFACE(), y hacer la asociación de los objetos directamente? Sí, y parecería, incluso, mucho más sencillo de implementar porque no sería necesario explicitar ciertas líneas de código y se podría desarrollar una clase menos. Sin embargo, incluyendo este manejo de interfaces se logra mayor orden:
    • Por un lado, se incluye implícitamente una validación de que el objeto externo cumple con el protocolo impuesto por la interfaz. Esto asegura que los métodos llamados por el objeto COM existen y no se va a generar un error.
    • Por otro lado, es la forma más eficiente de indicar qué métodos se deben escribir. En el ejemplo se ve que los métodos a escribir no precisamente concuerdan con los nombres de los métodos de ClaseOLE.

Pablo Lissa


Artículos relacionados:


No hay comentarios. :

Publicar un comentario

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