вторник, 28 февраля 2012 г.

MVC, ее двуликая Модель и шаблон Команда

Многие при разработке настольного приложения, имеющего более менее сложный графический интерфейс, используют MVC. Если приложение должно иметь еще и Undo/Redo функционал, то вам прямая дорога к использованию шаблона Команда.
Первая же сложность, с которой сталкивается разработчик в такой ситуации, как правильно использовать обе практики вместе. В интернете не то, чтобы полно, но есть примеры, как органично встроить Команду в MVC.
Но обо всем по порядку.


MVC
Если говорить о .NET, то MVC с использованием WinForms, например, выглядит немного странно. Определение Контроллера звучит так
Обеспечивает связь между пользователем и системой: контролирует ввод данных пользователем и использует модель и представление для реализации необходимой реакции.
Но ведь на самом деле ввод пользователя сильно связан с уровнем Представления. Например, нажатие кнопки на форме чаще всего связывают с определенным методом этой же формы. А далее уже вызываются методы контроллера.
Можно, конечно, держать все обработчики всего Представления в Контроллере, но я бы не советовал так делать. Это просто колоссально повысит связанность Контроллера и Представления.
Если верить классикам, то Контроллер - это вещь вообще всеобъемлющая. Он знает и про Модель и про Представление. На практике же, используя WinForms, мне никогда не приходилось указывать Контроллеру, какое Представление он будет использовать.

Undo/Redo
Для того, чтобы ваше приложение получило возможность отката действия пользователя, используйте шаблон Команда.
Принципиально все ясно. Клиент будет генерировать конкретную команду, передавать эту команду менеджеру команд, менеджер команд вызовет общеизвестный метод для выполнения команды.
Применительно к MVC (WinForms) менеджером команд сделаем Контроллер, клиентом будет Представление, получателем будет Модель.
Должен получиться примерно такой код

  1. public class Model: IModel
  2. {
  3.   public event UpdatedEventHandler Updated;
  4.  
  5.   public void SomeAction()
  6.   {
  7.     //...
  8.   }
  9.  
  10.   // ...
  11. }
  12.  
  13. public class Controller: IController
  14. {
  15.   private List<ICommand> _commands;
  16.  
  17.   public void Execute(ICommand command)
  18.   {
  19.     command.Execute();
  20.     _commands.Add(command);
  21.   }
  22.  
  23.   // ...
  24. }
  25.  
  26. public class DoSomeActionCommand: ICommand
  27. {
  28.   private IModel _model;
  29.  
  30.   public DoSomeActionCommand(IModel model)
  31.   {
  32.     _model = model;
  33.   }
  34.  
  35.   public void Execute()
  36.   {
  37.     _model.SomeAction();
  38.   }
  39.  
  40.   // ...
  41. }
  42.  
  43. public class View: IView
  44. {
  45.   private IController _controller;
  46.   private IModel _model;
  47.  
  48.   protected void Model_Updated(object sender, ModelUpdateEventArgs e)
  49.   {
  50.     // ...
  51.   }
  52.   
  53.   protected void Button_Click(object sender, EventArgs e)
  54.   {
  55.     _controller.Execute(new DoSomeActionCommand(_model));
  56.   }
  57.  
  58.   // ...
  59. }
* This source code was highlighted with Source Code Highlighter.
Таким образом, применением Команды мы порушили MVC. Контроллер неизвестны ни Представление ни Модель, так как Команда подразумевает отделение объекта-получателя от объекта-отправителя. Мы не говорим об этом как о недостатке. Для того, чтобы наше приложение отвечало соответствующим требованиям, мы были вынуждены модифицировать используемые практики. Это нормально.

Модель
Может на этом стоит и остановиться, и лишь дополнять систему нужными командами, но я заметил существенный на мой взгляд недочет.
Если над приложением работает команда, то при добавлении очередного обработчика в Представлении коллега может поддаться искушению и использовать Модель напрямую.

  1. public class View : IView
  2. {
  3.   private IController _controller;
  4.   private IModel _model;
  5.  
  6.   protected void Model_Updated(object sender, ModelUpdateEventArgs e)
  7.   {
  8.     // ...
  9.   }
  10.  
  11.   protected void Button_Click(object sender, EventArgs e)
  12.   {
  13.     _model.SomeAction();
  14.   }
  15.  
  16.   // ...
  17. }
* This source code was highlighted with Source Code Highlighter.
Конечно, можно объяснить коллеге, что все действия над моделью должны производиться через команды. Но на самом деле это не спасет ситуацию в целом. А все ли действия над моделью требуют соответствующих команд? Например, кнопка "Обновить данные" скорее всего нет. В данном случае Представлению просто требуется свежие данные. Тогда вы все-таки напишите так

  1. public class View : IView
  2. {
  3.   private IController _controller;
  4.   private IModel _model;
  5.  
  6.   protected void Model_Updated(object sender, ModelUpdateEventArgs e)
  7.   {
  8.     // ...
  9.   }
  10.  
  11.   protected void Update_Click(object sender, EventArgs e)
  12.   {
  13.     _model.Update();
  14.   }
  15.  
  16.   // ...
  17. }
