Если Вы работаете с C#/.NET – наверняка Вы слышали, пробовали и использовали оба архитектурных паттерна. Но, не будем лукавить, многим практикующим программистам часто не нравится абстрактный язык дизайна. Мы просто берём за основу шаблоны, сделанные коллегами, авторами книг или AI, и добавляем туда новые методы, новые переменные, новые свойства.
Но давайте также согласимся, что иногда весьма полезно уйти на высоту абстрактной модели и внимательно посмотреть на тот уже трудно распознаваемый начальный рисунок проекта.
Это нужно, чтобы понять суть базовых концепций и, часто, постараться «отрефакторить» Ваше создание: говоря на литературном языке остальной части человечества — постараться сделать его более совершенным.
Итак, два архитектурных паттерна, используемых для похожих и разных типов проектов:
Model-View-Controller и Model-View-ViewModel
Оба содержат Model и View.
Model (модель) — там, где лежат данные. Либо, в более сложных вариантах, это слой, разделенный на подслои, но не будем пока усложнять.
Модель содержит классы, реализующие данные и методы, манипулирующие данными непосредственно. Например, EntityFramework. Или Drapper. Или просто классы с прямыми вызовами SqlClient. Либо — просто классы, хранящие данные в памяти.
View – визуализация. То, что видит пользователь. Обычно довольно глупая, но красивая сущность.
Есть немного разницы, а именно:
Model-View-Controller
View отображает данные от Controller. Обычно не делает ничего само, а просто получает данные от Controller (о нём ниже).
Например:
@model List<Product>
<ul>
@foreach (var p in Model)
{
<li>@p.Name - $@p.Price</li>
}
</ul>
показывает, сколько стоит каждый продукт.
Model-View-ViewModel
View связана с ViewModel, причем двухсторонне, используя Data Binding. То есть действия пользователя передаются ниже, в View Model, а ViewModel может обновлять элементы View.
Например:
<Entry Placeholder="Name" Text="{Binding NewName}" />
отображает данные от ViewModel, а
<Button Text="Add" Command="{Binding AddCommand}" />
передает данные от действия пользователя во ViewModel.
И вот теперь о главных «действующих» сущностях: Controller и ViewModel.
Model-View-Controller
Controller – получает ввод пользователя, управлялет моделью и подготавливает данные для View. Потом вызывает полное (view.Render(model)) или частичное (view.UpdateSection(data
)) обновление View.
Пример:
MVC: ручное частичное обновление, контроллер вызывает обновление метки.
// ===== Model =====
class CounterModel {
public int Value { get; private set; }
public void Increment() => Value++;
}
// ===== View =====
class CounterView {
private Label _label;
private Button _button;
public event Action IncrementClicked;
public CounterView(Label label, Button button) {
_label = label;
_button = button;
_button.Click += (s, e) => IncrementClicked?.Invoke();
}
public void UpdateLabel(int value) {
_label.Text = $"Count: {value}";
}
}
// ===== Controller =====
class CounterController {
private CounterModel _model;
private CounterView _view;
public CounterController(CounterModel model, CounterView view) {
_model = model;
_view = view;
_view.IncrementClicked += OnIncrement;
_view.UpdateLabel(_model.Value); // начальное отображение
}
private void OnIncrement() {
_model.Increment();
_view.UpdateLabel(_model.Value); // частичное обновление View
}
}
Model-View-ViewModel
ViewModel — управляет состоянием представления (то есть UI), содержит его данные, передает данные в модель, реализует команды. Каждое изменение в ViewModel сразу видно в View.
Пример:
MVVM: ViewModel только меняет данные, а View сама реагирует через привязки.
// ===== ViewModel =====
class CounterViewModel : INotifyPropertyChanged {
private int _value;
public int Value {
get => _value;
set {
if (_value != value) {
_value = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
}
}
}
public ICommand IncrementCommand { get; }
public CounterViewModel() {
IncrementCommand = new Command(() => Value++);
}
public event PropertyChangedEventHandler PropertyChanged;
}
// ===== View (XAML, например MAUI/WPF) =====
/*
<VerticalStackLayout>
<Label Text="{Binding Value}" />
<Button Text="Increment" Command="{Binding IncrementCommand}" />
</VerticalStackLayout>
*/
// ===== Code-behind =====
public partial class CounterPage : ContentPage {
public CounterPage() {
InitializeComponent();
BindingContext = new CounterViewModel();
}
}
Ну и напоследок: почему? Почему используются две разных схемы.
Ответ прост: первый случай, контроллер, хорош для сильного разделения на «умный сервер» и «быстрое, но мало думающее, отображение». Это именно тот случай, когда сервер и интерфейс далеко друг от друга, а связь между ними слабая — типично для Web-приложений.
Ну а второй случай, View-Model, удобен для прозрачного и простого разделения логики и отображения для каждого из элементов, когда есть «много каналов связи», то есть для desktop или мобильных приложений.