Implementar drivers de impresora en Visual Basic 2005

Muchas veces en nuestras aplicaciones tenemos la necesidad de utilizar impresoras no estándar como por ejemplo, impresoras de tickets o controladores fiscales. Estos simpáticos aparatos frecuentemente son incompatibles entre sí, lo cual para nosotros significa escribir código que maneje cada modelo individualmente. En sistemas comerciales, donde el soporte para distintos modelos de una misma entidad (en este caso la impresora) es un imperativo, la situación se nos complica un poco.


Una solución posible sería guardar en una variable el tipo de impresora que el usuario ha configurado, y en el momento de imprimir dibujar un gran Select Case, que según la impresora, invoque a una u otra rutina cuyo código implementará la lógica particular de cada periférico. Esta solución, además de ser horrible, tiene un problema muy concreto: no es escalable. Cuando el sistema crezca, probablemente la lista de impresoras soportadas también se extienda y nuestro Select Case, junto con las rutinas a las que llama, se convertirán en monstruos inmantenibles. Algo así, por ejemplo:


Private Sub ImprimirALaViejaUsanza(ByVal Texto As String)
Select Case mTipoImpresora
Case "Impresora1"
ImprimirEnImpresora1(Texto)
Case "Impresora2"
ImprimirEnImpresora2(Texto)
Case "Impresora3"
ImprimirEnImpresora3(Texto)
Case "Impresora4"
ImprimirEnImpresora4(Texto)
Case "Impresora5"
ImprimirEnImpresora5(Texto)
Case "Impresora6"
ImprimirEnImpresora6(Texto)
Case "Impresora7"
ImprimirEnImpresora7(Texto)
Case "Impresora8"
ImprimirEnImpresora8(Texto)
Case "Impresora9"
ImprimirEnImpresora9(Texto)
End Select
End Sub


En los tiempos de la programación estructurada ésta era la única opción para manejar distintas periféricos, pero hoy existen formas mucho más elegantes, que pueden prevenir la esquizofrenia por lectura de código spaghetti. Por eso, para los iniciados en las artes de la programación orientada a objetos mostraremos otra solución utilizando polimorfismo por herencia.

A trabajar

Para mostrar el ejemplo, construiremos una pequeña aplicación tipo Notepad, que solamente tendrá funcionalidad para imprimir y seleccionar impresora. Para que todo quede prolijito, pondremos la interfaz de usuario en un proyecto tipo “WinForms” al cual llamaremos DriversFront, y la lógica de manejo de impresoras en un proyecto aparte de tipo “librería de clases”, el cual titularemos DriversBack. El proyecto DriversFront tendrá una referencia al proyecto DriversBack, para utilizar los servicios y las clases contenidas en él.

Una vez que tenemos el esqueleto de la solución, comenzaremos a trabajar sobre la arquitectura. La idea es diseñar un objeto impresora genérico que tenga la funcionalidad de base que requeriremos a nuestros drivers. En nuestro caso, implementaremos tres métodos: Abrir(), Cerrar() e Imprimir(Texto), este último requerirá como parámetro el texto a ser impreso. Para nuestro ejemplo, el código sería así:


