Colaboraciones en el Guille

Implementar drivers de impresora en Visual Basic 2005

 

Fecha: 05/May/2006 (03/05/2006)
Autor: Diego Cofré - diegocofre @ hotmail . com (sacar espacios)

 


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á la funcionalidad de 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 otro de tipo “librería de clases” que se titulará DriversBack.

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
    ''' <summary>
    ''' Abre e inicializa los recursos que utilizará la impresora
    ''' </summary>
    Public MustOverride Sub Abrir()
    ''' <summary>
    ''' Imprime el texto enviado como parámetro
    ''' </summary>
    Public MustOverride Sub Imprimir(ByVal Texto As String)
    ''' <summary>
    ''' Cierra y libera los recursos de la impresora
    ''' </summary>
    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

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.

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.

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.

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 setear 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. Esta clase 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

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 extenderse a otro 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 ;-)

 


Espacios de nombres usados en el código de este artículo:

System.IO
System.Windows.Forms

 


Fichero con el código de ejemplo: diegocofre_Drivers.zip - (41,5) KB

(MD5 checksum: [A2A4E515F7DDC4FD45A437D08D327F11])


ir al índice principal del Guille