Un amigo está desarrollando una aplicación en C# para aprender el lenguaje.
Producto de una conversación llegamos al tema de la programación en capas, un
tema bastante trillado en internet y al cual me voy a sumar con este artículo.
La programación en capas no es más que separar las "responsabilidades" del
software en distintos "responsables". Esta separación facilita la mantención del
software y permite, por ejemplo, cambiar fácilmente a los responsables. Para que
se entienda daré un ejemplo de la vida moderna.
Cuando una persona se somete a una operación, pasa por las manos de a lo menos
tres personas: un anestesiólogo, el cirujano y un instrumentista. El primero se
encarga de aplicar la anestesia, el segundo de realizar la operación y el
tercero de facilitar al cirujano los instrumentos que requiere para dicha
operación. Si la operación es de cerebro o de corazón la especialidad del
cirujano debería ser distinta y por tanto el cirujano será otro.
La programación en capas consiste precisamente en esto, identificar y separar
las responsabilidades en miembros especializados de modo que si se requiere
cambiar a un responsable, sea fácil hacerlo. La estructura más común de este
concepto es la programación en tres capas. En ésta los responsables son el
encargado de los datos, el encargado del negocio y el encargado de la
interacción con el usuario.
Desde luego las responsabilidades varían dependiendo del sistema y por
consecuencia los responsables también, sin embargo, las responsabilidades antes
mencionadas son las más comunes en los sistemas de administración.
Espero que hasta este punto se entienda el concepto, y si es así, es probable
que algunos estén pensado algo como "¡Ok, suena bonito!, pero ¿cómo llevo esto a
una aplicación?", así que vamos al ejemplo.
El ejemplo consistirá en algo tan simple como un mantenedor de datos de personas
(nombre, teléfono y correo electrónico) y nuestros responsables se encargarán de
lo siguiente:
- Datos : Leerá y grabara los datos en un archivo de texto.
-
- Negocio:
Entregará los datos al encargado de la
interacción con el usuario y se encarga de las validaciones que serán
dos:
-
-
El
nombre no puede estar en blanco.
-
El
correo electrónico, si es ingresado, debe ser válido.
-
- Interface (interacción con el usuario):
Contendrá
-
-
-
Una
grilla para mostrar y editar los datos.
-
Un
botón Cargar
-
Un
botón Guardar
Una vez definida las responsabilidades, es necesario tener en cuenta que hay un
elemento que los tres responsables deben conocer. Me refiero a la "persona" o
mejor dicho a sus datos, está claro que el encargado de datos debe saber que
grabar y que leer, el encargado de negocios debe saber sobre que aplicar las
reglas de negocio y el encargado de la interacción con el usuario debe saber que
datos mostrar o pedir.
El siguiente diagrama ilustra lo anterior:

Para quienes no sepan interpretar el diagrama, se lee
así:
-
Presentación utiliza Negocio
-
Uno o más Negocios utilizan solo un Dato
-
Presentación, Negocio y Dato requieren de Persona.
Al momento de construir la aplicación, es una buena práctica separar las
responsabilidades en proyectos distintos de manera que quede claramente
indicada la responsabilidad de cada proyecto, sin embargo, me ha tocado ver
aplicaciones en donde separan las responsabilidades en carpetas, el único
problema importante que veo en separar en carpetas es que podrían generarse
conflictos de responsabilidad y desorden, por ejemplo, un programador
inexperto podría utilizar el método grabar de la clase Dato desde la
Presentación, lo que implicaría que se estaría saltando las reglas de
negocios, en nuestro caso, no se validaría el nombre ni el correo
electrónico.
Construí la aplicación de ejemplo en C# con Visual Studio 2012 Express, el
vínculo de descarga lo dejo al final del artículo.
De todos modos aquí está el código:
Presentación:
namespace
EjemploTresCapas
{
partial
class Presentacion
{
/// <summary>
/// Variable del diseñador
requerida.
/// </summary>
private
System.ComponentModel.IContainer components =
null;
/// <summary>
/// Limpiar los recursos que se
estén utilizando.
/// </summary>
/// <param name="disposing">true
si los recursos administrados se deben eliminar;
/// false
en caso contrario.</param>
protected
override
void Dispose(bool
disposing)
{
if (disposing && (components
!= null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Código
generado por el Diseñador de Windows Forms
/// <summary>
/// Método necesario para
admitir el Diseñador. No se puede modificar
/// el contenido del método con
el editor de código.
/// </summary>
private
void
InitializeComponent()
{
this.components =
new
System.ComponentModel.Container();
this.dgPersona =
new
System.Windows.Forms.DataGridView();
this.nombreDataGridViewTextBoxColumn
= new
System.Windows.Forms.DataGridViewTextBoxColumn();
this.telefonoDataGridViewTextBoxColumn
= new
System.Windows.Forms.DataGridViewTextBoxColumn();
this.emailDataGridViewTextBoxColumn
= new
System.Windows.Forms.DataGridViewTextBoxColumn();
this.personaBindingSource
= new
System.Windows.Forms.BindingSource(this.components);
this.btnCargar =
new
System.Windows.Forms.Button();
this.btnGrabar =
new
System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.dgPersona)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.personaBindingSource)).BeginInit();
this.SuspendLayout();
//
// dgPersona
//
this.dgPersona.AllowUserToOrderColumns
= true;
this.dgPersona.Anchor =
((System.Windows.Forms.AnchorStyles)(((
(System.Windows.Forms.AnchorStyles.Top |
System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
|
System.Windows.Forms.AnchorStyles.Right)));
this.dgPersona.AutoGenerateColumns
= false;
this.dgPersona.ColumnHeadersHeightSizeMode
=
System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgPersona.Columns.AddRange(new
System.Windows.Forms.DataGridViewColumn[] {
this.nombreDataGridViewTextBoxColumn,
this.telefonoDataGridViewTextBoxColumn,
this.emailDataGridViewTextBoxColumn});
this.dgPersona.DataSource
= this.personaBindingSource;
this.dgPersona.Location
= new
System.Drawing.Point(0, 0);
this.dgPersona.Name =
"dgPersona";
this.dgPersona.Size =
new System.Drawing.Size(737,
368);
this.dgPersona.TabIndex
= 0;
//
//
nombreDataGridViewTextBoxColumn
//
this.nombreDataGridViewTextBoxColumn.DataPropertyName
= "Nombre";
this.nombreDataGridViewTextBoxColumn.HeaderText
= "Nombre";
this.nombreDataGridViewTextBoxColumn.Name
= "nombreDataGridViewTextBoxColumn";
//
//
telefonoDataGridViewTextBoxColumn
//
this.telefonoDataGridViewTextBoxColumn.DataPropertyName
= "Telefono";
this.telefonoDataGridViewTextBoxColumn.HeaderText
= "Telefono";
this.telefonoDataGridViewTextBoxColumn.Name
= "telefonoDataGridViewTextBoxColumn";
//
//
emailDataGridViewTextBoxColumn
//
this.emailDataGridViewTextBoxColumn.DataPropertyName
= "Email";
this.emailDataGridViewTextBoxColumn.HeaderText
= "Email";
this.emailDataGridViewTextBoxColumn.Name
= "emailDataGridViewTextBoxColumn";
//
// personaBindingSource
//
this.personaBindingSource.DataSource
= typeof(EjemploTresCapas.Entidad.Persona);
//
// btnCargar
//
this.btnCargar.Anchor =
((System.Windows.Forms.AnchorStyles)(
(System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Right)));
this.btnCargar.Location
= new
System.Drawing.Point(569, 374);
this.btnCargar.Name =
"btnCargar";
this.btnCargar.Size =
new System.Drawing.Size(75,
23);
this.btnCargar.TabIndex
= 1;
this.btnCargar.Text =
"Cargar";
this.btnCargar.UseVisualStyleBackColor
= true;
this.btnCargar.Click +=
new
System.EventHandler(this.btnCargar_Click);
//
// btnGrabar
//
this.btnGrabar.Anchor =
((System.Windows.Forms.AnchorStyles)(
(System.Windows.Forms.AnchorStyles.Bottom |
System.Windows.Forms.AnchorStyles.Right)));
this.btnGrabar.Location
= new
System.Drawing.Point(650, 374);
this.btnGrabar.Name =
"btnGrabar";
this.btnGrabar.Size =
new System.Drawing.Size(75,
23);
this.btnGrabar.TabIndex
= 2;
this.btnGrabar.Text =
"Grabar";
this.btnGrabar.UseVisualStyleBackColor
= true;
this.btnGrabar.Click +=
new
System.EventHandler(this.btnGrabar_Click);
//
// Presentacion
//
this.AutoScaleDimensions
= new
System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode =
System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize =
new System.Drawing.Size(737,
404);
this.Controls.Add(this.btnGrabar);
this.Controls.Add(this.btnCargar);
this.Controls.Add(this.dgPersona);
this.Name =
"Presentacion";
this.Text =
"Form1";
((System.ComponentModel.ISupportInitialize)(this.dgPersona)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.personaBindingSource)).EndInit();
this.ResumeLayout(false);
}
#endregion
private
System.Windows.Forms.DataGridView dgPersona;
private
System.Windows.Forms.Button btnCargar;
private
System.Windows.Forms.Button btnGrabar;
private
System.Windows.Forms.DataGridViewTextBoxColumn nombreDataGridViewTextBoxColumn;
private
System.Windows.Forms.DataGridViewTextBoxColumn telefonoDataGridViewTextBoxColumn;
private
System.Windows.Forms.DataGridViewTextBoxColumn emailDataGridViewTextBoxColumn;
private
System.Windows.Forms.BindingSource personaBindingSource;
}
}
using
EjemploTresCapas.Entidad;
using System;
using
System.Collections.Generic;
using
System.Configuration;
using
System.Windows.Forms;
namespace
EjemploTresCapas
{
public
partial
class Presentacion :
Form
{
private Negocio.Negocio
_negocio;
public Presentacion()
{
InitializeComponent();
_negocio =
new
Negocio.Negocio(ConfigurationManager.AppSettings["ArchivoDatos"]);
personaBindingSource.DataSource =
new List<Persona>();
}
private
void btnCargar_Click(object
sender, EventArgs e)
{
try
{
personaBindingSource.DataSource = _negocio.Leer();
}
catch (Exception error)
{
MessageBox.Show(error.Message);
}
}
private
void btnGrabar_Click(object
sender, EventArgs e)
{
try
{
if (personaBindingSource.DataSource
!= null &&
personaBindingSource.DataSource
is List<Persona>)
_negocio.Grabar(personaBindingSource.DataSource
as List<Persona>);
}
catch (Exception error)
{
MessageBox.Show(error.Message);
}
}
}
}
Negocio:
using
EjemploTresCapas.Dato;
using
EjemploTresCapas.Entidad;
using System;
using
System.Collections.Generic;
using
System.Globalization;
using
System.Text.RegularExpressions;
namespace
EjemploTresCapas.Negocio
{
public
class Negocio
{
private
bool _emailInvalido;
private Datos _dao;
public Negocio(string
nombreArchivo)
{
this._dao =
Datos.Instancia(nombreArchivo);
}
public List<Persona>
Leer()
{
return
this._dao.Leer();
}
public
void
Grabar(List<Persona> datos)
{
foreach (Persona item
in datos)
{
if (string.IsNullOrWhiteSpace(item.Nombre))
throw
new Exception("El
nombre de la persona no puede ser vacio.");
if (!string.IsNullOrEmpty(item.Email)
&& !IsValidEmail(item.Email))
throw
new Exception(string.Format("El
correo electrónico {0} no es válido", item.Email));
}
this._dao.Grabar(datos);
}
public
bool IsValidEmail(string
strIn)
{
_emailInvalido
= false;
if (String.IsNullOrEmpty(strIn))
return
false;
// Use IdnMapping class to
convert Unicode domain names.
try
{
strIn = Regex.Replace(strIn,
@"(@)(.+)$", this.DomainMapper,
RegexOptions.None, TimeSpan.FromMilliseconds(200));
}
catch (RegexMatchTimeoutException)
{
return
false;
}
if (_emailInvalido)
return
false;
// Return true if strIn is in
valid e-mail format.
try
{
return
Regex.IsMatch(strIn,
@"^(?("")(""[^""]+?""@)|(([0-9a-z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-z])@))"
+
@"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-z][-\w]*[0-9a-z]*\.)+[a-z0-9]{2,17}))$",
RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250));
}
catch (RegexMatchTimeoutException)
{
return
false;
}
}
private
string
DomainMapper(Match match)
{
// IdnMapping class with
default property values.
IdnMapping
idn = new IdnMapping();
string domainName =
match.Groups[2].Value;
try
{
domainName = idn.GetAscii(domainName);
}
catch (ArgumentException)
{
_emailInvalido = true;
}
return match.Groups[1].Value
+ domainName;
}
}
}
Dato:
using
EjemploTresCapas.Entidad;
using
System.Collections.Generic;
using System.IO;
namespace
EjemploTresCapas.Dato
{
public
class Datos
{
private
static Datos datos;
public
string NombreArchivo {
get;
private
set; }
public
static Datos Instancia(string
nombreArchivo)
{
if (datos ==
null)
datos = new
Datos(nombreArchivo);
return datos;
}
private Datos()
{
}
private Datos(string
nombreArchivo)
{
this.NombreArchivo =
nombreArchivo;
}
public List<Persona>
Leer()
{
List<Persona>
datos = new
List<Persona>();
if (File.Exists(NombreArchivo))
{
using (StreamReader sr
= new
StreamReader(NombreArchivo))
{
while (!sr.EndOfStream)
{
string[] arrDatos =
sr.ReadLine().Split(';');
datos.Add(new
Persona(arrDatos[0], arrDatos[1], arrDatos[2]));
}
sr.Close();
}
}
else
throw
new
FileNotFoundException("No se
encontró el archivo " + NombreArchivo);
return datos;
}
public
void
Grabar(List<Persona> datos)
{
using (StreamWriter sr
= new
StreamWriter(NombreArchivo,
false))
{
foreach (Persona item
in datos)
sr.WriteLine(item.ToString());
sr.Flush();
sr.Close();
}
}
}
}
Entidad:
namespace
EjemploTresCapas.Entidad
{
public
class Persona
{
public
string Nombre {
get;
set; }
public
string Telefono {
get;
set; }
public
string Email {
get;
set; }
public Persona() :
this (string.Empty,string.Empty,string.Empty)
{
}
public Persona(string
nombre, string telefono,
string email)
{
this.Nombre = nombre;
this.Telefono =
telefono;
this.Email = email;
}
public
override
string ToString()
{
return
string.Format("{0};{1};{2}",
Nombre, Telefono, Email);
}
}
}
Les dejo como ejercicio, cambiar la capa de datos por una en la cual los
datos sean guardados en una base de datos tradicional u orientada a objetos.
Para terminar, me queda señalar que si bien en el
ejemplo pusimos la validación del nombre obligatorio como parte de nuestro
negocio, esta responsabilidad podría trasladarse a la presentación, esto se debe
a que es común en los lenguajes modernos tener controles inteligentes que, por
ejemplo, obligan a ingresar datos, formatean cuadros de texto, realizan
validaciones de forma, etc. La decisión de donde poner cada responsabilidad
dependerá de la aplicación que se esté desarrollando.
|