Public MustInherit Class ImpresoraBase
'''
''' Abre e inicializa los recursos que utilizará la impresora
'''
Public MustOverride Sub Abrir()
'''
''' Imprime el texto enviado como parámetro
'''
Public MustOverride Sub Imprimir(ByVal Texto As String)
'''
''' Cierra y libera los recursos de la impresora
'''
Public MustOverride Sub Cerrar()
End Class

¿Simple no? Esta es nuestra clase base, su modificador MustInherit determina que no puede ser instanciada directamente sino que deberá ser extendida. A expensas de simplificar el ejemplo la redujimos a sólo tres métodos, una clase del mundo real probablemente ostentaría algunas propiedades, campos e incluso podríamos escribir métodos para que sean “vistos” solamente por sus clases hijas (protected). Como decía, de esta clase derivaremos otras que se ocuparán del manejo de cada impresora particular. Entonces, para una impresora de tipo X corresponderá una clase ImpresoraX que heredará de ImpresoraBase y que obligatoriamente (gracias al MustOverride) deberá escribir código para los métodos Abrir, Imprimir y Cerrar. Veamos la realización de una impresora virtual que escribirá la salida en un archivo de texto:

Public Class ImpresoraArchivo
Inherits ImpresoraBase
'este campo es el archivo que utilizaremos para imprimir
Dim strArchivo As IO.StreamWriter
'nombre del archivo de salida
Const NOMBREARCHIVOSALIDA As String = "ImpresionArchivo.txt"

Public Overrides Sub Abrir()
'abrimos el archivo
strArchivo = New IO.StreamWriter(String.Format("{0}{1}", ObtenerDirectorioDeEjecucion, NOMBREARCHIVOSALIDA), True)
End Sub

Public Overrides Sub Cerrar()
strArchivo.WriteLine()
strArchivo.WriteLine("__________________")
strArchivo.Close()
End Sub

Public Overrides Sub Imprimir(ByVal Texto As String)
strArchivo.Write(Texto)
strArchivo.Flush()
End Sub

Private Function ObtenerDirectorioDeEjecucion() As String
Dim DirEjecucion As String = My.Application.Info.DirectoryPath
'si no termina en contrabarra, la agregamos
If DirEjecucion.EndsWith("\") Then
Return DirEjecucion
Else
Return String.Format("{0}\", DirEjecucion)
End If
End Function
End Class



Diagrama de clases
Aquí vemos la estructura de herencia de nuestros drivers. Las tres clases derivadas implementan Abrir, Cerrar e Imprimir, aunque también pueden contener otros métodos, campos y propiedades.

Lo que vemos aquí, señoras y señores, es el fruto de la herencia. La clase ImpresoraArchivo es un vástago de ImpresoraBase, que para no ser desheredado ha debido implementar los métodos Abrir(), Cerrar() e Imprimir(Texto), pero con derecho a utilizar su propia y particular manera. En este caso, la impresora en realidad abre un archivo en el disco y escribe la salida en él. En una aplicación real quizás este driver no serviría de mucho, (salvo para hacer pruebas de impresión sin gastar papel) pero ahora veremos que lo realmente valioso del ejemplo es la arquitectura usada, que permite a cada hijo manejarse de manera independiente, utilizando su propia lógica -que puede ser muy distinta a la de sus “hermanos”- para realizar la tarea. Observen que la clase hija puede también incorporar funcionalidad adicional, por ejemplo ImpresoraArchivo tiene un método privado adicional –ObtenerDirectorioDeEjecucion()- por el cual dilucida el path de ejecución del sistema. Las clases hijas tienen la obligación de implementar ciertos caracteres que mandan sus padres, pero nada les impide desarrollar nuevos métodos, campos y propiedades, incluso pueden ampliar la interfaz con métodos y propiedades públicas (aunque, como veremos más adelante, esto no nos servirá de mucho en este tipo de arquitectura).
Volviendo al ejemplo, hemos desarrollado tres drivers: una impresora virtual a archivo, otra impresora virtual a pantalla (el cual abre un WinForm y escribe en él la salida de la impresora, inútil pero didáctico) y un tercero que sería el hermano vago ya que no hace nada. En realidad este último se comporta como impresora nula, y lo utilizaremos para el caso en que el usuario decida trabajar sin impresora.

Como vemos, la implementación de cada driver puede manejarse fácilmente, sólo es cuestión de heredar del padre correcto (evitemos la comparación con la vida real), y escribir la funcionalidad deseada en la plantilla. La cuestión a resolver ahora es cómo imprimiremos en nuestra impresora. Aquí echaremos manos a la magia de los objetos. El truco es utilizar Polimorfismo, de esta manera nuestra aplicación declarará un objeto de tipo ImpresoraBase, pero en el momento de ser instanciado su lugar será tomado por uno de sus hijos. Esto lo veremos en un momento, ahora déjenme mostrarles cómo usaremos nuestra impresora:

   Private mImpresoraSeleccionada As DriversBack.ImpresoraBase

Private Sub mnuImprimir_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles mnuImprimir.Click
If mImpresoraSeleccionada IsNot Nothing Then
Try
mImpresoraSeleccionada.Abrir()
mImpresoraSeleccionada.Imprimir(TextBox1.Text)
Catch ex As Exception
MessageBox.Show(String.Format("Se produjo un error al imprimir: {0}", ex.Message), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
Finally
mImpresoraSeleccionada.Cerrar()
End Try
Else
MessageBox.Show("Para imprimir primero seleccione una impresora.", "Atención", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
End If
End Sub

Simplemente, llamamos al método Abrir() de nuestro objeto ImpresoraBase declarado en la primera línea, imprimimos el texto con Imprimir(TextBox1.Text) y la cerramos en el Finally con Cerrar(). S-I-M-P-L-E. La belleza de este método es que, sin importar si el sistema soporta una impresora o un centenar, la rutina se mantiene inmutable ya que generalmente los cambios se harán en la implementación de cada driver. Probablemente, el interior del método Abrir() para una impresora determinada es un infierno; puede ser necesario abrir puertos, chequear configuración, si está activa, tiene papel o está en error. Para imprimir, algunas impresoras de tickets usan lenguajes propietarios; entonces en el método Imprimir(Texto) de esas impresoras deberemos traducir el texto al lenguaje de cada engendro particular. No obstante, desde el punto de vista del consumidor de la clase, el llamador sabe que lo único que tiene que hacer para imprimir un texto es invocar el método que tan responsablemente los hijos de ImpresoraBase se han comprometido a resolver, de una manera o de otra.

La belleza de este método es que, sin importar si el sistema soporta una impresora o un centenar, la rutina se mantiene inmutable ya que generalmente los cambios se harán en la implementación de cada driver.

El patrón

Lo único que nos queda por resolver es la forma de instanciar la clase. Es decir, cómo haremos para que, en tiempo de ejecución se pueda utilizar una impresora u otra de acuerdo a las necesidades de cada usuario. Para ello utilizaremos una clase adicional llamada, no caprichosamente, ImpresoraFactory. ¿Porqué le pusimos este nombre? Porque en realidad, lo que estamos haciendo aquí corresponde a un patrón de arquitectura, más específicamente el apodado Simple Factory (ver recuadro). En el contexto de este patrón, la responsabilidad de la clase Factory es proveer servicios para instanciar la clase correspondiente en tiempo de ejecución. Ésta puede publicar algún tipo de matriz o colección de claves cuyos elementos se usarán en el método de creación para instanciar el objeto. En nuestro ejemplo, así quedaría nuestra clase ImpresoraFactory.

Public Class ImpresoraFactory
Private Shared mTiposDeImpresora As String() = {"Impresora Virtual a archivo", "Impresora Virtual a pantalla", "Sin impresora"}

Public Shared ReadOnly Property TiposDeImpresora()
Get
Return mTiposDeImpresora
End Get
End Property

Public Function InstanciarImpresora(ByVal TipoDeImpresora As String) As ImpresoraBase
Select Case TipoDeImpresora
Case mTiposDeImpresora(0)
Return New ImpresoraArchivo
Case mTiposDeImpresora(1)
Return New ImpresoraPantalla
Case Else
'no se ha configurado ninguna impresora
Return New ImpresoraNula
End Select
End Function
End Class


Como decíamos, la propiedad TiposDeImpresora nos devuelve una matriz que representa todas las impresoras disponibles. Con el método InstanciarImpresora(TipoDeImpresora) obtenemos a cambio de uno de los elementos del array, un objeto hijo de ImpresoraBase, que como vimos, tiene todo lo necesario para que podamos imprimir. Desde el consumidor de la clase el código para crear el objeto se vería así:

   Private Sub mnuConfig_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles mnuConfig.Click
Dim fconf As New frmSeleccionarImpresora
fconf.ShowDialog()

Dim ImprFactory As New DriversBack.ImpresoraFactory
mImpresoraSeleccionada = ImprFactory.InstanciarImpresora(fconf.ImpresoraSeleccionada)
End Sub


Resumiendo, en el momento de llamar a InstanciarImpresora(TipoDeImpresora), le pasaremos como parámetro el String correspondiente a la impresora que ha seleccionado el usuario y este nos devolverá la impresora indicada. Esta será la única clase que tendremos que modificar en el caso de agregar más impresoras -además de obviamente construir la clase hija que la manejará- poniendo más valores en el arreglo de strings, y reconociendo el tipo de impresora pedido para la instanciación. Con el tiempo, el código de InstanciarImpresora(TipoDeImpresora) podría crecer, pero es el único lugar donde tendríamos que manejarnos con delicadeza. Si lo comparamos con el escenario inicial, donde para cada tipo de impresora debíamos escribir funciones distintas, creo que hemos dado un salto cualitativo.

Para terminar

En este artículo hemos visto una forma de utilizar la herencia para hacer más claro y manejable el código de manejo de impresoras en nuestras aplicaciones. No obstante, esta arquitectura puede utilizarse para cualquier tipo de periféricos. Por ejemplo, podría adaptarse para manejar controladoras fiscales. En este caso la clase base debería ser un poco más compleja ya que se necesitarían AbrirTicketFiscal, AgregarItem, AgregarPago, etc; sin embargo, el patrón sería el mismo (y justamente ahí está la razón de ser de los patrones). Otros destinos podrían ser: servicios de validación de transacciones, autorizaciones de tarjetas de crédito on-line o internas, salidas a displays de caracteres, etc. Siempre es deseable que este tipo de funcionalidad sea implementada con drivers, ya que aunque en el presente nuestro sistema trabaje con un solo tipo de aparato, nunca sabemos cuando podría requerir soporte otras marcas o modelos y lo mejor es, como un buen boy-scout, estar siempre listo ;-)

Para ver el artículo publicado en la página del Guille y bajar el ejemplo, hacé click aquí


SOBRE PATRONES DE ARQUITECTURA

Extraído de la arquitectura clásica, en arquitectura de software un patrón: “describe un problema que ocurre una y otra vez en nuestro entorno, y además, describe el núcleo de la solución a ese problema, de tal manera, que podemos usar esa solución un millón de veces más en el tiempo, sin que tenga que ser la misma cada vez”. La esencia de los patrones es su reutilización y el rápido reconocimiento de una solución a un problema típico, de esta manera se ahorra tiempo “reinventando la rueda”, y se puede manejar un corpus de conocimiento compartido entre los desarrolladores.
El patrón utilizado en esta nota suele llamarse Simple Factory y es considerado una simplificación de Abstract Factory y Factory Method, sin embargo los tres comparten la misma base conceptual. Se trata de un patrón de creación, cuyo objetivo es producir objetos cuyos tipos no se pueden prever hasta el momento de la ejecución. Su particularidad consiste en una clase (generalmente llamada Factory) en la cual se define un método (el Factory Method) a través del cuál se crean instancias de otras clases.



Comentarios

Anónimo dijo…
Que tal

Me encontre este articulo publicado en la revista .Code, dejame decirte que es muy bueno y me ha sido de bastante utilidad

Normalmente tengo que hacer uso de impresoras portatiles o de etiquetas y debo reconocer que hacia uso del modelo estructurado que mencionas al principio de tu articulo :P
Diego Cofré dijo…
Muchas gracias por tu comentario. Y me alegro de que estos conceptos te hayan servido para hacer mas manejable tus sistemas.
Carlos dijo…
Oye disculpa, tal vez sepas como incluir una impresora virtual creada, que no sea ya de las q hay en internet, osea yo estoy desarrolando eso pero mi problema es como insertarlo en mis impresoras tal vez sepas parte codigo agradesco tu ayuda