miércoles, 24 de agosto de 2011

Uso de Interfaces en aplicaciones n-capas

 

Hola a todos.

Ayer tuve una discusión sana con mi compañero Sergio (podéis seguir su magnífico blog aquí), acerca de las arquitecturas n-capas, y más concretamente del uso de “interfaces“para la comunicación entre capas.

La discusión giraba en torno a lo que es una “Interfaz”, como concepto,  en el contexto de comunicaciones entre capas. La idea que primero nos viene a la cabeza, es que la interfaz es lo que la capa expone a modo de servicios (métodos y clases públicas) para comunicarse con otras capas.

Y si bien esa idea es completamente correcta y cierta, tenemos que tener en cuenta que las interfaces (entendidas aquí como elementos dentro de una programación orientada a objetos), también nos proporcionan un contrato a cumplir por las clases que las implementen. ejemplos los tenemos a miles (IComparable, IEnumerable, IQueryable, .. y unas cuantas más)

Pues bien, el propósito de este post es mostrar cómo las interfaces nos pueden ayudar a conseguir cierto nivel de desacoplamiento entre capas, con todos los beneficios que eso nos trae: testeabilidad, división de responsabilidades, y otros muchos.

Bueno, vamos al turrón.

Para comenzar, he preparado un pequeño proyecto con tres componentes:

image

  • DAL: ensamblado que alberga un pequeño modelo de Entity Framework, y una clase ClienteDAL con un par de métodos que recuperan datos del modelo.
  • BLL: capa de “negocio” que referencia a DAL y que tiene una clase ClienteBLL que hace uso de la capa DAL. El cómo utiliza BLL a DAL es el quid de todo este post.
  • Presentacion: en este caso, es una simple aplicación de consola que hace uso de la clase ClienteBLL para mostrar un listado de clientes en la consola. la capa de presentación referencia tanto a Dal como a BLL, por facilidad de uso y para acceder a las entidades del modelo de Entity Framework.

El código de la clase ClienteDAL es:

  1: Public Class ClienteDAL
  2: 
  3:     Dim _context As New DataContext
  4: 
  5:     Public Function GetClientePorIdCliente(ByVal id As String) As Clientes
  6: 
  7:         Return (From c In _context.Clientes()
  8:                 Where c.IdCliente = id).SingleOrDefault()
  9: 
 10:     End Function
 11: 
 12:     Public Function GetAll() As IEnumerable(Of Clientes)
 13: 
 14:         Return (From c In _context.Clientes()).AsEnumerable()
 15: 
 16:     End Function
 17: 
 18: End Class
 19: 


El de ClienteBLL

  1: Public Class ClienteBLL
  2: 
  3:     Public Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.DAL.Clientes
  4: 
  5:         Dim clienteDAL As New InterfacesyCapas.DAL.ClienteDAL
  6:         Return clienteDAL.GetClientePorIdCliente(id)
  7: 
  8:     End Function
  9: 
 10:     Public Function GetAll() As IEnumerable(Of InterfacesyCapas.DAL.Clientes)
 11: 
 12:         Dim clienteDAL As New InterfacesyCapas.DAL.ClienteDAL
 13:         Return clienteDAL.GetAll
 14: 
 15:     End Function
 16: 
 17: End Class
 18: 


Y la aplicación de consola:

  1: Module Module1
  2: 
  3:     Sub Main()
  4: 
  5:         Dim clienteBLL As New InterfacesyCapas.BLL.ClienteBLL
  6:         For Each t In clienteBLL.GetAll
  7:             Console.WriteLine("cliente {0} de nombre {1}", t.IdCliente, t.Nombre)
  8:         Next
  9:         Console.ReadLine()
 10: 
 11:     End Sub
 12: 
 13: End Module
 14: 


Vamos a ver las métricas de código para la clase ClienteBLL, a ver qué nos dice. Con el botón derecho del ratón, buscamos la opción “Calcular métricas de código” o desde el menú “Analizar” buscamos la misma opción.



image



Ojo, esta opción sólo esta disponible para Visual Studio Ultimate o Premium (gracias Mookie!), por lo que no podremos ver estos resultados si no disponemos de alguna de estas versiones. Sorry!



