Sdílet prostřednictvím


Iterace č. 4 – vytvoření volně spárované aplikace (C#)

od Microsoftu

Stáhnout kód

V této čtvrté iteraci využijeme několik vzorů návrhu softwaru, které usnadňují údržbu a úpravy aplikace Contact Manager. Například refaktorujeme aplikaci tak, aby používala vzor úložiště a model injektáže závislostí.

Vytvoření aplikace Správy kontaktů ASP.NET MVC (C#)

V této sérii kurzů sestavíme od začátku do konce celou aplikaci Pro správu kontaktů. Aplikace Contact Manager umožňuje ukládat kontaktní informace – jména, telefonní čísla a e-mailové adresy – pro seznam osob.

Aplikaci sestavíme pomocí několika iterací. S každou iterací aplikaci postupně vylepšujeme. Cílem tohoto přístupu s více iteracemi je pochopit důvod každé změny.

  • Iterace č. 1 – vytvořte aplikaci. V první iteraci vytvoříme Správce kontaktů nejjednodušším možným způsobem. Přidáváme podporu základních databázových operací: vytvoření, čtení, aktualizace a odstranění (CRUD).

  • Iterace č. 2 – vzhled aplikace V této iteraci vylepšujeme vzhled aplikace úpravou výchozí ASP.NET stránky předlohy zobrazení MVC a šablony stylů CSS.

  • Iterace č. 3 – přidání ověření formuláře Ve třetí iteraci přidáme základní ověření formuláře. Bráníme uživatelům v odeslání formuláře bez vyplnění požadovaných polí formuláře. Ověřujeme také e-mailové adresy a telefonní čísla.

  • Iterace č. 4 – Nastavte aplikaci volně na párování. V této čtvrté iteraci využijeme několik vzorů návrhu softwaru, které usnadňují údržbu a úpravy aplikace Contact Manager. Například refaktorujeme aplikaci tak, aby používala vzor úložiště a model injektáže závislostí.

  • Iterace č. 5 – vytvoření testů jednotek V páté iteraci usnadňujeme údržbu a úpravy naší aplikace přidáním testů jednotek. Vysmíváme si třídy datového modelu a sestavujeme testy jednotek pro naše kontrolery a logiku ověřování.

  • Iterace č. 6 – použijte vývoj řízený testy. V této šesté iteraci přidáme do naší aplikace nové funkce tím, že nejprve napíšeme testy jednotek a proti testům jednotek napíšeme kód. V této iteraci přidáme skupiny kontaktů.

  • Iterace č. 7 – přidání funkcí Ajax V sedmé iteraci vylepšujeme rychlost odezvy a výkon naší aplikace přidáním podpory pro Ajax.

Tato iterace

V této čtvrté iteraci aplikace Contact Manager refaktorujeme aplikaci tak, aby byla volněji provázána. Když je aplikace volně svázána, můžete upravit kód v jedné části aplikace, aniž byste museli upravovat kód v jiných částech aplikace. Volně vázané aplikace jsou odolnější vůči změnám.

V současné době je veškerá logika přístupu k datům a ověřování používaná aplikací Contact Manager obsažena v třídách kontroleru. To je špatný nápad. Kdykoli potřebujete upravit jednu část aplikace, riskujete, že do jiné části aplikace zavlenete chyby. Pokud například upravíte logiku ověřování, riskujete, že do logiky přístupu k datům nebo kontroleru vnášíte nové chyby.

Poznámka

(SRP), třída by nikdy neměla mít více než jeden důvod ke změně. Kombinování kontroleru, ověřování a logiky databáze je velkým porušením principu single responsibility.

K úpravě aplikace může být potřeba mít několik důvodů. Možná budete muset do aplikace přidat novou funkci, možná budete muset opravit chybu v aplikaci nebo budete muset upravit způsob implementace funkce vaší aplikace. Aplikace jsou zřídka statické. Mají tendenci růst a mutovat v průběhu času.

Představte si například, že se rozhodnete změnit způsob implementace vrstvy přístupu k datům. Aplikace Contact Manager teď pro přístup k databázi používá Microsoft Entity Framework. Můžete se ale rozhodnout migrovat na novou nebo alternativní technologii přístupu k datům, jako je ADO.NET Data Services nebo NHibernate. Vzhledem k tomu, že kód pro přístup k datům není izolovaný od kódu ověření a kontroleru, neexistuje způsob, jak kód pro přístup k datům ve vaší aplikaci upravit bez úpravy jiného kódu, který s přístupem k datům přímo nesouvisí.

Pokud je aplikace volně svázána, můžete na druhé straně provádět změny v jedné části aplikace, aniž byste se dotýkali jiných částí aplikace. Můžete například přepínat technologie přístupu k datům beze změny logiky ověřování nebo kontroleru.

V této iteraci využijeme několik vzorů návrhu softwaru, které nám umožňují refaktorovat aplikaci Contact Manager do volněji propojené aplikace. Až skončíme, správce kontaktů nebude dělat nic, co předtím neudělal. V budoucnu ale budeme moct aplikaci snadněji změnit.

Poznámka

Refaktoring je proces přepsání aplikace takovým způsobem, aby neztratil žádné existující funkce.

Použití vzoru návrhu softwaru úložiště

Naší první změnou je využít vzor návrhu softwaru, který se nazývá model úložiště. Vzor úložiště použijeme k izolaci kódu pro přístup k datům od zbytku naší aplikace.

Implementace modelu úložiště vyžaduje, abychom dokončili následující dva kroky:

  1. Vytvoření rozhraní
  2. Vytvoření konkrétní třídy, která implementuje rozhraní

Nejprve musíme vytvořit rozhraní, které popisuje všechny metody přístupu k datům, které potřebujeme provést. Rozhraní IContactManagerRepository je obsaženo v výpisu 1. Toto rozhraní popisuje pět metod: CreateContact(), DeleteContact(), EditContact(), GetContact a ListContacts().

Výpis 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();

    }
}

