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:
- 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 ClienteDAL2:3: Dim _context As New DataContext4:5: Public Function GetClientePorIdCliente(ByVal id As String) As Clientes6:7: Return (From c In _context.Clientes()8: Where c.IdCliente = id).SingleOrDefault()9:10: End Function11:12: Public Function GetAll() As IEnumerable(Of Clientes)13:14: Return (From c In _context.Clientes()).AsEnumerable()15:16: End Function17:18: End Class19:
El de ClienteBLL
1: Public Class ClienteBLL2:3: Public Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.DAL.Clientes4:5: Dim clienteDAL As New InterfacesyCapas.DAL.ClienteDAL6: Return clienteDAL.GetClientePorIdCliente(id)7:8: End Function9:10: Public Function GetAll() As IEnumerable(Of InterfacesyCapas.DAL.Clientes)11:12: Dim clienteDAL As New InterfacesyCapas.DAL.ClienteDAL13: Return clienteDAL.GetAll14:15: End Function16:17: End Class18:
Y la aplicación de consola:
1: Module Module12:3: Sub Main()4:5: Dim clienteBLL As New InterfacesyCapas.BLL.ClienteBLL6: For Each t In clienteBLL.GetAll7: Console.WriteLine("cliente {0} de nombre {1}", t.IdCliente, t.Nombre)8: Next9: Console.ReadLine()10:11: End Sub12:13: End Module14:
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.
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:
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 ClienteBLL2:3: Dim _clienteDal As InterfacesyCapas.DAL.ClienteDAL4:5: Public Sub New(clienteDal As InterfacesyCapas.DAL.ClienteDAL)6: _clienteDal = clienteDal7: End Sub8:9: Public Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.DAL.Clientes10:11: Return _clienteDal.GetClientePorIdCliente(id)12:13: End Function14:15: Public Function GetAll() As IEnumerable(Of InterfacesyCapas.DAL.Clientes)16:17: Return _clienteDal.GetAll18:19: End Function20:21: End Class22:
Modificamos también nuestra pequeña aplicación para que tenga en cuenta estos cambios:
1: Module Module12:3: Sub Main()4:5: Dim clienteDAL As New InterfacesyCapas.DAL.ClienteDAL6: Dim clienteBLL As New InterfacesyCapas.BLL.ClienteBLL(clienteDAL)7: For Each t In clienteBLL.GetAll8: Console.WriteLine("cliente {0} de nombre {1}", t.IdCliente, t.Nombre)9: Next10: Console.ReadLine()11:12: End Sub13:14: End Module15:
Y si volvemos a ver las métricas de código, algo hemos conseguido!!
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 IClienteOperaciones2:3: Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.Entidades.Clientes4: Function GetAll() As IEnumerable(Of InterfacesyCapas.Entidades.Clientes)5:6: End Interface7:
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 ClienteBLL2:3: Dim _clienteQueCumpleElContrato As IClienteOperaciones4:5: Public Sub New(ByVal cliente As IClienteOperaciones)6: _clienteQueCumpleElContrato = cliente7: End Sub8:9: Public Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.Entidades.Clientes10:11: Return _clienteQueCumpleElContrato.GetClientePorIdCliente(id)12:13: End Function14:15: Public Function GetAll() As IEnumerable(Of InterfacesyCapas.Entidades.Clientes)16:17: Return _clienteQueCumpleElContrato.GetAll18:19: End Function20:21: End Class22:
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í:
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 ClienteDAL2: Implements InterfacesyCapas.BLL.IClienteOperaciones3:4: Dim _context As New DataContext5:6: Public Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.Entidades.Clientes Implements BLL.IClienteOperaciones.GetClientePorIdCliente7:8: Return (From c In _context.Clientes()9: Where c.IdCliente = id).SingleOrDefault()10:11: End Function12:13: Public Function GetAll() As IEnumerable(Of InterfacesyCapas.Entidades.Clientes) Implements BLL.IClienteOperaciones.GetAll14:15: Return (From c In _context.Clientes()).AsEnumerable()16:17: End Function18:19: End Class20:
Perfecto! Si ahora probamos nuestro proyecto, todo vuelve a funcionar, y si revisamos las métricas de código ¿qué obtendremos ahora?
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 ClienteDALFake2: Implements InterfacesyCapas.BLL.IClienteOperaciones3:4: Dim _context As New DataContext5:6: Public Function GetClientePorIdCliente(ByVal id As String) As InterfacesyCapas.Entidades.Clientes Implements BLL.IClienteOperaciones.GetClientePorIdCliente7:8: Dim cliente As New InterfacesyCapas.Entidades.Clientes()9: cliente.IdCliente = "99"10: cliente.Nombre = "Cliente FAKE encontrado"11: Return cliente12:13: End Function14:15: Public Function GetAll() As IEnumerable(Of InterfacesyCapas.Entidades.Clientes) Implements BLL.IClienteOperaciones.GetAll16:17: Dim k As New List(Of InterfacesyCapas.Entidades.Clientes)18:19: Dim clienteFake1 As New InterfacesyCapas.Entidades.Clientes20: clienteFake1.IdCliente = "F1"21: clienteFake1.Nombre = "Cliente Fake 1"22: k.Add(clienteFake1)23:24: Dim clienteFake2 As New InterfacesyCapas.Entidades.Clientes25: clienteFake2.IdCliente = "F2"26: clienteFake2.Nombre = "Cliente Fake 2"27: k.Add(clienteFake2)28:29: Dim clienteFake3 As New InterfacesyCapas.Entidades.Clientes30: clienteFake3.IdCliente = "F3"31: clienteFake3.Nombre = "Cliente Fake 3"32: k.Add(clienteFake3)33:34: Dim clienteFake4 As New InterfacesyCapas.Entidades.Clientes35: clienteFake4.IdCliente = "F4"36: clienteFake4.Nombre = "Cliente Fake 4"37: k.Add(clienteFake4)38:39: Return k.AsEnumerable40:41: End Function42:43: End Class44:
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 ClientesFactoria2:3: Public Function GetCliente(testing As Boolean) As BLL.IClienteOperaciones4:5: If Not testing Then6: Return New InterfacesyCapas.DAL.ClienteDAL7: Else8: Return New InterfacesyCapas.DAL.ClienteDALFake9: End If10:11: End Function12:13: End Class14:
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 = False4:5: Dim clientesFactoria As New ClientesFactoria6: Dim clienteBLL As New InterfacesyCapas.BLL.ClienteBLL(clientesFactoria.GetCliente(estamosEnTest))7: For Each t In clienteBLL.GetAll8: Console.WriteLine("cliente {0} de nombre {1}", t.IdCliente, t.Nombre)9: Next10:11: Console.ReadLine()12:13: End Sub14:
Y estos son los distintos resultados que obtenemos, cuando cambiamos nuestra variable estamosEnTest:
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!
Buena intro para aquellos que no se percataron que el uso de interfaces disminuye el desacoplamiento entre clases:D
ResponderEliminarMuy bueno, espero expongas otros mas avanzados....
Gracias...
Muy bueno!!! se agradece!
ResponderEliminarGracias! Muy buen artículo.
ResponderEliminar