viernes, 2 de diciembre de 2011

Ha nacido GLYPY The Hero

Hoy he recibido la notificación de que mi primera aplicación para Windows Phone 7 ha pasado el proceso de certificación y está disponible desde hoy en el marketplace de Microsoft.

Todo empezó como una simple prueba con XNA y C# pero al cabo de algún tiempo se ha convertido en  un juego completo, de principio a fin.

No creo que me haga rico con esto de los juegos pero me estoy divirtiendo mucho en el camino. :-)

GLYPY The Hero es un típico juego de plataformas en el que se “aplastan” bicharracos y se recogen cosas. Aquí os dejo una captura de pantalla para que os hagáis una idea.

image

Aquí dejo el enlace por si queréis probarlo.

 wp7_152x50_green

lunes, 12 de enero de 2009

Un ejemplo práctico y III

En esta tercera y última entrega del ejemplo práctico pretendo:

  • Teorizar sobre los diferentes enfoques al problema de carga de entidades en un diseño orientado a objetos.
  • Profundizar en el diseño de la clase DataLoader y en sus mecanismos de expansión.

En el día a día de un desarrollador es habitual la utilización de tablas de Código-Descripción. Con ellas se ahorra espacio ya que no se duplica la información, se normalizan los datos, etc. Lo que todos ya sabemos. Los problemas con el manejo de estas entidades comienzan cuando en nuestro sistema existen entidades con muchos "códigos" de "tablas maestras", claves externas vamos.

¿Cómo obtenemos estos valores relativos a estas claves?. ¿Utilizamos sentencias INNER JOIN?. ¿Realmente y vamos a necesitar cargar todos estos valores en todos los escenarios?. Abordar estas preguntas, a mi modo de ver, es crucial a la hora de diseñar un sistema para que sea AGIL y TOLERANTE a los cambios. Esto último es importantísimo en proyectos grandes en los que, como todos sabemos, los cambios son constantes y no hay que romper lo que ya funciona.

Voy a intentar poner un ejemplo sencillo: supongamos que tenemos la típica entidad Empleado, que tiene, además de los campos habituales, un código de departamento, un código de categoría, un código de Responsable y una lista de Proyectos en los que participa. A la hora de cargar un objeto Empleado la implementación más habitual es la de lanzar una sentencia SQL con un INNER JOIN de las diferentes tablas. Es obvio, que si un caso de uso concreto dice que solo tengo que listar el Nombre y el Departamento, estaremos solicitando y transmitiendo más datos de los que realmente necesitamos.

Con este modelo "INNER JOIN" si posteriormente se añade otra clave externa, por ejemplo TipoDeHorario, habrá que modificar la sentencia SQL para que el método Carga() llene TODAS las propiedades. Este cambio en el modelo implicará una penalización en el rendimiento de otros métodos ya implementados que funcionaban correctamente.

Si cada cambio que se realice en el modelo afecta al rendimiento de lo ya existente nuestro sistema NO ES AGIL ni TOLERANTE a cambios. Con unos cuantos cambios como estos, a posteriori, y con este enfoque, el sistema dejará rápidamente de comportarse como venía haciéndolo y lo que es peor: de como el usuario ya está acostumbrado.

Por lo tanto y como ya he escrito en otra entrada:

“Siempre que se pueda, y aquí entran cuestiones de rendimiento, el primer enfoque que doy a la carga de entidades relacionada es la Lazy Load o Carga Perezosa con algún mecanismo de cacheo. De esta manera los objetos no pierden “su integridad” y siguen funcionando como se espera tras implementar diferentes modos de carga: en un escenario haciendo un INNER JOIN con la tabla Provincias, en otro escenario con la tabla Facturas donde no interesa en absoluto la información de las anteriores.”

No estoy en contra de la utilización del método INNER JOIN, al contrario, muchas veces es el que hay que utilizar. Lo que planteo es que, antes de nada, es necesario un análisis exhaustivo de donde se  donde se va a utilizar y de como afectará  esto a a posibles futuras modificaciones.

Otra decisión importante que tenemos que tomar a la hora de cargar un objeto, es "Cuando" se cargarán las entidades relacionadas y las descripciones de las propiedades de las que solo tenemos su clave externa. En nuestro ejemplo, simple el, solamente existe una:  La lista de Proyectos. En una aplicación del mundo real podría haber, además, una lista de direcciones, una lista de teléfonos, etc. ¿Cómo abordamos este problema? ¿Cargamos todas las entidades en el método carga()?. De este modo estaríamos en disposición de obtener cualquier valor inmediatamente después de su invocación, pero de nuevo en la implementación del caso de uso Listar Nombre y Departamento estaríamos haciendo más cosas de las estrictamente necesarias.

Nota importante: Existen escenarios en que este enfoque es el correcto ya que SIEMPRE que se carga una entidad se van a utilizar/consultar sus entidades relacionadas. De no ser así es mas aconsejable utilizar mecanismos de Carga Perezosa (Lazy Load). De esta forma las cosas se cargan cuando realmente se necesitan.

