Compilar um modelo com validações de regra de negócios
pela Microsoft
Esta é a etapa 3 de um tutorial gratuito de aplicativo "NerdDinner" que explica como criar um aplicativo Web pequeno, mas completo, usando ASP.NET MVC 1.
A etapa 3 mostra como criar um modelo que podemos usar para consultar e atualizar o banco de dados para nosso aplicativo NerdDinner.
Se você estiver usando ASP.NET MVC 3, recomendamos que siga os tutoriais do Introdução With MVC 3 ou MVC Music Store.
NerdDinner Etapa 3: Criando o modelo
Em uma estrutura model-view-controller, o termo "modelo" refere-se aos objetos que representam os dados do aplicativo, bem como à lógica de domínio correspondente que integra a validação e as regras de negócios a ele. O modelo é, de muitas maneiras, o "coração" de um aplicativo baseado em MVC e, como veremos mais tarde, orienta fundamentalmente o comportamento dele.
A estrutura do ASP.NET MVC dá suporte ao uso de qualquer tecnologia de acesso a dados, e os desenvolvedores podem escolher entre uma variedade de opções de dados avançadas do .NET para implementar seus modelos, incluindo: LINQ to Entities, LINQ to SQL, NHibernate, LLBLGen Pro, SubSonic, WilsonORM ou apenas ADO.NET DataReaders ou DataSets brutos.
Para nosso aplicativo NerdDinner, vamos usar LINQ to SQL para criar um modelo simples que corresponda bastante ao nosso design de banco de dados e adicione algumas regras de negócio e lógica de validação personalizadas. Em seguida, implementaremos uma classe de repositório que ajuda a abstrair a implementação de persistência de dados do restante do aplicativo e nos permite testá-la facilmente.
LINQ to SQL
LINQ to SQL é um ORM (mapeador relacional de objeto) que é fornecido como parte do .NET 3.5.
LINQ to SQL fornece uma maneira fácil de mapear tabelas de banco de dados para classes .NET com as quais podemos codificar. Para nosso aplicativo NerdDinner, vamos usá-lo para mapear as tabelas Dinners e RSVP em nosso banco de dados para as classes Dinner e RSVP. As colunas das tabelas Dinners e RSVP corresponderão às propriedades nas classes Dinner e RSVP. Cada objeto Dinner e RSVP representará uma linha separada dentro das tabelas Dinners ou RSVP no banco de dados.
LINQ to SQL nos permite evitar a necessidade de construir manualmente instruções SQL para recuperar e atualizar objetos Dinner e RSVP com dados de banco de dados. Em vez disso, definiremos as classes Dinner e RSVP, como elas são mapeadas de/para o banco de dados e as relações entre elas. LINQ to SQL cuidará da geração da lógica de execução do SQL apropriada a ser usada em runtime quando interagirmos e usá-las.
Podemos usar o suporte à linguagem LINQ em VB e C# para escrever consultas expressivas que recuperam objetos Dinner e RSVP do banco de dados. Isso minimiza a quantidade de código de dados que precisamos escrever e nos permite criar aplicativos realmente limpo.
Adicionando classes LINQ to SQL ao nosso projeto
Começaremos clicando com o botão direito do mouse na pasta "Modelos" em nosso projeto e selecionamos o comando de menu Adicionar> Novo Item :
Isso abrirá a caixa de diálogo "Adicionar Novo Item". Filtraremos pela categoria "Dados" e selecionaremos o modelo "Classes LINQ to SQL" dentro dele:
Vamos nomear o item "NerdDinner" e clicar no botão "Adicionar". O Visual Studio adicionará um arquivo NerdDinner.dbml em nosso diretório \Models e abrirá o designer relacional do objeto LINQ to SQL:
Criando classes de modelo de dados com LINQ to SQL
LINQ to SQL nos permite criar rapidamente classes de modelo de dados do esquema de banco de dados existente. Para fazer isso, abriremos o banco de dados NerdDinner no servidor Explorer e selecionaremos as Tabelas que desejamos modelar nele:
Em seguida, podemos arrastar as tabelas para a superfície do designer LINQ to SQL. Quando fizermos esse LINQ to SQL criará automaticamente classes Dinner e RSVP usando o esquema das tabelas (com propriedades de classe mapeadas para as colunas da tabela de banco de dados):
Por padrão, o designer de LINQ to SQL automaticamente "pluraliza" nomes de tabela e coluna quando cria classes com base em um esquema de banco de dados. Por exemplo: a tabela "Jantares" em nosso exemplo acima resultou em uma classe "Jantar". Essa nomenclatura de classe ajuda a tornar nossos modelos consistentes com convenções de nomenclatura do .NET, e geralmente acho que fazer com que o designer corrija isso é conveniente (especialmente ao adicionar muitas tabelas). No entanto, se você não gostar do nome de uma classe ou propriedade gerada pelo designer, sempre poderá substituí-lo e alterá-lo para qualquer nome desejado. Você pode fazer isso editando o nome da entidade/propriedade na linha dentro do designer ou modificando-o por meio da grade de propriedades.
Por padrão, o designer de LINQ to SQL também inspeciona as relações de chave primária/chave estrangeira das tabelas e, com base nelas, cria automaticamente "associações de relação" padrão entre as diferentes classes de modelo criadas por ele. Por exemplo, quando arrastamos as tabelas Dinners e RSVP para o designer LINQ to SQL uma associação de relação um-para-muitos entre os dois foi inferida com base no fato de que a tabela RSVP tinha uma chave estrangeira para a tabela Dinners (isso é indicado pela seta no designer):
A associação acima fará com que LINQ to SQL adicione uma propriedade "Dinner" fortemente tipada à classe RSVP que os desenvolvedores podem usar para acessar o Jantar associado a um determinado RSVP. Isso também fará com que a classe Dinner tenha uma propriedade de coleção "RSVPs" que permite aos desenvolvedores recuperar e atualizar objetos RSVP associados a um jantar específico.
Abaixo, você pode ver um exemplo de intelliSense no Visual Studio quando criamos um novo objeto RSVP e o adicionamos a uma coleção RSVPs do Dinner. Observe como LINQ to SQL adicionado automaticamente uma coleção "RSVPs" no objeto Dinner:
Ao adicionar o objeto RSVP à coleção RSVPs do Dinner, estamos dizendo a LINQ to SQL associar uma relação de chave estrangeira entre o Dinner e a linha RSVP em nosso banco de dados:
Se você não gostar de como o designer modelou ou nomeou uma associação de tabela, poderá substituí-la. Basta clicar na seta de associação dentro do designer e acessar suas propriedades por meio da grade de propriedades para renomeá-la, excluí-la ou modificá-la. No entanto, para nosso aplicativo NerdDinner, as regras de associação padrão funcionam bem para as classes de modelo de dados que estamos criando e podemos apenas usar o comportamento padrão.
Classe NerdDinnerDataContext
O Visual Studio criará automaticamente classes .NET que representam os modelos e as relações de banco de dados definidas usando o designer LINQ to SQL. Uma classe LINQ to SQL DataContext também é gerada para cada arquivo de designer LINQ to SQL adicionado à solução. Como nomeamos nosso item de classe LINQ to SQL "NerdDinner", a classe DataContext criada será chamada de "NerdDinnerDataContext". Essa classe NerdDinnerDataContext é a principal maneira de interagirmos com o banco de dados.
Nossa classe NerdDinnerDataContext expõe duas propriedades - "Dinners" e "RSVPs" - que representam as duas tabelas que modelamos no banco de dados. Podemos usar C# para gravar consultas LINQ nessas propriedades para consultar e recuperar objetos Dinner e RSVP do banco de dados.
O código a seguir demonstra como instanciar um objeto NerdDinnerDataContext e executar uma consulta LINQ nele para obter uma sequência de Jantares que ocorrem no futuro. O Visual Studio fornece intelliSense completo ao escrever a consulta LINQ, e os objetos retornados dela são fortemente tipado e também dão suporte ao intelliSense:
Além de nos permitir consultar objetos Dinner e RSVP, um NerdDinnerDataContext também controla automaticamente todas as alterações feitas posteriormente nos objetos Dinner e RSVP que recuperamos por meio dele. Podemos usar essa funcionalidade para salvar facilmente as alterações no banco de dados sem precisar escrever nenhum código de atualização SQL explícito.
Por exemplo, o código abaixo demonstra como usar uma consulta LINQ para recuperar um único objeto Dinner do banco de dados, atualizar duas das propriedades Dinner e salvar as alterações de volta no banco de dados:
NerdDinnerDataContext db = new NerdDinnerDataContext();
// Retrieve Dinner object that reprents row with DinnerID of 1
Dinner dinner = db.Dinners.Single(d => d.DinnerID == 1);
// Update two properties on Dinner
dinner.Title = "Changed Title";
dinner.Description = "This dinner will be fun";
// Persist changes to database
db.SubmitChanges();
O objeto NerdDinnerDataContext no código acima controlou automaticamente as alterações de propriedade feitas no objeto Dinner que recuperamos dele. Quando chamamos o método "SubmitChanges()", ele executará uma instrução SQL "UPDATE" apropriada para o banco de dados para manter os valores atualizados novamente.
Criando uma classe DinnerRepository
Às vezes, para aplicativos pequenos, é bom fazer com que os Controladores funcionem diretamente em uma classe LINQ to SQL DataContext e insira consultas LINQ dentro dos Controladores. À medida que os aplicativos ficam maiores, porém, essa abordagem torna-se complicada para manter e testar. Isso também pode nos levar a duplicar as mesmas consultas LINQ em vários lugares.
Uma abordagem que pode tornar os aplicativos mais fáceis de manter e testar é usar um padrão de "repositório". Uma classe de repositório ajuda a encapsular a consulta de dados e a lógica de persistência e abstrai os detalhes de implementação da persistência de dados do aplicativo. Além de tornar o código do aplicativo mais limpo, o uso de um padrão de repositório pode facilitar a alteração das implementações de armazenamento de dados no futuro e pode ajudar a facilitar o teste de unidade de um aplicativo sem a necessidade de um banco de dados real.
Para nosso aplicativo NerdDinner, definiremos uma classe DinnerRepository com a assinatura abaixo:
public class DinnerRepository {
// Query Methods
public IQueryable<Dinner> FindAllDinners();
public IQueryable<Dinner> FindUpcomingDinners();
public Dinner GetDinner(int id);
// Insert/Delete
public void Add(Dinner dinner);
public void Delete(Dinner dinner);
// Persistence
public void Save();
}
Observação: posteriormente neste capítulo, extrairemos uma interface IDinnerRepository dessa classe e habilitaremos a injeção de dependência com ela em nossos Controladores. Para começar, porém, vamos começar simples e trabalhar diretamente com a classe DinnerRepository.
Para implementar essa classe, clicaremos com o botão direito do mouse em nossa pasta "Modelos" e escolheremos o comando de menu Adicionar> Novo Item . Na caixa de diálogo "Adicionar Novo Item", selecionaremos o modelo "Classe" e nomearemos o arquivo "DinnerRepository.cs":
Em seguida, podemos implementar nossa classe DinnerRepository usando o código abaixo:
public class DinnerRepository {
private NerdDinnerDataContext db = new NerdDinnerDataContext();
//
// Query Methods
public IQueryable<Dinner> FindAllDinners() {
return db.Dinners;
}
public IQueryable<Dinner> FindUpcomingDinners() {
return from dinner in db.Dinners
where dinner.EventDate > DateTime.Now
orderby dinner.EventDate
select dinner;
}
public Dinner GetDinner(int id) {
return db.Dinners.SingleOrDefault(d => d.DinnerID == id);
}
//
// Insert/Delete Methods
public void Add(Dinner dinner) {
db.Dinners.InsertOnSubmit(dinner);
}
public void Delete(Dinner dinner) {
db.RSVPs.DeleteAllOnSubmit(dinner.RSVPs);
db.Dinners.DeleteOnSubmit(dinner);
}
//
// Persistence
public void Save() {
db.SubmitChanges();
}
}
Recuperando, atualizando, inserindo e excluindo usando a classe DinnerRepository
Agora que criamos nossa classe DinnerRepository, vamos examinar alguns exemplos de código que demonstram tarefas comuns que podemos fazer com ela:
Exemplos de consulta
O código a seguir recupera um único Jantar usando o valor DinnerID:
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
O código a seguir recupera todos os próximos jantares e loops sobre eles:
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve all upcoming Dinners
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
// Loop over each upcoming Dinner and print out its Title
foreach (Dinner dinner in upcomingDinners) {
Response.Write("Title" + dinner.Title);
}
Inserir e atualizar exemplos
O código abaixo demonstra a adição de dois novos jantares. As adições/modificações no repositório não são confirmadas no banco de dados até que o método "Save()" seja chamado nele. LINQ to SQL encapsula automaticamente todas as alterações em uma transação de banco de dados, portanto, todas as alterações ocorrem ou nenhuma delas ocorre quando nosso repositório salva:
DinnerRepository dinnerRepository = new DinnerRepository();
// Create First Dinner
Dinner newDinner1 = new Dinner();
newDinner1.Title = "Dinner with Scott";
newDinner1.HostedBy = "ScotGu";
newDinner1.ContactPhone = "425-703-8072";
// Create Second Dinner
Dinner newDinner2 = new Dinner();
newDinner2.Title = "Dinner with Bill";
newDinner2.HostedBy = "BillG";
newDinner2.ContactPhone = "425-555-5151";
// Add Dinners to Repository
dinnerRepository.Add(newDinner1);
dinnerRepository.Add(newDinner2);
// Persist Changes
dinnerRepository.Save();
O código a seguir recupera um objeto Dinner existente e modifica duas propriedades nele. As alterações são confirmadas novamente no banco de dados quando o método "Save()" é chamado em nosso repositório:
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
// Update Dinner properties
dinner.Title = "Update Title";
dinner.HostedBy = "New Owner";
// Persist changes
dinnerRepository.Save();
O código a seguir recupera um jantar e adiciona um RSVP a ele. Ele faz isso usando a coleção RSVPs no objeto Dinner que LINQ to SQL criado para nós (porque há uma relação chave-primária/chave estrangeira entre os dois no banco de dados). Essa alteração é mantida de volta para o banco de dados como uma nova linha de tabela RSVP quando o método "Save()" é chamado no repositório:
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
// Create a new RSVP object
RSVP myRSVP = new RSVP();
myRSVP.AttendeeName = "ScottGu";
// Add RSVP to Dinner's RSVP Collection
dinner.RSVPs.Add(myRSVP);
// Persist changes
dinnerRepository.Save();
Exemplo de exclusão
O código a seguir recupera um objeto Dinner existente e, em seguida, marca-o para ser excluído. Quando o método "Save()" for chamado no repositório, ele confirmará a exclusão de volta para o banco de dados:
DinnerRepository dinnerRepository = new DinnerRepository();
// Retrieve specific dinner by its DinnerID
Dinner dinner = dinnerRepository.GetDinner(5);
// Mark dinner to be deleted
dinnerRepository.Delete(dinner);
// Persist changes
dinnerRepository.Save();
Integrando validação e lógica de regra de negócios com classes de modelo
A integração da validação e da lógica de regra de negócios é uma parte fundamental de qualquer aplicativo que funcione com dados.
Validação de esquema
Quando as classes de modelo são definidas usando o designer de LINQ to SQL, os tipos de dados das propriedades nas classes de modelo de dados correspondem aos tipos de dados da tabela de banco de dados. Por exemplo: se a coluna "EventDate" na tabela Dinners for um "datetime", a classe de modelo de dados criada por LINQ to SQL será do tipo "DateTime" (que é um tipo de dados interno do .NET). Isso significa que você receberá erros de compilação se tentar atribuir um inteiro ou booliano a ele a partir do código, e isso gerará um erro automaticamente se você tentar converter implicitamente um tipo de cadeia de caracteres não válido nele em runtime.
LINQ to SQL também manipulará automaticamente os valores sql de escape para você ao usar cadeias de caracteres , o que ajuda a protegê-lo contra ataques de injeção de SQL ao usá-lo.
Validação e lógica de regra de negócios
A validação de esquema é útil como uma primeira etapa, mas raramente é suficiente. A maioria dos cenários do mundo real exige a capacidade de especificar uma lógica de validação mais avançada que pode abranger várias propriedades, executar código e, muitas vezes, ter reconhecimento do estado de um modelo (por exemplo: está sendo criado/atualizado/excluído ou dentro de um estado específico do domínio, como "arquivado"). Há uma variedade de diferentes padrões e estruturas que podem ser usados para definir e aplicar regras de validação a classes de modelo e há várias estruturas baseadas em .NET que podem ser usadas para ajudar com isso. Você pode usar praticamente qualquer um deles em ASP.NET aplicativos MVC.
Para os fins de nosso aplicativo NerdDinner, usaremos um padrão relativamente simples e direto em que expomos uma propriedade IsValid e um método GetRuleViolations() em nosso objeto modelo Dinner. A propriedade IsValid retornará true ou false dependendo se as regras de validação e de negócios são válidas. O método GetRuleViolations() retornará uma lista de erros de regra.
Implementaremos IsValid e GetRuleViolations() para nosso modelo Dinner adicionando uma "classe parcial" ao nosso projeto. Classes parciais podem ser usadas para adicionar métodos/propriedades/eventos a classes mantidas por um designer vs (como a classe Dinner gerada pelo designer LINQ to SQL) e ajudar a evitar que a ferramenta mexa com nosso código. Podemos adicionar uma nova classe parcial ao nosso projeto clicando com o botão direito do mouse na pasta \Models e, em seguida, selecionando o comando de menu "Adicionar Novo Item". Em seguida, podemos escolher o modelo "Classe" na caixa de diálogo "Adicionar Novo Item" e nomeá-lo Dinner.cs.
Clicar no botão "Adicionar" adicionará um arquivo Dinner.cs ao nosso projeto e o abrirá no IDE. Em seguida, podemos implementar uma estrutura básica de imposição de regra/validação usando o código abaixo:
public partial class Dinner {
public bool IsValid {
get { return (GetRuleViolations().Count() == 0); }
}
public IEnumerable<RuleViolation> GetRuleViolations() {
yield break;
}
partial void OnValidate(ChangeAction action) {
if (!IsValid)
throw new ApplicationException("Rule violations prevent saving");
}
}
public class RuleViolation {
public string ErrorMessage { get; private set; }
public string PropertyName { get; private set; }
public RuleViolation(string errorMessage, string propertyName) {
ErrorMessage = errorMessage;
PropertyName = propertyName;
}
}
Algumas observações sobre o código acima:
- A classe Dinner é precedida por uma palavra-chave "parcial", o que significa que o código contido nela será combinado com a classe gerada/mantida pelo designer de LINQ to SQL e compilada em uma única classe.
- A classe RuleViolation é uma classe auxiliar que adicionaremos ao projeto que nos permite fornecer mais detalhes sobre uma violação de regra.
- O método Dinner.GetRuleViolations() faz com que nossas regras de validação e de negócios sejam avaliadas (vamos implementá-las em breve). Em seguida, ele retorna uma sequência de objetos RuleViolation que fornecem mais detalhes sobre quaisquer erros de regra.
- A propriedade Dinner.IsValid fornece uma propriedade auxiliar conveniente que indica se o objeto Dinner tem RuleViolations ativo. Ele pode ser verificado proativamente por um desenvolvedor usando o objeto Dinner a qualquer momento (e não gera uma exceção).
- O método parcial Dinner.OnValidate() é um gancho que LINQ to SQL fornece que nos permite ser notificados sempre que o objeto Dinner está prestes a ser persistido no banco de dados. Nossa implementação onValidate() acima garante que o Jantar não tenha RuleViolations antes de ser salvo. Se estiver em um estado inválido, ele gerará uma exceção, o que fará com que LINQ to SQL anule a transação.
Essa abordagem fornece uma estrutura simples na qual podemos integrar as regras de validação e de negócios. Por enquanto, vamos adicionar as regras abaixo ao nosso método GetRuleViolations():
public IEnumerable<RuleViolation> GetRuleViolations() {
if (String.IsNullOrEmpty(Title))
yield return new RuleViolation("Title required","Title");
if (String.IsNullOrEmpty(Description))
yield return new RuleViolation("Description required","Description");
if (String.IsNullOrEmpty(HostedBy))
yield return new RuleViolation("HostedBy required", "HostedBy");
if (String.IsNullOrEmpty(Address))
yield return new RuleViolation("Address required", "Address");
if (String.IsNullOrEmpty(Country))
yield return new RuleViolation("Country required", "Country");
if (String.IsNullOrEmpty(ContactPhone))
yield return new RuleViolation("Phone# required", "ContactPhone");
if (!PhoneValidator.IsValidNumber(ContactPhone, Country))
yield return new RuleViolation("Phone# does not match country", "ContactPhone");
yield break;
}
Estamos usando o recurso "retorno de rendimento" do C# para retornar uma sequência de qualquer RuleViolations. As seis primeiras verificações de regra acima simplesmente impõem que as propriedades de cadeia de caracteres em nosso Jantar não podem ser nulas ou vazias. A última regra é um pouco mais interessante e chama um método auxiliar PhoneValidator.IsValidNumber() que podemos adicionar ao nosso projeto para verificar se o formato de número contactphone corresponde ao país/região do Jantar.
Podemos usar . Suporte à expressão regular do NET para implementar esse suporte de validação por telefone. Veja abaixo uma implementação simples do PhoneValidator que podemos adicionar ao nosso projeto que nos permite adicionar verificações de padrão Regex específicas do país/região:
public class PhoneValidator {
static IDictionary<string, Regex> countryRegex = new Dictionary<string, Regex>() {
{ "USA", new Regex("^[2-9]\\d{2}-\\d{3}-\\d{4}$")},
{ "UK", new Regex("(^1300\\d{6}$)|(^1800|1900|1902\\d{6}$)|(^0[2|3|7|8]{1}[0-9]{8}$)|(^13\\d{4}$)|(^04\\d{2,3}\\d{6}$)")},
{ "Netherlands", new Regex("(^\\+[0-9]{2}|^\\+[0-9]{2}\\(0\\)|^\\(\\+[0-9]{2}\\)\\(0\\)|^00[0-9]{2}|^0)([0-9]{9}$|[0-9\\-\\s]{10}$)")},
};
public static bool IsValidNumber(string phoneNumber, string country) {
if (country != null && countryRegex.ContainsKey(country))
return countryRegex[country].IsMatch(phoneNumber);
else
return false;
}
public static IEnumerable<string> Countries {
get {
return countryRegex.Keys;
}
}
}
Manipulando violações de validação e lógica de negócios
Agora que adicionamos o código de regra de negócios e validação acima, sempre que tentarmos criar ou atualizar um Jantar, nossas regras lógicas de validação serão avaliadas e aplicadas.
Os desenvolvedores podem escrever código como abaixo para determinar proativamente se um objeto Dinner é válido e recuperar uma lista de todas as violações nele sem gerar exceções:
Dinner dinner = dinnerRepository.GetDinner(5);
dinner.Country = "USA";
dinner.ContactPhone = "425-555-BOGUS";
if (!dinner.IsValid) {
var errors = dinner.GetRuleViolations();
// do something to fix the errors
}
Se tentarmos salvar um Jantar em um estado inválido, uma exceção será gerada quando chamarmos o método Save() no DinnerRepository. Isso ocorre porque LINQ to SQL chama automaticamente nosso método parcial Dinner.OnValidate() antes de salvar as alterações do Jantar, e adicionamos código a Dinner.OnValidate() para gerar uma exceção se houver violações de regra no Jantar. Podemos capturar essa exceção e recuperar ativamente uma lista das violações a serem corrigidas:
Dinner dinner = dinnerRepository.GetDinner(5);
try {
dinner.Country = "USA";
dinner.ContactPhone = "425-555-BOGUS";
dinnerRepository.Save();
}
catch {
var errors = dinner.GetRuleViolations();
// do something to fix errors
}
Como nossas regras de validação e de negócios são implementadas em nossa camada de modelo e não dentro da camada de interface do usuário, elas serão aplicadas e usadas em todos os cenários em nosso aplicativo. Posteriormente, podemos alterar ou adicionar regras de negócios e ter todo o código que funciona com nossos objetos Dinner para honrá-las.
Ter a flexibilidade de alterar as regras de negócios em um só lugar, sem ter essas alterações onduladas em todo o aplicativo e na lógica da interface do usuário, é um sinal de um aplicativo bem escrito e um benefício que uma estrutura MVC ajuda a incentivar.
Próxima etapa
Agora temos um modelo que podemos usar para consultar e atualizar nosso banco de dados.
Agora vamos adicionar alguns controladores e exibições ao projeto que podemos usar para criar uma experiência de interface do usuário HTML em torno dele.