Aquí les comparto este articulo que les comparti a los amigos de MSCoder.
Introducción
Hoy en día todos los desarrolladores comprendimos que es necesaria una capa intermedia entre nuestra aplicación y el proveedor de los datos, conocida como "Capa de Acceso a los Datos", pero muy pocos han comprendido las implicaciones de continuar desarrollando aplicaciones que contengan "súper botones" que en la capa de presentación tiene toda la lógica del negocio. El problema de este enfoque es que estaremos desarrollando aplicaciones para el día, día sin una planificación que nos permita construir una infraestructura de software que nos permita disponer de servicios que puedan ser reutilizados y extendidos por las aplicaciones existentes o nuevas. Otro problema de no desarrollar aplicaciones con arquitectura de n-capas es que se abre una brecha o GAP de seguridad cada vez que necesitamos reescribir el código para otra plataforma sin garantizar que la "misma" aplicación se comporte igual sin importar la plataforma o dispositivo en que se desarrolla la nueva capa de presentación de nuestra aplicación.
Este articulo ha sido escrito asumiendo que ya se ha tenido alguna experiencia desarrollando aplicaciones con C# , ya que no explica el uso del lenguaje sino la aplicación de la arquitectura n-capas en aplicaciones desarrollados en la plataforma .NET Framework 2.0.
Definición
El tener una arquitectura n-capas bien definida reducirá los hoyos de vulnerabilidad que evidentemente se dan cuando no desarrollamos con esta arquitectura. Dentro de esta arquitectura la capa intermedia o la capa lógica de negocios ocupa un lugar angular en la construcción de una infraestructura de software que nos permitirá el crecimiento y la extensibilidad de servicios para todas las aplicaciones existentes y futuras.
La definición de los linderos de cada capa nos permitirá definir correctamente la colaboración que proveerá cada una de ellas y descubriremos que la capa intermedia es inevitablemente la lógica de negocios, esto hará una infraestructura robusta y lista para la extensibilidad y el crecimiento como proveedora de servicios. Veámoslo más a detalle:
La capa de acceso a los datos su propósito primario es separar al proveedor de datos del resto de la aplicación. Hoy en día gracias a la tecnología que nos provee el .NET Framework esta capa puede ser reutilizada para diversos proveedores sin hacer cambios significativos o prácticamente sin ningún cambio. Pero esta capa no debe de guardar ningún grado de complejidad mas que el ejecutar las operaciones fundamentales del proveedor de datos operaciones conocidas por sus siglas en inglés CRUD (Creación, Lectura, Actualización y Eliminación). Las Métodos de esta capa debe de realizar operaciones mono transaccionales, o atómicas. Ya que la capa superior, la intermedia se encargará de manejar las transacciones gracias a la disponibilidad del System.Transactions del .NET Framework 2.0 de la clase TransactionScope. He aquí la colaboración entre las capas, la capa de acceso a datos se convierte en una capa un sencilla gracias a que la capa intermedia se encargará de las transacciones y esto gracias a la tecnología disponible que tenemos en el .NET Framework 2.0.
A continuación mostraré un segmento de código que muestre lo expuesto.
public static class ConceptoTransaccionBLL
{
private const decimal FactorCalculoImpuesto = 0.10141728;
public const string CrearExplicacion = "Proceso bla, bla bla";
public static int? Crear(ConceptoTransaccion parametro)
{
int? Resultado = null;
if (ValidarCrear(parametro)
{
using (TransactionScope ts = new TransactionScope())
{
Resultado = ConceptoTransaccionDAL.Crear(parametro);
parametro.Nombre = "Prueba de Transaccion";
parametro.ValorImpuesto = parametro.Valor * FactorCalculoImpuesto;
OtraTransaccionDAL.Actualizar(parametro);
ts.Complete();
}
}
return Resultado;
}
}
Este segmento de código típico de la capa intermedia o de negocios nos muestra lo siguiente:
El flujo del proceso del negocio:
Como el ejemplo anterior que define que luego de crear El Concepto de la Transacción, Actualizará Otra Transacción.
Resultado = ConceptoTransaccionDAL.Crear(parametro);
…
OtraTransaccionDAL.Actualizar(parametro);
Las reglas del negocio:
En nuestro segmento de código en medio del proceso realizamos el calculo de un impuesto X.
private const decimal FactorCalculoImpuesto = 0.10141728;
….
parametro.ValorImpuesto = parametro.Valor * FactorCalculoImpuesto;
Para facilitar el ejemplo se declaró como una constante este factor pero bien pudo estar almacenada esta información en el proveedor de datos u en otro medio.
Validaciones de los datos:
Esta capa debe de garantizar que los datos requeridos para procesarla fueron debidamente validados en el ejemplo anterior encapsulamos estas validaciones en el método validar, y solo si es exitosa la validación puede iniciar el flujo del proceso del negocio.
if (ValidarCrear(parametro){{…}}
Textos de explicación de los procesos del negocio:
Estos textos aunque pertenecen a esta capa pueden ser útiles en la capa de presentación para informar al usuario de que se trata el proceso que ha seleccionado ejecutar.
public const string CrearExplicacion = "Proceso bla, bla bla";
Manejo de transacciones:
En esta capa utilizamos la librería de .NET Framework 2.0, System.Transactions para manejar las transacciones que se adaptan muy bien si en la capa de acceso a los datos estoy utilizando Las librerías de Práticas y Patrones " Microsoft Enterprise Library 2.0 de enero del 2006" que utiliza las mejores prácticas de acceso a los datos de la tecnología ADO.NET 2.0. La clase TransactionScope hace todo el trabajo por nosotros. Además podemos definir Opciones para variar el comportamiento del alcance de las transacciones, lo cual por simplificación no expondremos aquí pero lo invito a que investigue en la Red.
using (TransactionScope ts = new TransactionScope())
{
…
ts.Complete();
}
Ts.Complete() si llega ejecutarse realiza el commit o persistencia "física" en el proveedor de los datos, si se interrumpe el código antes de ejecutar esta sentencia, entonces hará un rollback(revertira lo actualizado en el proveedor de datos) de las transacciones que hasta ese momento se ejecutaron. Esta fuera del alcance de este documento enseñar a los lectores el manejo de transacciones, pero si es mi objetivo dejar una inquietud de conocer a detalle el manejo sencillo y eficiente de las transacciones en el .NET Framework 2.0.
Facilita su lectura y manejo en la Capa de Presentación
Por lo tanto ya que hemos encapsulado la lógica en la capa intermedia, en la capa de presentación solo tendremos que llenar la clase que representa la entidad de negocio e invocar el método de la capa de negocios para procesar la información como se muestra en el siguiente segmento de código:
private void Grabar()
{
try
{
Factura parametro = this.Factura;
parametro.UsuarioSistema = this.User.Identity.Name;
TransaccionBLL.Crear(parametro);
MessageBox.Show("Operacion realiza con Exito!");
}
catch (Exception ex)
{
MessageBox.ShowError(ex.Message);
}
}
En este segmento invocamos un Método en la capa de presentación Grabar que a su vez, invoca de la capa de negocio TransaccionBLL y su método Crear que recibirá como único parámetro la entidad Factura. Y eso es todo.
La entidad del negocio
Es muy importante definir la estratégica de comunicación entre las capas. En el ejemplo expuesto hemos utilizado Entidades de Negocio, representadas por Clientes, Facturas, Empleados, Nomina Quincenal, Movimientos Bancarios, etc.
Estas entidades carecen de métodos solo tiene propiedades, campos y posiblemente atributos, que nos pueden servir para almacenar la información como el mapeo de las propiedades a las columnas de la base de datos. Luciría de la siguiente manera:
[ ConnectionStringName("NORTHWIND"), ReadMethodName("OrderRead")]
public class Orden
{
private int? numeroOrden;
[ColumnName("OrderID"), ParameterName( "@OrderID",IsPrimaryKey=true,IsForCreate=false)]
public int? NumeroOrden
{
get { return numeroOrden; }
set { numeroOrden = value; }
}
private DateTime fecha;
[ColumnName("OrderDate"), ParameterName( "@OrderDate")]
public DateTime Fecha
{
get { return fecha; }
set { fecha = value; }
}
}
Donde ConnectionStringName, ReadMethodName, ColumnName, ParameterNamel, son atributos personalizados que el programador puede definir y que el .NET Framework utiliza ampliamente desde su versión 1.0. Se recuerda del [Seriealizable] este es un atributo que indica al CLR que la clase puede persistir en algún medio utilizado como lo es una variable de sessión, o una variable ViewState en ASP.NET.
Ahora veamos un ejemplo paso a paso de transformación de un súper botón a una implementación de arquitectura de n-capas.
A continuación les mostrare un ejemplo de un segmento de código que representa el evento de un botón que tiene como propósito insertar una orden de un cliente en la base de datos "Northwind" (incluida en SQL Server 2000). El código lo iremos transformando hasta alcanzar la infraestructura de negocios aplicando los conceptos de arquitectura de n-capas en .NET Framework 2.0.
private void btnInsertar_Click(object sender, EventArgs e)
{ // Validaciones de los datos capturados
if (parametroFechaOrden == null parametroFechaOrden == DateTime.MinValue)
{
MessageBox.Show("Debe de ingresar el numero de la Orden" );
return;
}
if (paremetroCodigoCliente == null paremetroCodigoCliente.Length == 0)
{
MessageBox.Show("Seleccione el cliente");
return;
}
// Segmento de Codigo usual en el manejo de la Capa de Presentacion
this.dataGridView1.Enabled = false; this.btnConsultar.Enabled = false;
// Manejo del acceso a los datos
Database db = DatabaseFactory .CreateDatabase("NORTHWIND");
string sqlCommand = "OrderCreate";
DbCommand orderCommand = db.GetStoredProcCommand(sqlCommand);
db.AddInParameter(orderCommand, "@CustomerID", DbType.String, paremetroCodigoCliente);
db.AddInParameter(orderCommand, "@EmployeeID", DbType.Int32, parametroCodigoEmpleado);
db.AddInParameter(orderCommand, "@OrderDate", DbType.Date, parametroFechaOrden);
sqlCommand = "OrderDetailCreate";
DbCommand orderDetailCommand = db.GetStoredProcCommand(sqlCommand);
using (DbConnection connection = db.CreateConnection())
{
connection.Open();
//manejo de transacciones
DbTransaction transaction = connection.BeginTransaction();
try
{
object valor = db.ExecuteScalar(orderCommand, transaction);
int numeroOrden = 0;
if (!int.TryParse(valor.ToString(), out numeroOrden)) throw new Exception("No pudo obtener el numero de Orden");
foreach (OrdenLinea linea in ordenDetalle)
{
if (orderDetailCommand.Parameters.Count > 0)
orderDetailCommand.Parameters.Clear();
db.AddInParameter(orderDetailCommand, "@OrderID", DbType.Int32, numeroOrden);
db.AddInParameter(orderDetailCommand, "@ProductID", DbType.String, linea.CodigoProducto );
db.AddInParameter(orderDetailCommand, "@UnitPrice", DbType.Decimal, linea.PrecioUnitario );
db.AddInParameter(orderDetailCommand, "@Quantity", DbType.Int32, linea.Cantidad );
db.AddInParameter(orderDetailCommand, "@Discount", DbType.Decimal, linea.Descuento);
db.ExecuteNonQuery(orderDetailCommand, transaction);
}
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
MessageBox.Show(ex.Message);
this.dataGridView1.Enabled = true; this.btnConsultar.Enabled = true;
return;
}
finally
{
connection.Close();
}
this.dataGridView1.Enabled = true; this.btnConsultar.Enabled = true;
MessageBox.Show( "Ordern Guardada!");
}
}
El código anterior muestra un código típico cuando "no" se utiliza la arquitectura de n-capas, en el desarrollo de aplicaciones empresariales. Es muy difícil interpretar este código porque incluye el manejo de la capa de presentación las validaciones de los datos, el manejo de errores. Adicional a ello si necesitamos realizar esto en otra opción en nuestra aplicación estaremos obligados a repetir el código, y entonces la brecha (GAP) se expande en ves de reducirse en el objetivo de minimizar las entradas maliciosas a nuestra aplicación. Este es otro aspecto el de Seguridad es el que nos obliga a tener una infraestructura de software bien definida para reducir los puntos de vulnerabilidad de nuestra aplicación.
Ahora veamos como quedaría nuestra aplicación si refactorizamos (la disciplina que promueve la mejora del código interno sin afectar el comportamiento externo de una aplicación. Uno de sus expositores mas famosos es Martin Fowler y su libro "Refactoring") este código e implementamos la arquitectura de n-capas:
private void btnInsertar_Click(object sender, EventArgs e)
{
// Segmento de Codigo usual en el manejo de la Capa de Presentacion
this.dataGridView1.Enabled = false; this.btnConsultar.Enabled = false;
Orden orden = new Orden();
orden.CodigoCliente = paremetroCodigoCliente;
orden.Fecha = parametroFechaOrden;
try
{
bool OperacionExitosa = OrdenBLL.Crear(orden, ordenDetalle);
if (OperacionExitosa)
MessageBox.Show("Ordern Guardada!");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
this.dataGridView1.Enabled = true; this .btnConsultar.Enabled = true;
}
Podemos notar que ahora es mucho mas fácil entender el código para hemos encapsulado en la capa intermedia la de negocios todo el código referente a la lógica del negocio. Deducimos por simple observación que hay una clase "OrdenBLL" que tiene un Método Crear; que Crea la orden (el encabezado y detalle de la orden), alrededor esta el código propio de la capa de presentación y el manejo del error, que para ejemplo solo lo capturamos para evitar una ruptura en el funcionamiento de la aplicación y mostramos el error.
Ahora vayamos mas adentro del código y veamos que encierra el método Crear de la clase "OrdenBLL":
public class OrdenBLL
{
public static bool Crear(Orden orden, OrdenLinea[] ordenDetalle){..}
public static bool Validar(Orden orden){…}
}
Esta clase de la capa intermedia tiene dos métodos uno que crea la orden y otro que valida los valores de la entidad de negocio Orden. Veamos ahora el método crear:
public static bool Crear(Orden orden, OrdenLinea[] ordenDetalle)
{
bool OperacionExistosa = false;
bool CreoDetalle = false;
if (Validar(orden))
{
using (TransactionScope ts = new TransactionScope ())
{
int? numeroOrden = OrdenDAL.Crear(orden);
if (numeroOrden == null numeroOrden == 0)
throw new Exception("No pudo obtener el numero de Orden" );
foreach (OrdenLinea linea in ordenDetalle)
{
linea.NumeroOrden = numeroOrden;
CreoDetalle = OrdenDAL.CrearDetalle(linea);
if (CreoDetalle) break ;
}
if (CreoDetalle)
ts.Complete();
}
}
return OperacionExistosa;
}
Este método se encarga de invocar el método validar que es publico por si se desea validar los datos sin efectuar ninguna operación. Luego de validar los datos esta capa se encarga de manejar las transacciones ya que invocamos dos diferentes métodos de la capa de acceso a datos Y cada uno de ellos maneja su propia conexión. Pero el código es fácilmente entendible indicándonos que hay una clase "OrdenDAL" que tiene un método para crear la orden (el encabezado) y otro el detalle si sucede algo sabemos que esta manejando transacciones y revertirá todo el proceso.
Ahora veamos los métodos de la capa de acceso a datos. Iniciemos con el método que Crea el encabezado de la orden:
public class OrdenDAL
{
public static int? Crear(Orden orden)
{
int? NuevoNumeroOrden = null;
Database db = DatabaseFactory .CreateDatabase("NORTHWIND");
string sqlCommand = "OrderCreate";
DbCommand orderCommand = db.GetStoredProcCommand(sqlCommand);
db.AddInParameter(orderCommand, "@CustomerID", DbType.String, orden.CodigoCliente);
db.AddInParameter(orderCommand, "@EmployeeID", DbType.Int32, orden.CodigoEmpleado);
db.AddInParameter(orderCommand, "@OrderDate", DbType.Date, orden.Fecha);
DbCommand orderDetailCommand = db.GetStoredProcCommand(sqlCommand);
using (DbConnection connection = db.CreateConnection())
{
connection.Open();
//manejo de transacciones
try
{
object valor = db.ExecuteScalar(orderCommand);
int numeroOrden = 0;
int.TryParse(valor.ToString(), out numeroOrden);
}
finally
{
connection.Close();
}
}
return NuevoNumeroOrden;
}
Esto método solo se encarga de pasar los valores de la entidad y ejecutar el procedimiento almacenado. A continuación el método que crea el detalle de la orden:
public static bool CrearDetalle(OrdenLinea detalle)
{
bool operacionExitosa = false;
Database db = DatabaseFactory.CreateDatabase("NORTHWIND" );
string sqlCommand = "OrderDetailCreate";
DbCommand orderDetailCommand = db.GetStoredProcCommand(sqlCommand);
db.AddInParameter(orderDetailCommand, "@OrderID", DbType.Int32, detalle.NumeroOrden);
db.AddInParameter(orderDetailCommand, "@ProductID", DbType.String, detalle.CodigoProducto );
db.AddInParameter(orderDetailCommand, "@UnitPrice", DbType.Decimal, detalle.PrecioUnitario );
db.AddInParameter(orderDetailCommand, "@Quantity", DbType.Int32, detalle.Cantidad);
db.AddInParameter(orderDetailCommand, "@Discount", DbType.Decimal, detalle.Descuento);
int afectados = db.ExecuteNonQuery(orderDetailCommand);
operacionExitosa = afectados > 0;
return operacionExitosa;
}
}
Este método la única diferencia del anterior es que crea el detalle de la orden pero hace lo mismo pasa los valores de la entidad y ejecuta el procedimiento almacenado. Esta capa gracias a la abstracción de la capa intermedia se convierte en una capa que realiza operaciones atómicas, mono-transaccionales, esto facilita su comprensión y reduce el riesgo de un error en el código porque solo hace una casa pasar los datos de la entidad al proveedor de datos. Si hay una falla del lado del proveedor de datos sabemos que es en esta capa o en el mismo proveedor de datos y no disperso por toda la aplicación o la capa de presentación.
El Futuro
Por ultimo quisiera mencionar que hay una fuerte tendencia y mala concepción que la lógica de negocio es más fácil y práctica dejarla en el proveedor de los datos, pero no es así. Si analizamos un poco mas encontraremos que si tenemos un lenguaje orientado a objetos como lo es C#, con disposición a definir Tipos Genéricos, la capacidad de utilizar la reflexión en las entidades, la definición de atributos, podremos reutilizar el código, cambiar su comportamiento, heredar los objetos y abstraer y encapsular el código de una forma mucho mas fácil y eficiente que desde un proveedor de datos relacional. En especial si vemos que en el futuro podremos invocar y manejar las colecciones como lo haríamos desde un proveedor de datos de una forma mas natural o través de funciones extensibles como lo serán el FROM, WHERE y SELECT dentro del mismo lenguaje de programación porque el .NET Framework 3.0 se encargará de generar el código al proveedor de datos o no ha oído o leído sobre LINQ. Aquí le dejo un segmento de código para que se emocione e investigue más al respecto:
from c in customers
join o in orders on c.CustomerID equals o.CustomerID into co
let n = co.Count()
where n >= 10
select new { c.Name, OrderCount = n }
Código extraído de las especificaciones del Nuevo C# 3.0 Mayo 2006.
Resumen
En resumen si tenemos definida la capa intermedia y su colaboración con la capa de acceso a los datos hacia abajo y hacia arriba la capa de presentación. Si fuera necesario que lo es hoy en día proveer a los usuarios de nuestra aplicación de acceso a otra plataforma o dispositivo de Hardware, solo nos preocuparíamos por crear una nueva capa de presentación y a lo sumo actualizar o modificar la capa de acceso a datos para acceder un proveedor mas liviano para implementar nuestra aplicación en un dispositivo móvil. Pero ya no nos preocuparíamos del negocio en sí, sino de la parte tecnológica que es la parte donde somos más competentes. Esto al largo plazo nos dará mayor seguridad y preparación para enfrentar los retos presentes y futuros, porque hemos desarrollado una infraestructura de software con servicios bien definidos gracias a la implementación de la arquitectura de n-capas.
Hasta una próxima vez!
Manolo Herrera
Líder de la Comunidad de Desarrolladores .NET en Guatemala