A continuación intentaré mostrar como realizar la carga de objetos para evitar, de algún modo, los problemas expuestos anteriormente.

Carga de un valor desde una tabla maestra.

En ocasiones es necesario mostrar la descripción de una propiedad en lugar del código o clave externa. Por ejemplo, supongamos que ahora necesitamos especificar un Tipo para la entidad Envio que ya conocemos de otras entradas del blog. Los tipos pueden ser: Local = 0, Nacional = 1 e Internacional = 2. Podríamos implementarlo de la siguiente forma:

Private _idTipo As Integer 
Public Property IdTipo() As Integer 
  Get 
    Return _idTipo 
  End Get 
  Set(ByVal value As Integer) 
    _idTipo = value 
  End Set 
End Property 

Private _TipoDeEnvio As String = "" 
Public ReadOnly Property TipoDeEnvio() As String 
  Get 
    Return _TipoDeEnvio 
  End Get 
End Property 

Con un enfoque tradicional del problema primero realizaríamos un INNER JOIN en la sentencia SQL, cambiaríamos el cargador del objeto y lo tendríamos. Lo MALO de este enfoque es que penalizamos el código ya existente que no necesita el tipo de envio. Por lo tanto es necesario hacernos las siguientes preguntas: ¿Es en todos los escenarios estrictamente necesaria la carga del Tipo de envio? ¿Lo será en futuros escenarios o casos de uso?. Lo más normal es dar un NO como respuesta estas dos preguntas.

Tendremos, por tanto, que mejorar el mecanismo de carga de este tipo de propiedades. Una primera aproximación puede ser la de cachear el valor. Lo vemos con el siguiente código:

Public ReadOnly Property TipoDeEnvio() As String 
  Get 
    If _TipoDeEnvio.Length = 0 Then 
      _TipoDeEnvio = New TipoDeEnvio().Carga(_idTipo).Descripcion 
    End If 
    Return _TipoDeEnvio 
  End Get 
End Property 

Si observamos con detenimiento esta nueva implementación nos daremos cuenta de que no interfiere con la anterior y de que en el caso de seguir cargando el objeto con INNER JOIN la creación y carga de un objeto TipoDeEnvio no será necesaria. Es muy importante, repito, que las modificaciones no rompan ni incidan en el rendimiento de lo que ya funciona.

Esta patrón puede ser interesante para la implementación de algunos casos de uso sencillos. Por ejemplo, si queremos abrir un solo envio y conocer su tipo. Necesitaremos, entonces, 2 accesos a la capa de Datos.

En el caso de que queramos, por el contrario, listar todos los envios de un mes cualquiera y mostrar su tipo, el patrón anterior no será el más adecuado, realizará una consulta a la base de datos por cada envio. No es del todo malo, ya que en una etapa inicial del desarrollo puede interesar más mostrar la futura funcionalidad, que el RENDIMIENTO.

Una versión mejorada puede hacer uso de mecanismos de cacheo globales o incrementales.

Public ReadOnly Property TipoDeEnvio() As String 
  Get 
    If _TipoDeEnvio.Length = 0 Then 
      If Helper.CacheDeTipos Is Nothing Then 
        _TipoDeEnvio = New TipoDeEnvio().Carga(_idTipo).Descripcion 
      Else 
        _TipoDeEnvio = Helper.CacheDeTipos.Item(_idTipo).Descripcion 
      End If 
    End If 
    Return _TipoDeEnvio 
  End Get 
End Property 

Se puede apreciar como esta última implementación “permitirá” el correcto funcionamiento del objeto independientemente del contexto en el que se utilice y de como se haya inicializado.

Carga de Entidades relacionadas

En ocasiones me he encontrado con código heredado en el que los objetos del tipo Cliente se cargaban en el constructor junto con una colección de TODAS las facturas del mismo, para finalmente mostrar por pantalla sencillamente la Localidad y la Provincia. Inadmisible.

Hemos de preguntarnos, como hicimos anteriormente, si es estrictamente necesario cargar todas, algunas o ninguna de las entidades relacionadas. Existirán casos en los que no haya más remedio que cargar todas las entidades y otros en los que habrá que dotar al objeto de mecanismos que le permitan ser usado de una forma Flexible.

Como norma general en el diseño de los objetos se establece que los métodos de carga recuperarán solamente los valores comunes a todos los casos de uso, es decir, los de la Tabla de la Base de Datos correspondiente a la entidad. Los demás valores o entidades relacionadas deberían ser cargadas bajo petición.

Un patrón de implementación de la propiedad Documentos es la siguiente:

Private _Documentos As Anexos = Nothing 
Public ReadOnly Property Documentos() As Anexos 
  Get 
    If _Documentos Is Nothing Then 
      _Documentos = New Anexos().Carga(Me.IdEnvio) 
    End If 
    Return _Documentos 
  End Get 
End Property

podremos utilizar el objeto de la siguiente forma:

. 
. 
With New Envio().Carga(350) 
  Debug.WriteLine(.this.IdEnvio) 
  For Each a As Anexo In .this.Documentos 
    Debug.WriteLine(a.FileName) 
  Next 
End With 
. 
.

De esta forma no existirá penalización en el rendimiento cuando solamente queramos cambiar, por ejemplo la fecha de un envio ya  que los Documentos SOLO son cargados cuando se solicitan.

Si requisitos posteriores indican, por ejemplo, la necesidad de incluir una lista de Destinatarios del envio, bastará con realizar la implementación de la propiedad Destinatarios de la misma forma que antes:

Private _Destinatarios As Destinatarios 
Public ReadOnly Property Destinatarios() As Destinatarios 
  Get 
    If _Destinatarios Is Nothing Then 
      _Destinatarios = New Destinatarios().Carga(Me.IdEnvio) 
    End If 
    Return _Destinatarios 
  End Get 
End Property

La ejecución de la nueva funcionalidad no interfiere en absoluto con la funcionalidad anterior consistente en listar los nombres de los documentos anexos.

With New Envio().Carga(350) 
  Debug.WriteLine(.this.IdEnvio)        
  For Each a As Destinatario In .this.Destinatario 
    Debug.WriteLine(a.Nombre) 
  Next 
End With 

Lo que perseguimos con patrones como estos es que los objetos se comporten como se espera que lo hagan y sobre todo, mantener está coherencia y efectividad aun cuando los requisitos cambien (Que cambiarán).

Al principio y cuando estamos acostumbrados a la carga INNER JOIN todo lo anterior nos parece que es complicar las cosas, porque, si me puedo traer toda la información de una sola vez, ¿Para qué montar tanto lio?. La respuesta es: Porque no sabemos cuales serán los requisitos en el en futuro, porque no queremos "Que el diseño inicial nos hipoteque el sistema", porque queremos desarrollos ágiles y porque queremos a toda costa evitar sentencias SLQ de más de 30 líneas que difícilmente entenderemos pasados cinco minutos.

Pensemos en que los grandes sistemas existentes, incluso los vivos, están compuestos por la agregación de pequeñas partes: bloques, ladrillos, células ... Lo complejo aquí es el sistema no sus componentes. Diseñemos, púes, componentes pequeños, sencillos, pensados para su expansión futura y, sobre todo, fácilmente comprensibles tanto en su funcionamiento individual como cuando interactúan con otros para formar el TODO.

El DataLoader y sus mecanismos de expansión.

Como hemos visto anteriormente los objetos de un sistema deben ser cargados de forma efectiva y coherente en sus todos y cada uno de los escenarios en donde son utilizados. Para ello debemos disponer de unos mecanismos de carga lo suficientemente flexibles para realizar con éxito esta tarea.

En el Modelo que propongo esta responsabilidad se le ha asignado a los propios objetos. No obstante se podría, por ejemplo, crear una clase  “BindingManager” que realizara la tarea de gestionar los “cargadores” de los objetos.

Los objeto de la capa de negocio, heredando y sobrescribiendo, implementan el siguiente interfaz:

Public Interface ILoader
  Function GetObjectLoader() As ObjectLoader
  Property ConnectionProvider() As Dal.ConnectionProviderDelegate
  Property transactionProvider() As Dal.TransactionProviderDelegate
End Interface

Como ya he dicho anteriormente los objetos inicialmente son cargados con sus valores mas “usados” y a la vez los mas “fácilmente” accesibles. Por lo tanto para este escenario el objeto proporciona a través de la interfaz ILoader su “cargador genérico”. Este cargador, normalmente, traspasa los valores de la base de datos (DataReader) a las variables privadas de las propiedades del objeto. El siguiente código muestra el cargador generico de la clase Envio:

Public Class Envio : Inherits ObjetoBase
  .
  .
  Private Shared Sub InitDataBinder()
    If _DataBinder Is Nothing Then
      _DataBinder = New Dal.ObjectLoader()
      With _DataBinder 
        .Add(New Dal.BindItem("id", "Id", 0, GetType(Integer)))
        .Add(New Dal.BindItem("idEnvio", "IdEnvio", 1, GetType(String)))
        .Add(New Dal.BindItem("observaciones", "Observaciones", 2, GetType(String)))
        .Add(New Dal.BindItem("numeroDeRegistro", "NumeroDeRegistro", 3, GetType(String)))
        .Add(New Dal.BindItem("fechaDeRegistro", "FechaDeRegistro", 4, GetType(Date)))
        .Add(New Dal.BindItem("fecha", "Fecha", 5, GetType(Date)))          
        .Add(New Dal.BindItem("usuario", "Usuario", 6, GetType(String)))          
      End with
    End If
  End Sub

  Shared _DataBinder As Dal.ObjectLoader = Nothing
  Public Overrides Function GetBinder() As Dal.ObjectLoader
    InitDataBinder()
    Return _DataBinder
  End Function
  .
  .
End Class
 

Este cargador está diseñado para trabajar con un DataReader con 6 campos que son los que actualmente devuelve el siguiente método de la capa de Datos:

Public Function GetItem(ByVal id As Integer) As IDataReader
  Using cmd As IDbCommand = CreateCommand()   
    cmd.CommandText = String.Format("Select * from [Envio] where Id={0}", id)
    Return DataServer.ExecuteReader(cmd)
  End Using
End Function

El responsable de la carga es el método LoadObject de la clase DataLoader que devuelve el objeto cargado con los valores de un DataReader:

Public Class Envio : Inherits ObjetoBase
  .
  .
  Public Function Carga(ByVal id As Integer) As Envio
    Using D__ As New Dal.Envios(ConnectionProvider, TransactionProvider)
      Dal.Loader.LoadObject(Me, D__.GetItem(id))
    End Using
    Return Me
  End Function
  .
  .

Hasta aquí hemos visto cual es la forma, genérica, en la que los objetos son cargados. Creo que es el momento de aclarar que en la actual implementación de los métodos de carga se generan dinámicamente funciones de carga de acuerdo a la información especificada en los ObjectLoader de los objetos. Esta generación dinámica de código intermedio se realiza una sola vez por tipo de cargador y almacenada como un delegado en el mismo ObjectLoader. El mecanismo de generación supone que existe una variable privada por cada bindItem cuyo nombre va precedido de “_”. Esta variable recibirá el valor del campo del DataReader cuyo índice se indica en el BindItem. Veamos una ejemplo de lo que digo:

en el cargador del objeto

.Add(New Dal.BindItem("id", "Id", 0, GetType(Integer)))

en la propiedad

.
.
Private _id As Integer
Public Property Id() As Integer
.
.

Nota: El segundo parámetro del constructor de BindItem actualmente no se usa. Es el nombre del campo en la tabla de la base de datos. Por rendimiento el acceso es por el indice de columna, el tercer parámetro.

A partir de ahora veremos como el Modelo nos permite especificar que valores cargar.

Supongamos que en un determinado caso de uso necesitamos conocer el Id, el Id del envio y el usuario que lo realizó. A la vista del código que ya tenemos utilizamos el método genérico de carga para implementar dicho caso de uso. Bueno, funciona, y en un primer estadío del desarrollo nos sirve para mostrar la funcionalidad del sistema al cliente pero sin entrar en cuestiones de rendimiento. El cliente, sin que sirva de precedente, nos indica que “Que si, que le gusta la funcionalidad” pero que estima que “el tiempo de respuesta debe ser bastante menor”. En este momento, o una vez que detectamos un punto en el que el rendimiento es importante, pasaríamos a la optimización del código de carga para este caso particular.

La primera optimización que podemos hacer es la creación en la capa de datos de un método que nos devuelva un DataReader únicamente con los columnas necesarias:

Public Function GetItemsExt() As IDataReader
  Using cmd As IDbCommand = CreateCommand()
    cmd.CommandText = "Select Id,IdEnvio,Usuario from [Envio]"
    Return DataServer.ExecuteReader(cmd)
  End Using
End Funtion

A continuación deberemos crear un método que pueda ser usado para implementar el exigente Caso de Uso. Este método debe especificar cuales son las propiedades que se van a cargar. Hasta ahora hemos visto que el cargador de objetos necesita un objeto y un DataReader con los valores de las propiedades para realizar su trabajo. Lo que no vemos tan fácilmente es que necesita también un cargador del objeto ya que este es solicitado al objeto si no se proporciona. De todas formas, y este es un típico mecanismo de expansión del diseño, existen versiones sobrecargadas de los métodos de carga: LoadObject y LoadObjects que aceptan un Cargador Personalizado:

Public Shared Sub LoadObject(Of T As {Class, New, ILoader})(ByVal Target As T, ByVal dr As IDataReader, ByVal Loader_ As ObjectLoader) Public Shared Sub LoadObject(Of T As {Class, New, ILoader})(ByVal Target As T, ByVal dr As IDataReader) Public Shared Function LoadObjects(Of T As {Class, New, ILoader})(ByVal Target As Generic.List(Of T), ByVal dr As IDataReader) As Generic.List(Of T) Public Shared Function LoadObjects(Of T As {Class, New, ILoader})(ByVal Target As Generic.List(Of T), ByVal dr As IDataReader, ByVal Loader_ As ObjectLoader) As Generic.List(Of T)

En el siguiente ejemplo el cargador se crearía en cada llamada, no obstante vemos claramente que este es un punto en el que se podrían aplicar mecanismos de cacheo de cargadores específicos. No lo hago aquí por simplicidad.

Public Function CargaExt() As Envios
  Using D__ As New Dal.Envios(ConnectionProvider, TransactionProvider)
    Dim customLoader As New Dal.ObjectLoader
    With customLoader
      .Add(New Dal.BindItem("id", "Id", 0, GetType(Integer)))
      .Add(New Dal.BindItem("idEnvio", "IdEnvio", 1, GetType(String)))
      .Add(New Dal.BindItem("usuario", "Usuario", 2, GetType(String)))
    End With
    Return Dal.Loader.LoadObjects(Of Envio)(Me, D__.GetItemsExt(), customLoader)
  End Using