* This source code was highlighted with Source Code Highlighter.
Ну либо другой вариант - сделать таки команду UpdateModelCommand. Но тогда непонятно, что вы укажете как Undo, и зачем этой команде Redo.
Чтобы внести ясность во всю сложившуюся ситуацию, я предлагаю использовать ISP и разделить интерфейс Модели.

  1. public interface IModelWriteable
  2. {
  3.   void SomeAction();
  4. }
  5.  
  6. public interface IModelUpdateable
  7. {
  8.   event UpdatedEventHandler Updated;
  9.   void Update();
  10. }
  11.  
  12. public interface IModel: IModelUpdateable, IModelWriteable
  13. {
  14.  
  15. }
* This source code was highlighted with Source Code Highlighter.
Теперь можно в Представление отдавать только IModelUpdateable. Команды при этом также изменятся.

  1. public class DoSomeActionCommand : ICommand
  2. {
  3.   private IModel _model;
  4.  
  5.   public DoSomeActionCommand()
  6.   {
  7.  
  8.   }
  9.  
  10.   public void Execute(IModel model)
  11.   {
  12.     _model = model;
  13.     _model.SomeAction();
  14.   }
  15.  
  16.   // ...
  17. }
  18.  
  19. public class View : IView
  20. {
  21.   private IController _controller;
  22.   private IModelUpdateable _model;
  23.  
  24.   protected void Model_Updated(object sender, ModelUpdateEventArgs e)
  25.   {
  26.     // ...
  27.   }
  28.  
  29.   protected void Update_Click(object sender, EventArgs e)
  30.   {
  31.     _model.Update();
  32.   }
  33.  
  34.   protected void Button_Click(object sender, EventArgs e)
  35.   {
  36.     _controller.Execute(new DoSomeActionCommand());
  37.   }
  38.  
  39.   // ...
  40. }
* This source code was highlighted with Source Code Highlighter.
Контроллеру теперь придется хранить ссылку на изменяемую Модель, чтобы выполнять команды

  1. public class Controller : IController
  2. {
  3.   private List<ICommand> _commands;
  4.   private IModelWriteable _model;
  5.  
  6.   public void Execute(ICommand command)
  7.   {
  8.     command.Execute(_model);
  9.     _commands.Add(command);
  10.   }
  11.  
  12.   // ...
  13. }
* This source code was highlighted with Source Code Highlighter.
Чего мы добились такими изменениями в архитектуре.
Контроллер может менять модель. Да, но я предполагаю, что в контроллер не придется часто вносить правки. Все что нужно в Контроллере, можно сделать здесь и сейчас.
Вызов Execute у команд должен быть ранее, чем вызовы Undo/Redo. Это, безусловно, проблема. Это плата за чистоту в коде выше. Частично решить эту проблему можно при помощи шаблонного метода.

  1. public abstract class TemplateCommand: ICommand
  2. {
  3.   protected IModel _model;
  4.   
  5.   public void Execute(IModel model)
  6.   {
  7.     _model = model;
  8.     Do();
  9.   }
  10.  
  11.   public void UnExecute()
  12.   {
  13.     Undo();
  14.   }
  15.  
  16.   public abstract void Do();
  17.   public abstract void Undo();
  18.  
  19.   // ...
  20. }
  21.  
  22. public class DoSomeActionCommand : TemplateCommand
  23. {
  24.   public override void Do()
  25.   {
  26.     _model.SomeAction();
  27.   }
  28.  
  29.   public override void Undo()
  30.   {
  31.     //...
  32.   }
  33.  
  34.   // ...
  35. }
* This source code was highlighted with Source Code Highlighter.
Как думаете, все эти ухищрения оправданы?

4 комментария:

  1. Если логика работы программы выполняется в командах, то зачем тебе контроллер? Если он чисто рулит командами, то это не контроллер, а CommandManager.
    >>Таким образом, применением Команды мы порушили MVC. Контроллер неизвестны ни Представление ни Модель...
    Ну так у тебя от MVC тут мало что осталось. Собственно, MVC это уже не назвать.
    Думаю, что WinForms не предназначена для таких манипуляций. С WPF всё было бы попроще. Хотя, если у тебя задача заимплементить именно возможность Undo, то паттерн команда тут как нельзя кстати.

    ОтветитьУдалить
    Ответы
    1. Бизнес логика в Модели. Иначе ТТУК получится из Контроллера.
      Да, Контроллер в приведенном коде только выполняет роль CommandManager.
      Почему ты решил, что для WinForms MVC не используется?
      С WPF как раз все было бы далеко не так. Там рекомендуют использовать MVVM.

      Удалить
    2. Мурад, спасибо за идею. Однако я с Артемом соглашусь, от MVC ничего не осталось. Если смотреть, что это применяется к WinForms, то там MVC вообще слабо применимо, лучше брать MVP. Все-таки WinForms обычно имеет какую-то основную форму в UI и в нем нет запросов разных пользователей с перенаправлениями и выбором отображения.

      Что касается "команд" и MVC (там где действительно нужен этот шаблон для UI) полезно глянуть статью http://lostechies.com/jimmybogard/2011/06/22/cleaning-up-posts-in-asp-net-mvc

      Удалить
    3. Александр, спасибо за ссылку.
      Ну да, я же сказал, что это уже не совсем MVC. Кроме того Команда уже как бы и не Команда. Но своего мы добились - архитектура поддерживает предъявленные требования.
      Поэтому я и спросил. Стоило ли все это так делать, или есть какой-то другой путь.

      Удалить