Vemos el resultado obtenido:



SNAGHTML1529e57



A nosotros nos interesa la medida “Acoplamiento de clases” (ver mas métricas). Vamos a ver si somos capaces de bajar ese valor. Hay que intentarlo! Lo primero que se nos puede ocurrir, es que en vez de instanciar en cada método a la clase ClienteDal, la clase ClienteBLL reciba un objeto ClienteDal en su contructor. dicho objeto ClienteDal deberá ser instanciado entonces desde la capa de presentación. Vamos a ver las modificaciones que deberíamos realizar y a ver qué obtenemos:



Nuestra clase ClienteBLL se modifica, con un nuevo constructor que permite establecer un objeto clienteDal, con el que funcionar a nivel interno.

  1: Public Class ClienteBLL
  2: 
  3:     Dim _clienteDal As InterfacesyCapas.DAL.ClienteDAL
  4: 
  5:     Public Sub New(clienteDal As InterfacesyCapas.DAL.ClienteDAL)
  6:         _clienteDal = clienteDal
  7:     End Sub
  8: 
  9:     Public Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.DAL.Clientes
 10: 
 11:         Return _clienteDal.GetClientePorIdCliente(id)
 12: 
 13:     End Function
 14: 
 15:     Public Function GetAll() As IEnumerable(Of InterfacesyCapas.DAL.Clientes)
 16: 
 17:         Return _clienteDal.GetAll
 18: 
 19:     End Function
 20: 
 21: End Class
 22: 


Modificamos también nuestra pequeña aplicación para que tenga en cuenta estos cambios:

  1: Module Module1
  2: 
  3:     Sub Main()
  4: 
  5:         Dim clienteDAL As New InterfacesyCapas.DAL.ClienteDAL
  6:         Dim clienteBLL As New InterfacesyCapas.BLL.ClienteBLL(clienteDAL)
  7:         For Each t In clienteBLL.GetAll
  8:             Console.WriteLine("cliente {0} de nombre {1}", t.IdCliente, t.Nombre)
  9:         Next
 10:         Console.ReadLine()
 11: 
 12:     End Sub
 13: 
 14: End Module
 15: 


Y si volvemos a ver las métricas de código, algo hemos conseguido!!



SNAGHTML15d410c



Pero nuestro afán optimizador, no conoce límites.. y queremos el “más difícil todavía!”. La idea es que especifiquemos mediante una interfaz en la capa BLL, las operaciones que debe cumplir el objeto que pasamos a nuestro ClienteBLL para que todo funcione. Vamos a ir viéndolo en partes (Jack the Ripper, rule 1).



1. Creamos nuestra interfaz, en BLL, especificando las operaciones que las clases que deseen hablar con nuestra BLL deben cumplir.

  1: Public Interface IClienteOperaciones
  2: 
  3:     Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.Entidades.Clientes
  4:     Function GetAll() As IEnumerable(Of InterfacesyCapas.Entidades.Clientes)
  5: 
  6: End Interface
  7: 


2. Cambiamos nuestra clase ClienteBLL, para que ahora en su constructor, reciba un objeto que implemente la interfaz que acabamos de definir:

  1: Public Class ClienteBLL
  2: 
  3:     Dim _clienteQueCumpleElContrato As IClienteOperaciones
  4: 
  5:     Public Sub New(ByVal cliente As IClienteOperaciones)
  6:         _clienteQueCumpleElContrato = cliente
  7:     End Sub
  8: 
  9:     Public Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.Entidades.Clientes
 10: 
 11:         Return _clienteQueCumpleElContrato.GetClientePorIdCliente(id)
 12: 
 13:     End Function
 14: 
 15:     Public Function GetAll() As IEnumerable(Of InterfacesyCapas.Entidades.Clientes)
 16: 
 17:         Return _clienteQueCumpleElContrato.GetAll
 18: 
 19:     End Function
 20: 
 21: End Class
 22: 


3. Quitamos la referencia a DAL que tenemos en BLL. Eso hará que el proyecto no nos compile, ya que BLL utiliza las entidades del modelo de Entity Framework que están en la capa DAL… ¿cual es la solución? pues sacar las entidades del modelo a otro ensamblado, que será compartido por DAL y BLL. El proceso es relativamente sencillo, y podéis verlo en este post, sobre como generar entidades Self Tracking.