End Function

Bueno pues esto es todo por hoy. Hemos visto la forma generica de cargar los objetos y como, en casos específicos, es necesaria una carga especifica  y optimizada de valores para obtener unos rendimientos altos.

Espero que todo esto sirva, al menos, para daros algunas ideas.

Saludos y hasta la próxima.

jueves, 18 de diciembre de 2008

Un ejemplo práctico II Bis.

Todo modelo que se precie debe implementar mecanismos que permitan tanto su expansión como su trazabilidad. Es muy conveniente poder saber, por ejemplo, por qué esta fallando nuestra aplicación al intentar realizar una operación en la base de datos en un entorno de producción. Para conseguir este objetivo es requisito indispensable que cualquier operación de acceso a base de datos se realice desde un punto común. De esta forma, por tanto, podremos trazar dichas operaciones e informar de los posibles errores que estas puedan producir.

En el modelo esto es responsabilidad de la clase Dal.DataServer. A continuación el fragmento de código que lo realiza:

.
.
Public Shared Function ExecuteReader(ByVal cmd As IDbCommand) As IDataReader
  Return ExecuteCommand(Of IDataReader)(cmd, AddressOf _ExecuteReader)
End Function
Public Shared Function ExecuteNonQuery(ByVal cmd As IDbCommand) As Integer
  Return ExecuteCommand(Of Integer)(cmd, AddressOf _ExecuteNonQuery)
End Function
Public Shared Function ExecuteScalar(Of T)(ByVal cmd As IDbCommand) As T
  Return ExecuteCommand(Of T)(cmd, AddressOf _ExecuteScalar)
End Function
' Para centralizar los mensajes de error de la Capa de Acceso a Datos
' de forma que se puedan: Escribir la sentencia SQL, escribir en un log, tratar el texto de error, etc
Private Shared Function ExecuteCommand(Of T)(ByVal cmd As IDbCommand, ByVal handler As CommandHandler(Of T)) As T
  Try
    If _Trazas.Enabled And DisableLog = False Then
      Trace.WriteLine(IIf(cmd.Transaction IsNot Nothing, "Dal --> Transaction.", "DAL.") & "ExecuteCommand : " & cmd.CommandText)
    End If
    DisableLog = False
    Return handler(cmd)
  Catch e As Exception
    ' Trace the exception into the same log.
    Trace.WriteLine("--- Exception : " & e.Message)
    ' Aqui se podría inntentar controlar los mensajes de error de clave duplicada, etc...
    Throw New Exception("Data Access Layer Exception ", e)
  End Try
End Function
Private Shared Function _ExecuteReader(ByVal Cmd As IDbCommand) As IDataReader
  Return Cmd.ExecuteReader()
End Function
Private Shared Function _ExecuteNonQuery(ByVal Cmd As IDbCommand) As Integer
  Dim Res As Integer = Cmd.ExecuteNonQuery()
  If _Trazas.Enabled Then
    Trace.WriteLine(IIf(Cmd.Transaction IsNot Nothing, "Dal --> Transaction ", "DAL ") & Res & " Filas")
  End If
  Return Res
End Function
Private Shared Function _ExecuteScalar(Of T)(ByVal Cmd As IDbCommand) As T
  Return Cmd.ExecuteScalar
End Function
.
.
.

Es una traducción-adaptación a vb, allá por febrero del 2006, de un articulo lo publicado en www.codeproject.com titulado: “Templating via Generics and delegates” de Oren Ellenbogen.

Configuración del Debug.

Image1Vamos a modificar el formulario del proyecto de pruebas añadiéndole algunos botones de comando, cambiaremos el control de texto por un RichTextBox y adecentamos un poco el formulario.

Además vamos a necesitar una clase que nos permita visualizar las trazas que nuestro modelo va dejando. Esta clase va a heredar de TraceListener y enviará todo mensaje que escribamos en el objeto Trace al control RichTextBox dándole un aspecto más vistoso. Una buena presentación siempre es de  agradecer.

Este es el código de nuestro, bueno mío, TraceLister:

Friend Class MyListener : Inherits TraceListener
  Private _theRichTextBox As RichTextBox
  Private _theSetValueDelegate As SetValueDelegate

  Public Sub New(ByVal control As RichTextBox)
    MyBase.new()
    _theRichTextBox = control
    _theRichTextBox.Multiline = True
    _theSetValueDelegate = New SetValueDelegate(AddressOf SetValueProc)
  End Sub

  Public Overloads Overrides Sub Write(ByVal message As String)
    _theRichTextBox.BeginInvoke(_theSetValueDelegate, New Object() {message})
  End Sub

  Public Overloads Overrides Sub WriteLine(ByVal message As String)
    _theRichTextBox.BeginInvoke(_theSetValueDelegate, New Object() {message & Environment.NewLine})
  End Sub

  Private Delegate Sub SetValueDelegate(ByVal value As String)
  Private Sub SetValueProc(ByVal value As String)
    _theRichTextBox.SelectionColor = Color.Blue
    If value.IndexOf("Objetos/Segundo") > -1 Then _theRichTextBox.SelectionColor = Color.Gray
    If System.Text.RegularExpressions.Regex.Match(value, _
                    "(Commit)|(Rollback)|(BeginTransaction)|(Transaction.)").Success Then _theRichTextBox.SelectionColor = Color.Green    
    _theRichTextBox.AppendText(New String(" "c, 2 * Trace.IndentLevel) & value)
    _theRichTextBox.ScrollToCaret()
  End Sub

