Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
di Microsoft
In questa quarta iterazione si sfruttano diversi modelli di progettazione software per semplificare la gestione e la modifica dell'applicazione Contact Manager. Ad esempio, si esegue il refactoring dell'applicazione per usare il modello repository e il modello di inserimento delle dipendenze.
Compilazione di un'applicazione MVC ASP.NET Gestione contatti (C#)
In questa serie di esercitazioni viene creata un'intera applicazione Contact Management dall'inizio alla fine. L'applicazione Contact Manager consente di archiviare le informazioni di contatto, ovvero nomi, numeri di telefono e indirizzi di posta elettronica, per un elenco di persone.
L'applicazione viene compilata su più iterazioni. Con ogni iterazione, l'applicazione viene migliorata gradualmente. L'obiettivo di questo approccio a più iterazioni è consentire di comprendere il motivo di ogni modifica.
Iterazione n. 1: creare l'applicazione. Nella prima iterazione viene creato il Contact Manager nel modo più semplice possibile. Viene aggiunto il supporto per le operazioni di base del database: Create, Read, Update e Delete (CRUD).
Iterazione n. 2: rendere l'applicazione più bella. In questa iterazione viene migliorata l'aspetto dell'applicazione modificando la pagina master della visualizzazione MVC predefinita ASP.NET e il foglio di stile css.
Iterazione n. 3: aggiungere la convalida dei moduli. Nella terza iterazione viene aggiunta la convalida dei moduli di base. Microsoft impedisce agli utenti di inviare un modulo senza completare i campi modulo obbligatori. Vengono convalidati anche gli indirizzi di posta elettronica e i numeri di telefono.
Iterazione n. 4: rendere l'applicazione ad accoppiamento libero. In questa quarta iterazione si sfruttano diversi modelli di progettazione software per semplificare la gestione e la modifica dell'applicazione Contact Manager. Ad esempio, si esegue il refactoring dell'applicazione per usare il modello repository e il modello di inserimento delle dipendenze.
Iterazione n. 5: creare unit test. Nella quinta iterazione l'applicazione risulta più semplice da gestire e modificare aggiungendo unit test. Si simulano le classi del modello di dati e si compilano unit test per i controller e la logica di convalida.
Iterazione n. 6: usare lo sviluppo basato su test. In questa sesta iterazione si aggiungono nuove funzionalità all'applicazione scrivendo prima unit test e scrivendo codice per gli unit test. In questa iterazione si aggiungono gruppi di contatti.
Iterazione n. 7: aggiungere la funzionalità Ajax. Nella settima iterazione viene migliorata la velocità di risposta e le prestazioni dell'applicazione aggiungendo il supporto per Ajax.
Iterazione
In questa quarta iterazione dell'applicazione Contact Manager viene refactoring dell'applicazione per rendere l'applicazione più ad accoppiamento debole. Quando un'applicazione è ad accoppiamento libero, è possibile modificare il codice in una parte dell'applicazione senza dover modificare il codice in altre parti dell'applicazione. Le applicazioni ad accoppiamento libero sono più resilienti al cambiamento.
Attualmente, tutti gli accessi ai dati e la logica di convalida usati dall'applicazione Contact Manager sono contenuti nelle classi controller. Questa è una cattiva idea. Ogni volta che è necessario modificare una parte dell'applicazione, si rischia di introdurre bug in un'altra parte dell'applicazione. Ad esempio, se si modifica la logica di convalida, si rischia di introdurre nuovi bug nella logica di accesso ai dati o controller.
Nota
(SRP), una classe non deve mai avere più di un motivo per cambiare. La combinazione di controller, convalida e logica del database è una violazione massiccia del principio di responsabilità singola.
Esistono diversi motivi per cui potrebbe essere necessario modificare l'applicazione. Potrebbe essere necessario aggiungere una nuova funzionalità all'applicazione, potrebbe essere necessario correggere un bug nell'applicazione oppure potrebbe essere necessario modificare la modalità di implementazione di una funzionalità dell'applicazione. Le applicazioni sono raramente statiche. Tendono a crescere e mutare nel tempo.
Si supponga, ad esempio, di decidere di modificare la modalità di implementazione del livello di accesso ai dati. Al momento, l'applicazione Contact Manager usa Microsoft Entity Framework per accedere al database. È tuttavia possibile decidere di eseguire la migrazione a una tecnologia di accesso ai dati nuova o alternativa, ad esempio ADO.NET Data Services o NHibernate. Tuttavia, poiché il codice di accesso ai dati non è isolato dal codice di convalida e controller, non è possibile modificare il codice di accesso ai dati nell'applicazione senza modificare altro codice non direttamente correlato all'accesso ai dati.
Quando un'applicazione è ad accoppiamento libero, invece, è possibile apportare modifiche a una parte di un'applicazione senza toccare altre parti di un'applicazione. Ad esempio, è possibile cambiare le tecnologie di accesso ai dati senza modificare la logica di convalida o controller.
In questa iterazione si sfruttano diversi modelli di progettazione software che consentono di effettuare il refactoring dell'applicazione Contact Manager in un'applicazione ad accoppiamento più debole. Al termine, contact manager non esegue alcuna operazione che non ha fatto prima. Tuttavia, sarà possibile modificare più facilmente l'applicazione in futuro.
Nota
Il refactoring è il processo di riscrittura di un'applicazione in modo che non perda alcuna funzionalità esistente.
Uso del modello di progettazione del software del repository
La prima modifica consiste nel sfruttare un modello di progettazione software denominato schema repository. Si userà il modello Repository per isolare il codice di accesso ai dati dal resto dell'applicazione.
Per implementare il modello repository è necessario completare i due passaggi seguenti:
- Creare un'interfaccia
- Creare una classe concreta che implementa l'interfaccia
Prima di tutto, è necessario creare un'interfaccia che descrive tutti i metodi di accesso ai dati che è necessario eseguire. L'interfaccia IContactManagerRepository è contenuta nell'elenco 1. Questa interfaccia descrive cinque metodi: CreateContact(), DeleteContact(), EditContact(), GetContact e ListContacts().
Elenco 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();
}
}
Successivamente, è necessario creare una classe concreta che implementa l'interfaccia IContactManagerRepository. Poiché microsoft usa Microsoft Entity Framework per accedere al database, verrà creata una nuova classe denominata EntityContactManagerRepository. Questa classe è contenuta nell'elenco 2.
Elenco 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();
}
}
}
Si noti che la classe EntityContactManagerRepository implementa l'interfaccia IContactManagerRepository. La classe implementa tutti e cinque i metodi descritti da tale interfaccia.
Ci si potrebbe chiedere perché è necessario preoccuparsi con un'interfaccia. Perché è necessario creare sia un'interfaccia che una classe che la implementa?
Con un'eccezione, il resto dell'applicazione interagirà con l'interfaccia e non con la classe concreta. Anziché chiamare i metodi esposti dalla classe EntityContactManagerRepository, verranno chiamati i metodi esposti dall'interfaccia IContactManagerRepository.
In questo modo, è possibile implementare l'interfaccia con una nuova classe senza dover modificare il resto dell'applicazione. Ad esempio, in una data futura, potrebbe essere necessario implementare una classe DataServicesContactManagerRepository che implementa l'interfaccia IContactManagerRepository. La classe DataServicesContactManagerRepository può usare ADO.NET Data Services per accedere a un database anziché a Microsoft Entity Framework.
Se il codice dell'applicazione viene programmato in base all'interfaccia IContactManagerRepository anziché alla classe EntityContactManagerRepository concreta, è possibile cambiare classi concrete senza modificare il resto del codice. Ad esempio, è possibile passare dalla classe EntityContactManagerRepository alla classe DataServicesContactManagerRepository senza modificare l'accesso ai dati o la logica di convalida.
La programmazione su interfacce (astrazioni) anziché classi concrete rende l'applicazione più resiliente alle modifiche.
Nota
È possibile creare rapidamente un'interfaccia da una classe concreta all'interno di Visual Studio selezionando l'opzione di menu Refactoring, Extract Interface (Refactoring), Extract Interface (Estrai interfaccia). Ad esempio, è possibile creare prima la classe EntityContactManagerRepository e quindi usare Extract Interface per generare automaticamente l'interfaccia IContactManagerRepository.
Uso del modello di progettazione del software di inserimento delle dipendenze
Ora che è stata eseguita la migrazione del codice di accesso ai dati a una classe Repository separata, è necessario modificare il controller contact per usare questa classe. Si userà un modello di progettazione software denominato Inserimento dipendenze per usare la classe Repository nel controller.
Il controller contatto modificato è contenuto nell'elenco 3.
Elenco 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();
}
}
}
}
Si noti che il controller contatto nell'elenco 3 ha due costruttori. Il primo costruttore passa un'istanza concreta dell'interfaccia IContactManagerRepository al secondo costruttore. La classe controller Contact usa l'inserimento delle dipendenze del costruttore.
L'unica e l'unica posizione in cui viene usata la classe EntityContactManagerRepository è il primo costruttore. Il resto della classe usa l'interfaccia IContactManagerRepository anziché la classe EntityContactManagerRepository concreta.
In questo modo è possibile passare facilmente alle implementazioni della classe IContactManagerRepository in futuro. Se si vuole usare la classe DataServicesContactRepository anziché la classe EntityContactManagerRepository, è sufficiente modificare il primo costruttore.
L'inserimento delle dipendenze del costruttore rende anche la classe controller contact molto testabile. Negli unit test è possibile creare un'istanza del controller Contact passando un'implementazione fittizia della classe IContactManagerRepository. Questa funzionalità di Dependency Injection sarà molto importante per noi nell'iterazione successiva quando si compilano unit test per l'applicazione Contact Manager.
Nota
Se si vuole separare completamente la classe controller Contact da una particolare implementazione dell'interfaccia IContactManagerRepository, è possibile sfruttare un framework che supporta l'inserimento delle dipendenze, ad esempio StructureMap o Microsoft Entity Framework (MEF). Sfruttando un framework di inserimento delle dipendenze, non è mai necessario fare riferimento a una classe concreta nel codice.
Creazione di un livello di servizio
Potrebbe essere stato notato che la logica di convalida è ancora mista alla logica del controller nella classe controller modificata nell'elenco 3. Per lo stesso motivo per cui è consigliabile isolare la logica di accesso ai dati, è consigliabile isolare la logica di convalida.
Per risolvere questo problema, è possibile creare un livello di servizio separato. Il livello di servizio è un livello separato che è possibile inserire tra le classi controller e repository. Il livello di servizio contiene la logica di business, inclusa tutta la logica di convalida.
ContactManagerService è contenuto nell'elenco 4. Contiene la logica di convalida dalla classe controller Contact.
Elenco 4 - Modelli\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
}
}
Si noti che il costruttore per ContactManagerService richiede un ValidationDictionary. Il livello di servizio comunica con il livello controller tramite questo validationDictionary. Viene illustrato in dettaglio la convalidaDictionary nella sezione seguente quando si illustra il modello Decorator.
Si noti inoltre che ContactManagerService implementa l'interfaccia IContactManagerService. È consigliabile cercare sempre di programmare le interfacce anziché le classi concrete. Altre classi nell'applicazione Contact Manager non interagiscono direttamente con la classe ContactManagerService. Invece, con un'eccezione, il resto dell'applicazione Contact Manager viene programmato nell'interfaccia IContactManagerService.
L'interfaccia IContactManagerService è contenuta nell'elenco 5.
Elenco 5 - Modelli\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 classe controller Contact modificata è contenuta nell'elenco 6. Si noti che il controller Contatto non interagisce più con il repository ContactManager. Il controller Contatto interagisce invece con il servizio ContactManager. Ogni livello è isolato il più possibile da altri livelli.
Elenco 6 - Controller\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();
}
}
}
L'applicazione non esegue più l'afoul del principio di responsabilità singola (SRP). Il controller di contatto nell'elenco 6 è stato rimosso da ogni responsabilità diversa dal controllo del flusso di esecuzione dell'applicazione. Tutta la logica di convalida è stata rimossa dal controller di contatto e eseguita il push nel livello di servizio. È stato eseguito il push di tutte le logiche di database nel livello del repository.
Uso del modello decorator
Si vuole essere in grado di separare completamente il livello di servizio dal livello controller. In principio, è necessario compilare il livello di servizio in un assembly separato dal livello controller senza dover aggiungere un riferimento all'applicazione MVC.
Tuttavia, il livello di servizio deve essere in grado di passare i messaggi di errore di convalida al livello controller. Come è possibile abilitare il livello di servizio per comunicare i messaggi di errore di convalida senza accoppiare il controller e il livello di servizio? È possibile sfruttare un modello di progettazione software denominato modello Decorator.
Un controller usa un modelStateDictionary denominato ModelStatestate per rappresentare gli errori di convalida. Pertanto, potrebbe essere tentato di passare ModelState dal livello controller al livello di servizio. Tuttavia, l'uso di ModelState nel livello di servizio rende il livello di servizio dipendente da una funzionalità del framework ASP.NET MVC. Ciò potrebbe essere negativo perché, un giorno, potrebbe essere necessario usare il livello di servizio con un'applicazione WPF anziché un'applicazione MVC ASP.NET. In questo caso, non si vuole fare riferimento al framework MVC di ASP.NET per usare la classe ModelStateDictionary.
Il modello Decorator consente di eseguire il wrapping di una classe esistente in una nuova classe per implementare un'interfaccia. Il progetto Contact Manager include la classe ModelStateWrapper contenuta nell'elenco 7. La classe ModelStateWrapper implementa l'interfaccia in Listato 8.
Elenco 7 - Modelli\Convalida\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; }
}
}
}
Elenco 8 - Modelli\Convalida\IValidationDictionary.cs
namespace ContactManager.Models.Validation
{
public interface IValidationDictionary
{
void AddError(string key, string errorMessage);
bool IsValid {get;}
}
}
Se si esamina l'elenco 5, si noterà che il livello di servizio ContactManager usa esclusivamente l'interfaccia IValidationDictionary. Il servizio ContactManager non dipende dalla classe ModelStateDictionary. Quando il controller Contact crea il servizio ContactManager, il controller esegue il wrapping del modelloState simile al seguente:
_service = new ContactManagerService(new ModelStateWrapper(this.ModelState));
Riepilogo
In questa iterazione non è stata aggiunta alcuna nuova funzionalità all'applicazione Contact Manager. L'obiettivo di questa iterazione è quello di eseguire il refactoring dell'applicazione Contact Manager in modo che sia più semplice gestire e modificare.
Prima di tutto, è stato implementato il modello di progettazione software repository. È stata eseguita la migrazione di tutto il codice di accesso ai dati in una classe di repository ContactManager separata.
È stata isolata anche la logica di convalida dalla logica del controller. È stato creato un livello di servizio separato che contiene tutto il codice di convalida. Il livello controller interagisce con il livello di servizio e il livello di servizio interagisce con il livello del repository.
Quando è stato creato il livello di servizio, è stato sfruttato il modello Decorator per isolare ModelState dal livello di servizio. Nel livello di servizio è stata programmata l'interfaccia IValidationDictionary anziché ModelState.
Infine, è stato sfruttato un modello di progettazione software denominato modello di inserimento delle dipendenze. Questo modello consente di programmare le interfacce (astrazioni) anziché le classi concrete. L'implementazione del modello di progettazione di inserimento delle dipendenze rende anche il codice più testabile. Nell'iterazione successiva si aggiungono unit test al progetto.