Dále musíme vytvořit konkrétní třídu, která implementuje rozhraní IContactManagerRepository. Vzhledem k tomu, že pro přístup k databázi používáme Microsoft Entity Framework, vytvoříme novou třídu s názvem EntityContactManagerRepository. Tato třída je obsažena ve výpisu 2.

Výpis 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();
        }

    }
}

Všimněte si, že Třída EntityContactManagerRepository implementuje rozhraní IContactManagerRepository. Třída implementuje všech pět metod popsaných tímto rozhraním.

Možná vás zajímá, proč se musíme obtěžovat s rozhraním. Proč potřebujeme vytvořit rozhraní i třídu, která ho implementuje?

Až na jednu výjimku bude zbytek naší aplikace pracovat s rozhraním, a ne s konkrétní třídou. Místo volání metod vystavených třídou EntityContactManagerRepository budeme volat metody vystavené rozhraním IContactManagerRepository.

Tímto způsobem můžeme implementovat rozhraní s novou třídou, aniž bychom museli upravovat zbytek naší aplikace. Například v budoucnosti můžeme chtít implementovat DataServicesContactManagerRepository třídy, která implementuje rozhraní IContactManagerRepository. DataServicesContactManagerRepository Třída může používat ADO.NET Data Services pro přístup k databázi místo Microsoft Entity Framework.

Pokud je kód aplikace naprogramován proti rozhraní IContactManagerRepository místo konkrétní třídy EntityContactManagerRepository, můžeme přepnout konkrétní třídy beze změny jakékoli zbývající části našeho kódu. Můžeme například přepnout z entityContactManagerRepository třídy DataServicesContactManagerRepository beze změny přístupu k datům nebo logiky ověřování.

Programování proti rozhraním (abstrakcím) místo konkrétních tříd dělá naši aplikaci odolnější vůči změnám.

