Programación en Capas

 

Ejemplo en C# Ejemplo en PHP Visual Studio Ej. en ASP y C# Volver

Programación en C#

 

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. 

 

 

Volver

 

 

*