Паттерны: MVC, MVP и MVVM

    • ASP.NET
    • WPF
    • TDD
    • Mock
    • MVC
    • MVP
    • MVVM
    • Patterns
  • modified:
  • reading: 6 minutes

В данной статье я бы хотел рассказать, в чем различие данных паттернов. Начнем с первого главного – Model-View-Controller – это фундаментальный паттерн, который нашел применение во многих технологиях, дал развитие новым технологиям и каждый день облегчает жизнь программистам. Если вы начнете спрашивать архитекторов о том, как реализовать данный паттерн, то, я думаю, вы сможете услышать несколько разных ответов и соответственно несколько разных решений. Вообще, объединяет все эти паттерны – выделение User Interface (UI) от логики программирования, что позволяет дизайнерам делать свою работу, не задумываясь о коде программы. Если вспомнить школьное и студенческое программирование, то всплывает картина огромного количества строчек, написанных в code behind интерфейсов, что не является хорошей практикой. Так же предоставляется возможность выделения модели данных, что дает разработчикам возможность создания модульных тестов над ними.

Model-View-Controller

MVC Schema 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

MVP Schema Данный паттерн опять-таки состоит из трех компонент. Только посмотрев на приведенную схему становится ясно, что представлению нет надобности подписываться на изменения модели, теперь контроллер, переименованный в 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].

Литература

  1. Model-View-Controller on MSDN
  2. Jean-Paul Boodhoo - Model View Presenter
  3. Josh Smith - WPF Apps With The Model-View-ViewModel Design Pattern
  4. Niraj Bhatt - MVC vs. MVP vs. MVVM
  5. Implementing Model-View-Controller in ASP.NET
  6. Model-View-Controller в .Net - Model-View-Presenter и сопутствующие паттерны

P.S. Надеюсь моя статья даст основное представление в понимании данных паттернов. Буду рад комментариям.

See Also