Poznámka

V sadě Visual Studio můžete rychle vytvořit rozhraní z konkrétní třídy výběrem možnosti nabídky Refaktorovat, Extrahovat rozhraní. Můžete například nejprve vytvořit třídu EntityContactManagerRepository a pak pomocí rozhraní Extract vygenerovat rozhraní IContactManagerRepository automaticky.

Použití vzoru návrhu softwaru pro injektáž závislostí

Teď, když jsme migrovali kód pro přístup k datům do samostatné třídy Úložiště, musíme upravit kontroler kontaktů tak, aby tuto třídu používal. K použití třídy Repository v našem kontroleru využijeme vzor návrhu softwaru s názvem Injektáž závislostí.

Upravený kontroler kontaktů je obsažen ve výpisu 3.

Výpis 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();
            }
        }

    }
}

Všimněte si, že kontroler kontaktu ve výpisu 3 má dva konstruktory. První konstruktor předává konkrétní instanci IContactManagerRepository rozhraní druhému konstruktoru. Třída kontroleru kontaktů používá injektáž závislostí konstruktoru.

Jediné místo, kde je použita třída EntityContactManagerRepository, je v prvním konstruktoru. Zbývající třída používá rozhraní IContactManagerRepository místo konkrétní třídy EntityContactManagerRepository.

To usnadňuje přepínání implementací IContactManagerRepository třídy v budoucnu. Pokud chcete použít Třídu DataServicesContactRepository místo třídy EntityContactManagerRepository, stačí upravit první konstruktor.

Injektáž závislostí konstruktoru také velmi testuje třídu kontroleru kontaktů. V testech jednotek můžete vytvořit instanci kontroleru kontaktu předáním napodobené implementace IContactManagerRepository třídy. Tato funkce injektáže závislostí bude pro nás velmi důležitá v další iteraci při sestavování testů jednotek pro aplikaci Contact Manager.

Poznámka

Pokud chcete zcela oddělit třídu kontroleru kontaktů od konkrétní implementace rozhraní IContactManagerRepository pak můžete využít rozhraní, které podporuje injektáž závislostí, jako je Například StructureMap nebo Microsoft Entity Framework (MEF). Když využijete architekturu injektáže závislostí, nemusíte v kódu odkazovat na konkrétní třídu.

Vytvoření vrstvy služby

Možná jste si všimli, že naše logika ověřování je stále smíšená s logikou kontroleru ve třídě upraveného kontroleru ve výpisu 3. Ze stejného důvodu, jako je vhodné izolovat logiku přístupu k datům, je vhodné izolovat logiku ověřování.

Abychom tento problém vyřešili, můžeme vytvořit samostatnou vrstvu služby. Vrstva služby je samostatná vrstva, kterou můžeme vložit mezi třídu kontroleru a úložiště. Vrstva služby obsahuje naši obchodní logiku včetně veškeré logiky ověřování.

Služba ContactManagerService je obsažena ve výpisu 4. Obsahuje logiku ověřování z třídy Kontroleru kontaktů.

Výpis 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
    }
}

Všimněte si, že konstruktor pro ContactManagerService vyžaduje ValidationDictionary. Vrstva služby komunikuje s vrstvou kontroleru prostřednictvím tohoto ověřovacího slovníku. Ověřovací slovník probereme podrobně v následující části, kde probereme model Dekorator.

Všimněte si také, že ContactManagerService implementuje rozhraní IContactManagerService. Měli byste se vždy snažit programovat proti rozhraním místo konkrétních tříd. Jiné třídy v aplikaci Contact Manager nepracují přímo s třídou ContactManagerService. Místo toho, s jednou výjimkou, zbytek aplikace Contact Manager je naprogramován na IContactManagerService rozhraní.

Rozhraní IContactManagerService je obsaženo ve výpisu 5.

Výpis 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();
    }
}