End Class

El siguiente paso es inicializar el mecanismo de notificación de carga de objetos que nos ofrece el Dal y agregar nuestro listener a la lista para que reciba los mensajes de depuración

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
  .
  ' Resto del código omitido intencionadamente
  .       
  Dal.Loader.OnBeginLoad = New Dal.BeginLoadDelegate(AddressOf OnBeginLoad)
  Dal.Loader.OnEndLoad = New Dal.EndLoadDelegate(AddressOf OnEndLoad)
  Dal.DXTimer.Init()
    
  With Trace.Listeners
    .Add(New MyListener(Me.TextBox1))
  End With

End Sub

Private Shared Sub OnBeginLoad()
  DAL.DXTimer.GetElapsedMilliseconds()
End Sub

Private Shared Sub OnEndLoad(ByVal Properties As Integer, ByVal Objects As Integer)
  Dim time As Double = DAL.DXTimer.GetElapsedMilliseconds
  Trace.WriteLine( _
    String.Format("{0}s para {1} objetos con {2} campos.{3} Objetos/Segundo", _
                    New String() {(time / 1000.0).ToString("0.###0"), _
                                   Objects, _
                                   Properties, _
                                   (Objects / (time / 1000.0)).ToString("0.###0") _
                                   }))
End Sub
A partir de este momento tendremos completa información sobre las sentencias SQL ejecutadas así como del tiempo empleado en crear y cargar los objetos con los valores obtenidos.

Carga de objetos

Image2
Una vez que hemos realizado los cambios anteriores la pulsación del botón Cargar, como ya hacía antes, llenará lista con los ids de los envíos recuperados. Pero esta vez tenemos más cosas. Tenemos INFORMACIÓN y la información es poder.
Vemos como se van creando conexiones, como se van cerrando, que sentencias se ejecutan y cual es el tiempo que el sistema tarda en crear y cargar los objetos de la capa de Negocio.
Aquí, y seguro que alguno ya se ha dado cuenta, los datos nos muestran diferentes valores en lo referente al tiempo de carga de objetos: 0,2881 s y 0,004s. Estos valores corresponden a dos ejecuciones consecutivas y, aunque no reflejan fielmente la velocidad de carga ya que sólo son dos los registros de la tabal, si nos indican que la primera vez tarda más.
Esta tardanza es debida a la generación del delegado de carga de ese tipo de objeto. Esta generación solo se produce una vez en la “vida” del proceso que hospeda nuestros objetos. Las siguientes llamadas ejecutan código intermedio generado dinámicamente con la información de del DataBinder del objeto. Las pruebas están hechas con VS2008 en un Pentium II, no os digo más. Si es el que tengo en casa, ¡¡¡que pasa!!!!.
Entraré a fondo en este tema otro día. En www.codeproject.com  estaba la inspiración “A General Fast Method Invoker” por Luyan.

Las transacciones en el Modelo

Para probar el funcionamiento de las transacciones hemos añadido un botón con el título Insertar y establecido un manejador para el mimo. Este procedimiento simplemente insertará dos registro en la base de datos. En una primera ejecución vamos a olvidar un campo requerido por la tabla para observar que es lo que ocurre.
Este es el código del procedimiento:
 
Private Sub btnInsertar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnInsertar.Click    
    Dim TH As New Negocio.TransactionHelper(Dal.DataServer.GetNewConection())
    Try
      TH.BeginTransaction()
      ' Insertar el primer elemento (sintaxis 1)
      With New Negocio.Envio()
        .ConnectionProvider = TH.ConnectionProvider
        .TransactionProvider = TH.TransactionProvider
        .Id = 0 ' Segun nuestro diseño esto provocará una inserción.
        .IdEnvio = "Envio_02"
        .NumeroDeRegistro = "REG002"
        .FechaDeRegistro = "17/12/2008"
        .Observaciones = "Con método 2"
        .Usuario = "Test"
        .Save()
      End With
      ' Insertar el segundo elemento (Sintaxis 2)
      With Negocio.Factory.Crear(Of Negocio.Envio)(TH)
        .Id = 0
        .IdEnvio = "Envio_03"
        '.NumeroDeRegistro = "REG003"
        .FechaDeRegistro = "17/12/2008"
        .Observaciones = "Con la sintaxis 2 ahorramos lineas"
        .Usuario = "Test"
        .Save()
      End With
      TH.Commit()
      Trace.WriteLine("Inserción realizada")
    Catch ex As Exception
      TH.RollBack()
      Trace.WriteLine(ex.Message)
      MsgBox(ex.Message)
    Finally
      TH.Dispose()
    End Try
