Compartir a través de


Iteración n.º 4: Hacer que la aplicación tenga un acoplamiento flexible (C#)

por Microsoft

Descargar código

En esta cuarta iteración, aprovechamos varios patrones de diseño de software para facilitar el mantenimiento y modificación de la aplicación Contact Manager. Por ejemplo, refactorizamos la aplicación para usar el patrón Repositorio y el patrón de inserción de dependencias.

Creación de una aplicación de administración de contactos ASP.NET MVC (C#)

En esta serie de tutoriales, creamos una aplicación completa de administración de contactos de principio a fin. La aplicación Contact Manager le permite almacenar información de contacto (nombres, números de teléfono y direcciones de correo electrónico) para obtener una lista de personas.

Compilamos la aplicación en varias iteraciones. Con cada iteración, mejoramos gradualmente la aplicación. El objetivo de este enfoque de iteración múltiple es permitirle comprender el motivo de cada cambio.

  • Iteración n.º 1: Crear la aplicación. En la primera iteración, creamos el Administrador de contactos de la manera más sencilla posible. Agregamos compatibilidad con operaciones básicas de base de datos: Crear, Leer, Actualizar y Eliminar (CRUD).

  • Iteración n.º 2: Hacer que la aplicación tenga un buen aspecto. En esta iteración, mejoramos la apariencia de la aplicación modificando la página maestra de vista predeterminada de ASP.NET MVC y la hoja de estilos en cascada.

  • Iteración n.º 3: Añadir una validación de formulario. En la tercera iteración, añadimos validación básica de formularios. Evitamos que los usuarios envíen un formulario sin completar los campos de formulario obligatorios. También validamos las direcciones de correo electrónico y los números de teléfono.

  • Iteración n.º 4: Hacer que la aplicación tenga un acoplamiento flexible. En esta cuarta iteración, aprovechamos varios patrones de diseño de software para facilitar el mantenimiento y modificación de la aplicación Contact Manager. Por ejemplo, refactorizamos la aplicación para usar el patrón Repositorio y el patrón de inserción de dependencias.

  • Iteración n.º 5: Crear pruebas unitarias. En la quinta iteración, hacemos que la aplicación sea más fácil de mantener y modificar agregando pruebas unitarias. Simulamos nuestras clases de modelo de datos y compilamos pruebas unitarias para nuestros controladores y lógica de validación.

  • Iteración n.º 6: Utilizar el desarrollo mediante pruebas. En esta sexta iteración, añadimos una nueva funcionalidad a nuestra aplicación escribiendo primero pruebas unitarias y escribiendo código en las pruebas unitarias. En esta iteración, agregamos grupos de contactos.

  • Iteración n.º 7: Agregar funcionalidad de Ajax. En la séptima iteración, mejoramos la capacidad de respuesta y el rendimiento de nuestra aplicación agregando compatibilidad con Ajax.

Esta iteración

En esta cuarta iteración de la aplicación Contact Manager, refactorizamos la aplicación para que la aplicación esté acoplada de forma más flexible. Cuando una aplicación está acoplada de forma flexible, puede modificar el código en una parte de la aplicación sin necesidad de modificar el código en otras partes de la aplicación. Las aplicaciones acopladas de forma flexible son más resistentes al cambio.

Actualmente, toda la lógica de acceso a los datos y de validación utilizada por la aplicación Contact Manager está contenida en las clases de controlador. Es una mala idea. Siempre que tenga que modificar una parte de su aplicación, corre el riesgo de introducir errores en otra. Por ejemplo, si modifica su lógica de validación, corre el riesgo de introducir nuevos errores en la lógica de acceso a los datos o en la de controladores.

Nota:

(SRP), una clase nunca debería tener más de una razón para cambiar. La combinación de lógica de controlador, validación y base de datos es una enorme infracción del Principio de responsabilidad única.

Hay varias razones por las que puede necesitar modificar su aplicación. Puede que necesite agregar una nueva característica a su aplicación, que necesite corregir un error en su aplicación o que necesite modificar cómo se implementa una característica de su aplicación. Las aplicaciones rara vez son estáticas. Tienden a crecer y mutar con el tiempo.

Imagine, por ejemplo, que decide cambiar la forma de implementar su capa de acceso a los datos. En este momento, la aplicación Contact Manager usa Microsoft Entity Framework para acceder a la base de datos. Sin embargo, es posible que decida migrar a una tecnología de acceso a datos nueva o alternativa, como ADO.NET Data Services o NHibernate. Sin embargo, como el código de acceso a los datos no está aislado del código de validación y de controlador, no hay forma de modificar el código de acceso a los datos en su aplicación sin modificar otro código que no esté directamente relacionado con el acceso a los datos.

Cuando una aplicación está acoplada de forma flexible, por otro lado, puede realizar cambios en una parte de una aplicación sin tocar otras partes de una aplicación. Por ejemplo, puede cambiar las tecnologías de acceso a datos sin modificar la lógica de validación o controlador.

En esta iteración, aprovechamos varios patrones de diseño de software que nos permiten refactorizar nuestra aplicación Contact Manager y convertirla en una aplicación acoplada de forma más flexible. Cuando hayamos terminado, el Administrador de contactos no hará nada que no hiciera antes. Sin embargo, en el futuro podremos cambiar la aplicación más fácilmente.

Nota:

La refactorización es el proceso de reescribir una aplicación de forma que no pierda ninguna funcionalidad existente.

Usar el patrón de diseño de software de repositorio

Nuestro primer cambio consiste en aprovechar un patrón de diseño de software denominado patrón de repositorio. Usaremos el patrón de repositorio para aislar nuestro código de acceso a los datos del resto de nuestra aplicación.

Implementar el patrón de repositorio requiere que finalicemos los dos pasos siguientes:

  1. Creación de una interfaz
  2. Crear una clase concreta que implemente la interfaz

En primer lugar, necesitamos crear una interfaz que describa todos los métodos de acceso a datos que necesitamos realizar. La interfaz IContactManagerRepository está contenida en la lista 1. Esta interfaz describe cinco métodos: CreateContact(), DeleteContact(), EditContact(), GetContact y ListContacts().

Lista 1: Models\IContactManagerRepository.cs

using System;
using System.Collections.Generic;

namespace ContactManager.Models
{
    public interface IContactRepository
    {
        Contact CreateContact(Contact contactToCreate);
        void DeleteContact(Contact contactToDelete);
        Contact EditContact(Contact contactToUpdate);
        Contact GetContact(int id);
        IEnumerable<Contact> ListContacts();

    }
}

A continuación, tenemos que crear una clase concreta que implemente la interfaz IContactManagerRepository. Como estamos usando Microsoft Entity Framework para acceder a la base de datos, crearemos una nueva clase llamada EntityContactManagerRepository. Esta clase está contenida en la lista 2.

Lista 2: Models\EntityContactManagerRepository.cs

using System.Collections.Generic;
using System.Linq;

namespace ContactManager.Models
{
    public class EntityContactManagerRepository : ContactManager.Models.IContactManagerRepository
    {
        private ContactManagerDBEntities _entities = new ContactManagerDBEntities();

        public Contact GetContact(int id)
        {
            return (from c in _entities.ContactSet
                    where c.Id == id
                    select c).FirstOrDefault();
        }

        public IEnumerable ListContacts()
        {
            return _entities.ContactSet.ToList();
        }

        public Contact CreateContact(Contact contactToCreate)
        {
            _entities.AddToContactSet(contactToCreate);
            _entities.SaveChanges();
            return contactToCreate;
        }

        public Contact EditContact(Contact contactToEdit)
        {
            var originalContact = GetContact(contactToEdit.Id);
            _entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit);
            _entities.SaveChanges();
            return contactToEdit;
        }

        public void DeleteContact(Contact contactToDelete)
        {
            var originalContact = GetContact(contactToDelete.Id);
            _entities.DeleteObject(originalContact);
            _entities.SaveChanges();
        }

    }
}

Observe que la clase EntityContactManagerRepository implementa la interfaz IContactManagerRepository. La clase implementa los cinco métodos descritos por esa interfaz.

Puede que se pregunte por qué necesitamos molestarnos con una interfaz. ¿Por qué necesitamos crear tanto una interfaz como una clase que la implemente?

Con una excepción, el resto de nuestra aplicación interactuará con la interfaz y no con la clase concreta. En lugar de llamar a los métodos expuestos por la clase EntityContactManagerRepository, llamaremos a los métodos expuestos por la interfaz IContactManagerRepository.

De este modo, podemos implementar la interfaz con una nueva clase sin necesidad de modificar el resto de nuestra aplicación. Por ejemplo, es posible que en un futuro queramos implementar una clase DataServicesContactManagerRepository que implemente la interfaz IContactManagerRepository. La clase DataServicesContactManagerRepository podría usar ADO.NET Data Services para acceder a una base de datos en lugar de Microsoft Entity Framework.

Si nuestro código de aplicación está programado en la interfaz IContactManagerRepository en lugar de la clase EntityContactManagerRepository concreta, podemos cambiar clases concretas sin modificar ninguno del resto del código. Por ejemplo, podemos cambiar de la clase EntityContactManagerRepository a la clase DataServicesContactManagerRepository sin modificar nuestra lógica de acceso a los datos o de validación.

La programación con interfaces (abstracciones) en lugar de clases concretas hace que nuestra aplicación sea más resistente al cambio.

Nota:

Puede crear rápidamente una interfaz a partir de una clase concreta en Visual Studio seleccionando la opción de menú Refactorizar, Extraer interfaz. Por ejemplo, puede crear primero la clase EntityContactManagerRepository y después usar Extraer interfaz para generar automáticamente la interfaz IContactManagerRepository.

Uso del patrón de diseño de software de Inserción de dependencias

Ahora que hemos migrado nuestro código de acceso a los datos a una clase Repository independiente, tenemos que modificar nuestro controlador Contact para que use esta clase. Aprovecharemos un patrón de diseño de software llamado Inserción de dependencias para usar la clase Repository en nuestro controlador.

El controlador Contact modificado está contenido en la lista 3.

Lista 3: Controllers\ContactController.cs

using System.Text.RegularExpressions;
using System.Web.Mvc;
using ContactManager.Models;

namespace ContactManager.Controllers
{
    public class ContactController : Controller
    {
        private IContactManagerRepository _repository;

        public ContactController()
            : this(new EntityContactManagerRepository())
        {}

        public ContactController(IContactManagerRepository repository)
        {
            _repository = repository;
        }

        protected void ValidateContact(Contact contactToValidate)
        {
            if (contactToValidate.FirstName.Trim().Length == 0)
                ModelState.AddModelError("FirstName", "First name is required.");
            if (contactToValidate.LastName.Trim().Length == 0)
                ModelState.AddModelError("LastName", "Last name is required.");
            if (contactToValidate.Phone.Length > 0 && !Regex.IsMatch(contactToValidate.Phone, @"((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}"))
                ModelState.AddModelError("Phone", "Invalid phone number.");
            if (contactToValidate.Email.Length > 0 && !Regex.IsMatch(contactToValidate.Email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"))
                ModelState.AddModelError("Email", "Invalid email address.");
        }

        public ActionResult Index()
        {
            return View(_repository.ListContacts());
        }

        public ActionResult Create()
        {
            return View();
        } 

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Contact contactToCreate)
        {
            // Validation logic
            ValidateContact(contactToCreate);
            if (!ModelState.IsValid)
                return View();

            // Database logic
            try
            {
                _repository.CreateContact(contactToCreate);
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

        public ActionResult Edit(int id)
        {
            return View(_repository.GetContact(id));
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Edit(Contact contactToEdit)
        {
            // Validation logic
            ValidateContact(contactToEdit);
            if (!ModelState.IsValid)
                return View();

            // Database logic
            try
            {
                _repository.EditContact(contactToEdit);
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

        public ActionResult Delete(int id)
        {
            return View(_repository.GetContact(id));
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Delete(Contact contactToDelete)
        {
            try
            {
                _repository.DeleteContact(contactToDelete);
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

    }
}

Observe que el controlador Contact de la lista 3 tiene dos constructores. El primer constructor pasa una instancia concreta de la interfaz IContactManagerRepository al segundo constructor. La clase del controlador Contact usa Inserción de dependencias del constructor.

El único lugar donde se usa la clase EntityContactManagerRepository es en el primer constructor. El resto de la clase usa la interfaz IContactManagerRepository en lugar de la clase concreta EntityContactManagerRepository.

Esto facilita el cambio de implementaciones de la clase IContactManagerRepository en el futuro. Si quiere usar la clase DataServicesContactRepository en lugar de la clase EntityContactManagerRepository, solo tiene que modificar el primer constructor.

La Inserción de dependencias del constructor también hace que la clase del controlador Contact sea muy fácil de probar. En sus pruebas unitarias, puede instanciar el controlador Contact pasando una implementación ficticia de la clase IContactManagerRepository. Esta característica de la inserción de dependencias será muy importante para nosotros en la próxima iteración, cuando compilemos pruebas unitarias para la aplicación Contact Manager.

Nota:

Si quiere desacoplar por completo la clase del controlador Contact de una implementación concreta de la interfaz IContactManagerRepository, puede aprovechar un marco de trabajo compatible con la Inserción de dependencias, como StructureMap o Microsoft Entity Framework (MEF). Al aprovechar las ventajas de un marco de Inserción de dependencias, nunca tendrá que hacer referencia a una clase concreta en su código.

Creación de una capa de servicio

Puede que se haya dado cuenta de que nuestra lógica de validación sigue mezclada con nuestra lógica de controlador en la clase de controlador modificada de la lista 3. Por la misma razón que es una buena idea aislar nuestra lógica de acceso a los datos, también lo es aislar nuestra lógica de validación.

Para solucionar este problema, podemos crear una capa de servicio independiente. La capa de servicio es una capa independiente que podemos insertar entre nuestras clases de controlador y repositorio. La capa de servicio contiene nuestra lógica de negocios, incluida toda nuestra lógica de validación.

El ContactManagerService está contenido en la lista 4. Contiene la lógica de validación de la clase del controlador Contact.

Lista 4: Models\ContactManagerService.cs

using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web.Mvc;
using ContactManager.Models.Validation;

namespace ContactManager.Models
{
    public class ContactManagerService : IContactManagerService
    {
        private IValidationDictionary _validationDictionary;
        private IContactManagerRepository _repository;

        public ContactManagerService(IValidationDictionary validationDictionary) 
            : this(validationDictionary, new EntityContactManagerRepository())
        {}

        public ContactManagerService(IValidationDictionary validationDictionary, IContactManagerRepository repository)
        {
            _validationDictionary = validationDictionary;
            _repository = repository;
        }

        public bool ValidateContact(Contact contactToValidate)
        {
            if (contactToValidate.FirstName.Trim().Length == 0)
                _validationDictionary.AddError("FirstName", "First name is required.");
            if (contactToValidate.LastName.Trim().Length == 0)
                _validationDictionary.AddError("LastName", "Last name is required.");
            if (contactToValidate.Phone.Length > 0 && !Regex.IsMatch(contactToValidate.Phone, @"((\(\d{3}\) ?)|(\d{3}-))?\d{3}-\d{4}"))
                _validationDictionary.AddError("Phone", "Invalid phone number.");
            if (contactToValidate.Email.Length > 0 && !Regex.IsMatch(contactToValidate.Email, @"^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"))
                _validationDictionary.AddError("Email", "Invalid email address.");
            return _validationDictionary.IsValid;
        }

        #region IContactManagerService Members

        public bool CreateContact(Contact contactToCreate)
        {
            // Validation logic
            if (!ValidateContact(contactToCreate))
                return false;

            // Database logic
            try
            {
                _repository.CreateContact(contactToCreate);
            }
            catch
            {
                return false;
            }
            return true;
        }

        public bool EditContact(Contact contactToEdit)
        {
            // Validation logic
            if (!ValidateContact(contactToEdit))
                return false;

            // Database logic
            try
            {
                _repository.EditContact(contactToEdit);
            }
            catch
            {
                return false;
            }
            return true;
        }

        public bool DeleteContact(Contact contactToDelete)
        {
            try
            {
                _repository.DeleteContact(contactToDelete);
            }
            catch
            {
                return false;
            }
            return true;
        }

        public Contact GetContact(int id)
        {
            return _repository.GetContact(id);
        }

        public IEnumerable<Contact> ListContacts()
        {
            return _repository.ListContacts();
        }

        #endregion
    }
}

Observe que el constructor para el ContactManagerService requiere un ValidationDictionary. La capa de servicio se comunica con la capa de controlador a través de este ValidationDictionary. Se describe el ValidationDictionary en detalle en la siguiente sección cuando se describe el patrón Decorator.

Observe, además, que el ContactManagerService implementa la interfaz IContactManagerService. Siempre debe esforzarse por programar con interfaces en lugar de con clases concretas. Otras clases de la aplicación Contact Manager no interactúan directamente con la clase ContactManagerService. En su lugar, con una excepción, el resto de la aplicación Contact Manager se programa con la interfaz IContactManagerService.

La interfaz IContactManagerService está contenida en la lista 5.

Lista 5: Models\IContactManagerService.cs

using System.Collections.Generic;

namespace ContactManager.Models
{
    public interface IContactManagerService
    {
        bool CreateContact(Contact contactToCreate);
        bool DeleteContact(Contact contactToDelete);
        bool EditContact(Contact contactToEdit);
        Contact GetContact(int id);
        IEnumerable ListContacts();
    }
}

La clase de controlador Contact modificada está en la lista 6. Observe que el controlador Contact ya no interactúa con el repositorio ContactManager. En su lugar, el controlador Contact interactúa con el servicio ContactManager. Cada capa está aislada en la medida de lo posible de otras capas.

Lista 6: Controllers\ContactController.cs

using System.Web.Mvc;
using ContactManager.Models;

namespace ContactManager.Controllers
{
    public class ContactController : Controller
    {
        private IContactManagerService _service;

        public ContactController()
        {
            _service = new ContactManagerService(new ModelStateWrapper(this.ModelState));

        }

        public ContactController(IContactManagerService service)
        {
            _service = service;
        }
        
        public ActionResult Index()
        {
            return View(_service.ListContacts());
        }

        public ActionResult Create()
        {
            return View();
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Contact contactToCreate)
        {
            if (_service.CreateContact(contactToCreate))
                return RedirectToAction("Index");
            return View();
        }

        public ActionResult Edit(int id)
        {
            return View(_service.GetContact(id));
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Edit(Contact contactToEdit)
        {
            if (_service.EditContact(contactToEdit))
                return RedirectToAction("Index");
            return View();
        }

        public ActionResult Delete(int id)
        {
            return View(_service.GetContact(id));
        }

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Delete(Contact contactToDelete)
        {
            if (_service.DeleteContact(contactToDelete))
                return RedirectToAction("Index");
            return View();
        }

    }
}

Nuestra aplicación ya no incumple el Principio de responsabilidad única (SRP). El controlador Contact de la lista 6 ha sido despojado de toda responsabilidad que no sea la de controlar el flujo de ejecución de la aplicación. Toda la lógica de validación se ha eliminado del controlador Contact y se ha trasladado a la capa de servicio. Toda la lógica de la base de datos se ha trasladado a la capa del repositorio.

Uso del patrón Decorator

Queremos poder desacoplar completamente nuestra capa de servicios de nuestra capa de controlador. En principio, deberíamos ser capaces de compilar nuestra capa de servicios en un ensamblado separado de nuestra capa de controlador sin necesidad de agregar una referencia a nuestra aplicación MVC.

Sin embargo, nuestra capa de servicio necesita poder volver a pasar mensajes de error de validación a la capa de controlador. ¿Cómo podemos habilitar la capa de servicio para comunicar mensajes de error de validación sin acoplar el controlador y la capa de servicio? Podemos aprovechar un patrón de diseño de software denominado patrón Decorator.

Un controlador usa un ModelStateDictionary llamado ModelState para representar errores de validación. Por lo tanto, puede verse tentado a pasar ModelState de la capa de controlador a la capa de servicio. Sin embargo, usar ModelState en la capa de servicio haría que su capa de servicio dependiera de una característica del marco ASP.NET MVC. Esto no sería conveniente porque, algún día, podría querer usar la capa de servicios con una aplicación WPF en lugar de una aplicación de ASP.NET MVC. En ese caso, no querrá hacer referencia al marco ASP.NET MVC para usar la clase ModelStateDictionary.

El patrón Decorator permite ajustar una clase existente en una nueva clase para implementar una interfaz. Nuestro proyecto de Administrador de contactos incluye la clase ModelStateWrapper contenida en la lista 7. La clase ModelStateWrapper implementa la interfaz de la lista 8.

Lista 7: Models\Validation\ModelStateWrapper.cs

using System.Web.Mvc;

namespace ContactManager.Models.Validation
{
    public class ModelStateWrapper : IValidationDictionary
    {
        private ModelStateDictionary _modelState;

        public ModelStateWrapper(ModelStateDictionary modelState)
        {
            _modelState = modelState;
        }

        public void AddError(string key, string errorMessage)
        {
            _modelState.AddModelError(key, errorMessage);
        }

        public bool IsValid
        {
            get { return _modelState.IsValid; }
        }
    }
}

Lista 8: Models\Validation\IValidationDictionary.cs

namespace ContactManager.Models.Validation
{
    public interface IValidationDictionary
    {
        void AddError(string key, string errorMessage);
        bool IsValid {get;}
    }
}

Si observa detenidamente la lista 5, verá que la capa de servicio ContactManager usa exclusivamente la interfaz IValidationDictionary. El servicio ContactManager no depende de la clase ModelStateDictionary. Cuando el controlador Contact crea el servicio ContactManager, el controlador encapsula su ModelState de esta manera:

_service = new ContactManagerService(new ModelStateWrapper(this.ModelState));

Resumen

En esta iteración, no hemos agregado ninguna funcionalidad nueva a la aplicación Contact Manager. El objetivo de esta iteración era refactorizar la aplicación Contact Manager para que sea más fácil de mantener y modificar.

En primer lugar, implementamos el patrón de diseño de software de repositorios. Hemos migrado todo el código de acceso a los datos a una clase de repositorio ContactManager independiente.

También aislamos nuestra lógica de validación de la lógica de nuestros controlador. Creamos una capa de servicio independiente que contiene todo nuestro código de validación. La capa de controlador interactúa con la capa de servicios, y la capa de servicios interactúa con la capa de repositorios.

Cuando creamos la capa de servicio, aprovechamos el patrón Decorator para aislar ModelState de nuestra capa de servicio. En nuestra capa de servicio, programamos con la interfaz IValidationDictionary en lugar de ModelState.

Por último, aprovechamos un patrón de diseño de software denominado patrón de Inserción de dependencias. Este patrón nos permite programar interfaces (abstracciones) en lugar de clases concretas. Implementar el patrón de diseño de Inserción de dependencias también hace que nuestro código sea más fácil de probar. En la siguiente iteración, agregaremos pruebas unitarias a nuestro proyecto.