Una vez hecho esto (y después de renombrar, colocar correctamente las referencias nuevas y demás cosillas), ahora tenemos un nuevo ensamblado Entidades, y nuestro proyecto pinta así:



image



4. Ahora es cuando viene lo bueno. Como hemos quitado la referencia a DAL desde BLL, lo que tenemos que hacer es que DAL referencie a BLL, y la clase ClienteDAL implemente la interfaz IClienteOperaciones.



Si recordamos, la interfaz IClienteOperaciones estipulada como el contrato que debe cumplir cualquier clase que desee interactuar con ClienteBLL. Aunque de manera muy básica (no conozco aún los entresijos de la técnica) digamos que es una forma primitiva de utilizar la inyección de dependencias. Veamos la implementación de la interfaz en ClienteDAL:

  1: Public Class ClienteDAL
  2:     Implements InterfacesyCapas.BLL.IClienteOperaciones
  3: 
  4:     Dim _context As New DataContext
  5: 
  6:     Public Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.Entidades.Clientes Implements BLL.IClienteOperaciones.GetClientePorIdCliente
  7: 
  8:         Return (From c In _context.Clientes()
  9:                 Where c.IdCliente = id).SingleOrDefault()
 10: 
 11:     End Function
 12: 
 13:     Public Function GetAll() As IEnumerable(Of InterfacesyCapas.Entidades.Clientes) Implements BLL.IClienteOperaciones.GetAll
 14: 
 15:         Return (From c In _context.Clientes()).AsEnumerable()
 16: 
 17:     End Function
 18: 
 19: End Class
 20: 


Perfecto! Si ahora probamos nuestro proyecto, todo vuelve a funcionar, y si revisamos las métricas de código ¿qué obtendremos ahora?



SNAGHTML190cd35



Pues parece ser que nuestro índice de acoplamiento de clases no ha bajado… pero sin embargo, el índice de mantenimiento ha subido notablemente. Pensemos en que hemos eliminado la referencia a DAL, pero que sin embargo hemos incluido una referencia a Entidades, con lo que nos hemos quedado casi igual… pero con un índice de mantenimiento mucho más interesante.



Entonces, que ¿beneficios podemos obtener con esta técnica? hemos hablado de que evidentemente al basar las interacciones entre capas en contratos, promovemos su aislamiento, así como el testeo de las capas. ¿Es esto cierto? vamos a ver un pequeño ejemplo (me estoy emocionando y extendiéndome más de la cuenta, creo yo… animo!).



Vamos a crear en Dal una clase ClienteDALFake que igualmente implemente la interfaz IClienteOperaciones, pero que no consulte la base de datos, sino que nos retorne objetos creados “ad hoc”. Vamos a ver si somos capaces:

  1: Public Class ClienteDALFake
  2:     Implements InterfacesyCapas.BLL.IClienteOperaciones
  3: 
  4:     Dim _context As New DataContext
  5: 
  6:     Public Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.Entidades.Clientes Implements BLL.IClienteOperaciones.GetClientePorIdCliente
  7: 
  8:         Dim cliente As New InterfacesyCapas.Entidades.Clientes()
  9:         cliente.IdCliente = "99"
 10:         cliente.Nombre = "Cliente FAKE encontrado"
 11:         Return cliente
 12: 
 13:     End Function
 14: 
 15:     Public Function GetAll() As IEnumerable(Of InterfacesyCapas.Entidades.Clientes) Implements BLL.IClienteOperaciones.GetAll
 16: 
 17:         Dim k As New List(Of InterfacesyCapas.Entidades.Clientes)
 18: 
 19:         Dim clienteFake1 As New InterfacesyCapas.Entidades.Clientes
 20:         clienteFake1.IdCliente = "F1"
 21:         clienteFake1.Nombre = "Cliente Fake 1"
 22:         k.Add(clienteFake1)
 23: 
 24:         Dim clienteFake2 As New InterfacesyCapas.Entidades.Clientes
 25:         clienteFake2.IdCliente = "F2"
 26:         clienteFake2.Nombre = "Cliente Fake 2"
 27:         k.Add(clienteFake2)
 28: 
 29:         Dim clienteFake3 As New InterfacesyCapas.Entidades.Clientes
 30:         clienteFake3.IdCliente = "F3"
 31:         clienteFake3.Nombre = "Cliente Fake 3"
 32:         k.Add(clienteFake3)
 33: 
 34:         Dim clienteFake4 As New InterfacesyCapas.Entidades.Clientes
 35:         clienteFake4.IdCliente = "F4"
 36:         clienteFake4.Nombre = "Cliente Fake 4"
 37:         k.Add(clienteFake4)
 38: 
 39:         Return k.AsEnumerable
 40: 
 41:     End Function
 42: 
 43: End Class
 44: 