End Sub
 
En la imagen se puede ver cual ha sido el resultado de esta ejecución accidentada.

Image3

Gracias a nuestro mecanismo centralizado de consultas y trazas podemos fácilmente ver como el error que se ha producido en la segunda inserción es debido a la omisión de un valor requerido. Además observamos como: se abre y cierra correctamente la conexión; las operaciones se ejecutaban dentro de una transacción y que esta ha sido cancelada.

La siguiente imagen muestra una ejecución correcta una vez quitado el comentario del NumeroDeRegistro.
 
Image4

El código superior nos muestra las dos formas de especificar que conexión y que transacción usan los objetos. En la primera se establecen explícitamente, en la segunda se hace a través de un artilugio software (una clase vamos) que nos crea el objeto y establece estos valores obteniéndolos de TranscationHelper, pero, y esto es lo importante, con una sola línea de código. “Menos líneas menos errores”

Trabajar con conexiones

La forma en que el modelo hace uso de las conexiones es muy ¿simpe?: Si el objeto de la capa de datos tiene una conexión asignada la usa, si no y si tiene un ConnectionProvider lo invocará almacenando el resultado. En el caso de que las dos anteriores condiciones no se cumplan buscará primero una conexión compartida que usar. Si no la encuentra creará una nueva conexión y la almacenará por si el objeto la necesitara posteriormente. Es muy importante llamas al metodo Dispose de los objetos del DAL y que de no hacerlo las conexiones siguen abiertas. por lo tanto es “obligatorio” utilizar el patrón Using que implicitamente llamará a Dispose por nosotros:

Public Function Carga() As Negocio.Envios
  Using D__ As New Dal.Envios(ConnectionProvider, TransactionProvider)
    Return Dal.Loader.LoadObjects(Of Envio)(Me, D__.GetItems())
  End Using
End Function
 
En el formulario de pruebas hemos agregado un nuevo botón y un manejador para realizar, ahora, unas pruebas de modificación de objetos.
 
  Private Sub btnModificar_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnModificar.Click
    Dim TH As New Negocio.TransactionHelper(Dal.DataServer.GetNewConection())
    Try
      With Negocio.Factory.Crear(Of Negocio.Envios)(TH).Carga()
        For Each env In .this.Where(Function(envio) envio.Usuario = "Test")
          env.Usuario = "OldTest"
          env.Save()
        Next
      End With
    Catch ex As Exception
      Trace.WriteLine(ex.Message)
      MsgBox(ex.Message)
    Finally
      TH.Dispose()
    End Try
  End Sub
 
Este es su aspecto después de ejecutar el código superior
 
Image5

Aquí podemos ver como creamos un objeto TransactionHelper para pasárselo al “Creador de objetos” Factory e inmediatamente llamar al método Carga() del objeto devuelto. Con este método, y aprovechando la sentecia With, nos ahorramos la posible variable local, creamos y cargamos en la misma línea. Después vemos la utilidad de la propiedad This de “nuestros” objetos al permitirnos referenciar la colección dentro de un With y utilizando LINQ obtener solo los envios hechos por “Test”.

Cambiamos el valor de alguna propiedad y grabamos. ESTO SON OBJETOS. Creo que este tipo de flexibilidad es la que hay que ir buscando a la hora de diseñar los objeto ya que, con la actual capacidad de proceso de las maquinas con las que nos encontramos hoy en día, el recuperar 30 objetos con una simple sentencia “Select * from tabla” filtrarlos con LINQ y actualizar finalmente 4 es preferible a aplicaciones con decenas de “Optimizadas” y  “es un solo sitio usadas” operaciones de actualizacion y/o borrado. Por otro lado el utilizar estructuras simples y poco acopladas unas con otras reduce sustancialmente los tiempos de desarrollo. Posteriormente siempre es posible “Optimizar” el procedimiento que hemos detectado problemático.

En resumén si lo que quiero es realizar “una sola” operación de carga no es necesario especificar la conexión a utilizar bastaría lo siguiente:

With New Negocio.Envios().Carga()
  For Each env In .this.Where(Function(envio) envio.Usuario = "Test")
    Trace.WriteLine(env.Id)
  Next
End With
 
   Si lo que queremos es realizar varias operaciones, por ejemplo: cargar un grupo de clientes y sus facturas sería conveniente aprovechar la misma conexión con el fin de evitar una constante creación de conexiones. Utilizaríamos el siguiente patrón:
 
Dim TH As New Negocio.TransactionHelper(Dal.DataServer.GetNewConection())
With Negocio.Factory.Crear(Of Negocio.Envios)(TH).Carga()
  For Each env In .this.Where(Function(envio) envio.Usuario = "Test")
    With Negocio.Factory.Crear(Of Negocio.Anexo)(TH).Carga(env.Id)
        .
        .  
    End With
    With Negocio.Factory.Crear(Of Negocio.Historico)(TH).Carga(env.Id)
        .
        .
    End With
  Next
