Паттерны: MVC, MVP и MVVM
- modified:
- reading: 6 minutes
В данной статье я бы хотел рассказать, в чем различие данных паттернов. Начнем с первого главного – Model-View-Controller – это фундаментальный паттерн, который нашел применение во многих технологиях, дал развитие новым технологиям и каждый день облегчает жизнь программистам. Если вы начнете спрашивать архитекторов о том, как реализовать данный паттерн, то, я думаю, вы сможете услышать несколько разных ответов и соответственно несколько разных решений. Вообще, объединяет все эти паттерны – выделение User Interface (UI) от логики программирования, что позволяет дизайнерам делать свою работу, не задумываясь о коде программы. Если вспомнить школьное и студенческое программирование, то всплывает картина огромного количества строчек, написанных в code behind интерфейсов, что не является хорошей практикой. Так же предоставляется возможность выделения модели данных, что дает разработчикам возможность создания модульных тестов над ними.
Model-View-Controller
MVC состоит из трех компонент: View (представление, пользовательский интерфейс), Model (модель, ваша бизнес логика) и Controller (контроллер, содержит логику на изменение модели при определенных действиях пользователя, реализует Use Case). Основная идея этого паттерна в том, что и контроллер и представление зависят от модели, но модель никак не зависит от этих двух компонент. Это как раз и позволяет разрабатывать и тестировать модель, ничего не зная о представлениях и контроллерах. В идеале контроллер так же ничего не должен знать о представлении (хотя на практике это не всегда так), и в идеале для одного представления можно переключать контроллеры, а также один и тот же контроллер можно использовать для разных представлений (так, например, контроллер может зависеть от пользователя, который вошел в систему). Пользователь видит представление, на нем же производит какие-то действия, эти действия представление перенаправляет контроллеру и подписывается на изменение данной модели, контроллер в свою очередь производит определенные действия над моделью данных, представление получает последнее состояние модели и отображает ее пользователю.
Реализация в ASP.NET будет выглядит следующим образом (пример взят с MSDN [5]). Представление – это обычная aspx разметка:
<html>
<body>
<form id="start" method="post" runat="server">
<asp:dropdownlist id="recordingSelect" runat="server" />
<asp:button runat="server" text="Submit" OnClick="SubmitBtn_Click" />
<asp:datagrid id="MyDataGrid" runat="server"
enableviewstate="false" />
</form>
</body>
</html>
Модель – отдельный класс, у которого есть методы получения данных (модель в реализациях часто включает в себя так же и Data Access Level):
public class DatabaseGateway
{
public static DataSet GetRecordings()
{
DataSet ds = ...
return ds;
}
public static DataSet GetTracks(string recordingId)
{
DataSet ds = ...
return ds;
}
}
Пример модели не самый удачный в данном случае, но все-таки не всегда бывает необходимость иметь действительно описанную бизнес модель в классах, иногда хватает и работы с DataSet'ами. Самое интересное, это реализация контроллера, по сути это code behind aspx страницы.
using System;
using System.Data;
using System.Collections;
using System.Web.UI.WebControls;
public class Solution : System.Web.UI.Page
{
private void Page_Load(object sender, System.EventArgs e)
{
if(!IsPostBack)
{
DataSet ds = DatabaseGateway.GetRecordings();
recordingSelect.DataSource = ds;
recordingSelect.DataTextField = "title";
recordingSelect.DataValueField = "id";
recordingSelect.DataBind();
}
}
void SubmitBtn_Click(Object sender, EventArgs e)
{
DataSet ds =
DatabaseGateway.GetTracks(
(string)recordingSelect.SelectedItem.Value);
MyDataGrid.DataSource = ds;
MyDataGrid.DataBind();
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.submit.Click += new System.EventHandler(this.SubmitBtn_Click);
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
Данный подход даст нам возможность с легкостью написать тесты для модели, но не для контроллера (конечно же, все возможно, но придется постараться).
[TestFixture]
public class GatewayFixture
{
[Test]
public void Tracks1234Query()
{
DataSet ds = DatabaseGateway.GetTracks("1234");
Assertion.AssertEquals(10, ds.Tables["Track"].Rows.Count);
}
[Test]
public void Tracks2345Query()
{
DataSet ds = DatabaseGateway.GetTracks("2345");
Assertion.AssertEquals(3, ds.Tables["Track"].Rows.Count);
}
[Test]
public void Recordings()
{
DataSet ds = DatabaseGateway.GetRecordings();
Assertion.AssertEquals(4, ds.Tables["Recording"].Rows.Count);
DataTable recording = ds.Tables["Recording"];
Assertion.AssertEquals(4, recording.Rows.Count);
DataRow firstRow = recording.Rows[0];
string title = (string)firstRow["title"];
Assertion.AssertEquals("Up", title.Trim());
}
}
Model-View-Presenter
Данный паттерн опять-таки состоит из трех компонент. Только посмотрев на приведенную схему становится ясно, что представлению нет надобности подписываться на изменения модели, теперь контроллер, переименованный в Presenter дает знать представлению об изменениях. Данный подход позволяет создавать абстракцию представления. Реализовать данный паттерн можно при помощи вынесения интерфейсов представления. У каждого представления будут интерфейсы с определенными наборами методов и свойств, необходимых презентеру, презентер в свою очередь инициализируется с данным интерфейсом, подписывается на события представления и по необходимости подсовывает данные. Данный подход позволяет разрабатывать приложения с использованием методологии TDD (Test-driven development). Данный паттерн так же можно применить к ASP.NET, давайте посмотрим на предыдущем примере. Оставим представление и модель из предыдущего примера, а code behind страницы немного распилим.
//Abstract View
public interface ISolutionView
{
string SelectedRecord { get; }
DataSet Recordings { set; }
DataSet Tracks { set; }
}
//Presenter
public class SolutionPresenter
{
private ISolutionView _view;
public SolutionPresenter(ISolutionView view)
{
_view = view;
}
public void ShowTracks()
{
DataSet ds = DatabaseGateway.GetTracks(_view.SelectedRecord);
_view.Tracks = ds;
}
public void Initialize()
{
DataSet ds = DatabaseGateway.GetRecordings();
_view.Recordings = ds;
}
}
//View
public class Solution : System.Web.UI.Page, ISolutionView
{
private SolutionPresenter _presenter;
private void Page_Load(object sender, System.EventArgs e)
{
if(!IsPostBack)
{
_presenter.Initialize();
}
}
override protected void OnInit(EventArgs e)
{
base.OnInit(e);
_presenter = new SolutionPresenter(this);
submit.Click += delegate { _presenter.ShowTracks(); };
}
public string SelectedRecord
{
get { return (string)recordingSelect.SelectedItem.Value; }
}
public DataSet Recordings
{
set
{
recordingSelect.DataSource = value;
recordingSelect.DataTextField = "title";
recordingSelect.DataValueField = "id";
recordingSelect.DataBind();
}
}
public DataSet Tracks
{
set
{
MyDataGrid.DataSource = value;
MyDataGrid.DataBind();
}
}
}
Хотя логика и слабая, но все же теперь она вся в презентере, и мы теперь можем тестировать отдельно SolutionPresenter вместе с ISolutionView, используя Mock’и. Более подробный пример присутствует в статье [2].
Model-View-ViewModel
Честно говоря, не знаю, используется ли данный паттерн где-то, кроме WPF и Silverlight. Здесь опять присутствуют три компоненты: модель, представление и третий компонент – дополнительная модель под названием ViewModel. Данный паттерн подходит к таким технологиям, где присутствует двухсторонний биндинг (синхронизация) элементов управления на модель, как в WPF. Отличие от MVP паттерна заключается в том, что свойство SelectedRecord, из предыдущего примера, должно находится не в представлении, а в контроллере (ViewModel), и оно должно синхронизироваться с необходимым полем в представлении. Как раз и выходит, что в этом и есть основная идея WPF. ViewModel – это некоторый суперконвертор, который преобразует данные модели в представление, в нем описываются основные свойства представления, а также логика взаимодействия с моделью. Рекомендую ознакомиться со статьей [3].
Литература
- Model-View-Controller on MSDN
- Jean-Paul Boodhoo - Model View Presenter
- Josh Smith - WPF Apps With The Model-View-ViewModel Design Pattern
- Niraj Bhatt - MVC vs. MVP vs. MVVM
- Implementing Model-View-Controller in ASP.NET
- Model-View-Controller в .Net - Model-View-Presenter и сопутствующие паттерны
P.S. Надеюсь моя статья даст основное представление в понимании данных паттернов. Буду рад комментариям.