Amiguitos programadores, parece que lo hemos conseguido.



Ahora, vamos a retocar nuestra aplicación cliente, implementando una clase ClientesFactoria, que nos retornara o el cliente bueno, o el cliente Fake, dependiendo de si estamos en Tests o no (por ejemplo):

  1: Public Class ClientesFactoria
  2: 
  3:     Public Function GetCliente(testing As Boolean) As BLL.IClienteOperaciones
  4: 
  5:         If Not testing Then
  6:             Return New InterfacesyCapas.DAL.ClienteDAL
  7:         Else
  8:             Return New InterfacesyCapas.DAL.ClienteDALFake
  9:         End If
 10: 
 11:     End Function
 12: 
 13: End Class
 14: 


Lo más interesante de esta clase, es que es una implementación del patrón Factoria (a mi manera y para este ejemplo, ojo), y que retorna objetos que implementan la interfaz IClienteOperaciones. Esto permite que si mañana tenemos cualquier otra implementación a incluir, podamos hacerlo sin demasiados problemas. Además, en el ejemplo que nos ocupa nos permite pasar del “Entorno real” al “Entorno de test” de una manera simplísima! Veamos la aplicación cliente:

  1:     Sub Main()
  2: 
  3:         Dim estamosEnTest As Boolean = False
  4: 
  5:         Dim clientesFactoria As New ClientesFactoria
  6:         Dim clienteBLL As New InterfacesyCapas.BLL.ClienteBLL(clientesFactoria.GetCliente(estamosEnTest))
  7:         For Each t In clienteBLL.GetAll
  8:             Console.WriteLine("cliente {0} de nombre {1}", t.IdCliente, t.Nombre)
  9:         Next
 10: 
 11:         Console.ReadLine()
 12: 
 13:     End Sub
 14: 


Y estos son los distintos resultados que obtenemos, cuando cambiamos nuestra variable estamosEnTest:



SNAGHTML1aacbee



Objetivo conseguido!!!



Como reflexión final, comentaros que este ejemplo es sólo una manera de presentar conceptos que a mi modo de ver no son precisamente sencillos (Inyección de dependencias, Inversión de Control, desacoplamiento, pruebas, DDD) pero que sin ninguna duda nos ayudarán a realizar software de mayor calidad. Como desarrolladores, creo que tenemos la obligación de estar al día en las tecnologías que van apareciendo (aunque “aparecer” signifique que ahora se ponen “de moda” en el mundo Microsoft, por ejemplo MVC) y evaluar seriamente las posibilidades que los nuevos lenguajes y Frameworks nos ofrecen.



Si habéis llegado hasta aquí, enhorabuena! os he soltado un rollo muy serio. Espero que al menos, vuestra perspectiva en el uso de Interfaces y otras técnicas haya comenzado a bullir en vuestra cabeza. En la mía lleva una temporada dando vueltas hasta hoy, que ha salido.



Podéis descargaros el código aqui



Un saludo y gracias!

3 comentarios:

  1. Buena intro para aquellos que no se percataron que el uso de interfaces disminuye el desacoplamiento entre clases:D
    Muy bueno, espero expongas otros mas avanzados....
    Gracias...

    ResponderEliminar
  2. Muy bueno!!! se agradece!

    ResponderEliminar
  3. Gracias! Muy buen artículo.

    ResponderEliminar