End with

 

Saludos a todos.

 

.

Un ejemplo práctico II.

En esta segunda entrega del ejemplo práctico pretendo mostrar:

  1. Las posibilidades de obtener las trazas de toda consulta realizada en la capa DAL.
  2. Las diferentes formas de inicialización, carga y utilización de los objetos.
  3. La implementación de transacciones en el modelo.
  4. Utilización de las conexiones.

Como es interesante conocer en todo momento los tiempos de carga de los objetos, el modelo dispone de un mecanismo de notificación de comienzo y fin de carga (OnBeginLoad y OnEndLoad). Pero esto lo veremos más adelante. Ahora solo agregar el código para un Timer mas preciso.

No recuerdo muy bien dónde encontré el código en C# pero creo que fue en www.kalme.de.

Este el código de Dal.Core.DXTimer.vb, archivo que hay que añadir al proyecto DAL aunque, ahora tras algún tiempo, estoy empezando a cuestionarme si no debiera estar en otra Capa. No obstante el cambio de localización de esta clase no compromete en nada el modelo.

Imports System
Imports System.Runtime.InteropServices

Namespace Dal
  Public Class DXTimer

#Region " imported functions "
    <System.Security.SuppressUnmanagedCodeSecurity()> _
    Private Declare Function QueryPerformanceFrequency Lib "kernel32" (ByRef PerformanceFrequency As Long) As Boolean
    <System.Security.SuppressUnmanagedCodeSecurity()> _
    Private Declare Function QueryPerformanceCounter _
    Lib "kernel32" (ByRef PerformanceCount As Long) As Boolean
#End Region

    ' The last number of ticks.
    Private Shared lLastTime As Long = 0
    ' The current number of ticks.
    Private Shared lCurrentTime As Long = 0
    ' The number of ticks per second for this system.
    ' This will be a constant value.
    Private Shared lTicksPerSecond As Long = 0
    ' Indicates if the Timer is initialized
    Private Shared bInitialized As Boolean = False
    ' The elapsed seconds since the last GetElapsedSeconds() call.
    Private Shared dElapsedSeconds As Double = 0.0
    ' The elapsed milliseconds since the last   GetElapsedMilliseconds() call.
    Private Shared dElapsedMilliseconds As Double = 0.0

    ' Property to query the ticks per second  for this system (Timer has to be initialized).
    Public Shared ReadOnly Property TicksPerSecond() As Long
      Get
        Return lTicksPerSecond
      End Get
    End Property

    ' The initialization of the timer. Tries to query
    ' performance frequency and determines if performance
    ' counters are supported by this system.
    Public Shared Sub Init()
      ' Try to read frequency. 
      ' If this fails, performance counters are not supported.
      If QueryPerformanceFrequency(lTicksPerSecond) = False Then
        Throw New Exception("Performance Counter not supported on this system!")
      End If
      ' Initialization successful
      bInitialized = True
    End Sub

    ' Starts the Timer. This set the initial time value.
    ' Timer has to be initialized for this.
    Public Shared Sub Start()
      ' Check if initialized
      If bInitialized = False Then
        Throw New Exception("Timer no initializado!")
      End If

      ' Initialize time value
      QueryPerformanceCounter(lLastTime)

    End Sub


    ' Gets the elapsed milliseconds since the last
    ' call to this function. Timer has to be initialized
    ' for this.
    ' Returns The number of milliseconds.
    Public Shared Function GetElapsedMilliseconds() As Double

      ' Check if initialized
      If bInitialized = False Then
        Throw New Exception("Timer no initializado!")
      End If

      ' Get current number of ticks
      QueryPerformanceCounter(lCurrentTime)

      ' Calculate number of milliseconds since last call
      dElapsedMilliseconds = (Convert.ToDouble(lCurrentTime - lLastTime) / Convert.ToDouble(lTicksPerSecond)) * 1000.0

      ' Store current number of ticks for next call
      lLastTime = lCurrentTime

      ' Return milliseconds
      Return dElapsedMilliseconds
    End Function


    ' Gets the elapsed seconds since the last call
    ' to this function. Timer has to be initialized for this.

    ' Returns The number of seconds.
    Public Shared Function GetElapsedSeconds() As Double
      ' Check if initialized
      If bInitialized = False Then
        Throw New Exception("Timer no initializedo!")
      End If
      ' Get current number of ticks
      QueryPerformanceCounter(lCurrentTime)

      ' Calculate elapsed seconds
      dElapsedSeconds = Convert.ToDouble(lCurrentTime - lLastTime) / Convert.ToDouble(lTicksPerSecond)

      ' Store current number of ticks for next call
      lLastTime = lCurrentTime

      ' Return number of seconds
      Return dElapsedSeconds
    End Function

    Public Shared Rnd As New Random(DateTime.Now.Millisecond)
  End Class
End Namespace