Upravená třída kontroleru kontaktů je obsažena ve výpisu 6. Všimněte si, že kontroler kontaktů už nepracuje s úložištěm ContactManager. Místo toho kontakt kontroler komunikuje se službou ContactManager. Každá vrstva je co nejvíce izolovaná od ostatních vrstev.

Výpis 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();
        }

    }
}

Naše aplikace už není v souladu s principem SRP (Single Responsibility Principle). Kontroleru kontaktů ve výpisu 6 byly odebrány všechny zodpovědnosti kromě řízení toku provádění aplikace. Veškerá logika ověřování byla odebrána z kontroleru kontaktu a vložena do vrstvy služby. Veškerá logika databáze byla vložena do vrstvy úložiště.

Použití vzoru dekorátoru

Chceme mít možnost zcela oddělit vrstvu služby od vrstvy kontroleru. V zásadě bychom měli být schopni zkompilovat naši vrstvu služby v samostatném sestavení, než je vrstva kontroleru, aniž bychom museli přidávat odkaz na naši aplikaci MVC.

Naše vrstva služby ale musí být schopná předávat chybové zprávy ověřování zpět vrstvě kontroleru. Jak můžeme vrstvě služby povolit, aby předávala chybové zprávy ověřování bez propojení kontroleru a vrstvy služby? Můžeme využít vzor návrhu softwaru s názvem Dekorátor.

Kontroler používá ModelStateDictionary s názvem ModelState k reprezentaci chyb ověření. Proto můžete být v pokušení předat ModelState z vrstvy kontroleru do vrstvy služby. Pokud ale použijete ModelState ve vrstvě služby, bude vaše vrstva služby závislá na funkci architektury ASP.NET MVC. To by bylo špatné, protože jednou budete chtít použít vrstvu služby s aplikací WPF místo aplikace ASP.NET MVC. V takovém případě byste nechtěli odkazovat na architekturu ASP.NET MVC pro použití třídy ModelStateDictionary.

Model Dekorátor umožňuje zabalit existující třídu do nové třídy, aby bylo možné implementovat rozhraní. Náš projekt Contact Manageru zahrnuje třídu ModelStateWrapper obsaženou ve výpisu 7. ModelStateWrapper Třída implementuje rozhraní ve výpisu 8.

Výpis 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; }
        }
    }
}

Výpis 8 – Models\Validation\IValidationDictionary.cs

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

Pokud se na výpis 5 podíváte zblízka, uvidíte, že vrstva služby ContactManager používá výhradně rozhraní IValidationDictionary. Služba Není závislá na ModelStateDictionary třídy. Když kontroler kontaktů vytvoří službu ContactManager, zabalí kontroler modelState takto:

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

Souhrn

V této iteraci jsme do aplikace Contact Manager nepřidali žádné nové funkce. Cílem této iterace bylo refaktorovat aplikaci Contact Manager tak, aby se snadněji udržovala a upravovala.

Nejprve jsme implementovali vzor návrhu softwaru repository. Veškerý kód pro přístup k datům jsme migrovali do samostatné třídy úložiště ContactManager.

Také jsme oddělili logiku ověřování od logiky kontroleru. Vytvořili jsme samostatnou vrstvu služby, která obsahuje veškerý ověřovací kód. Vrstva kontroleru komunikuje s vrstvou služby a vrstva služby komunikuje s vrstvou úložiště.

Při vytváření vrstvy služby jsme využili vzor Decorator k izolování modelu ModelState od vrstvy služby. V naší vrstvě služby jsme naprogramovali rozhraní IValidationDictionary místo modelState.

Nakonec jsme využili vzor návrhu softwaru s názvem Model injektáže závislostí. Tento model nám umožňuje programovat proti rozhraním (abstrakcím) místo konkrétních tříd. Implementace vzoru návrhu injektáže závislostí také zpřístupňuje testování našeho kódu. V další iteraci přidáme do projektu testy jednotek.