Реализация MVP на основе ApplicationController и IoC в WinForms приложении

—

от автора

Добрый день!

В этой статье я расскажу о том как я внедрял паттерн MVP в своём Windows Forms приложении и опишу практические ситуации и особенности использования IoC и ApplicationController. Переход от codebehind к MVP мне позволил:

  • улучшить читатемость за счёт лучшего разделения кода (SRP) — отделить BL от View;
  • выработать методику дальнейшего расширения функциональности приложения;
  • избавиться от singleton, который я использовал для работы с настройками приложения.

О приложении

Приложение для помощи в ведении групп ВК. Позволяет наполнять группу отложенными постами. Основная функциональность на данный момент — загрузка отложенных постов в группу ВК с картинками или видео, хэштэгами, опросами, геопозицией и возможностью конфигурировать время публикации и количество постов по дням. На данный момент у приложения одна форма.

С ростом его функциональности накопилось достаточно много codebehind, который стал смущать тем, что форма стала очень загружена и содержала всё в себе. Планируя дальнейшее развитие проекта я не мог представлять, что дальше можно продолжать в том же духе и вот пришёл момент, когда главной задачей стало не наращивание функционала, а рефакторинг. И я стал искать решение, которое бы помогло оптимизировать и разделить код и в целом улучшить архитектуру приложения, чтобы с ним было приятнее работать.

Исходный код

Решение по рефакторингу

Решение было — внедрение паттерна MVP. За основу я взял статью Особенности реализации MVP для Windows Forms.

В статье разбирается расширенный пример простого приложения с 3мя формами: 2мя основными и 1й модальной. В статье рассматривается очень расширенный подход:

  • помимо ApplicationController и IoC там используется ещё и Adapter, позволяющий использовать разные IoC;
  • 3 вида форм: с параметрами, без параметров и модальная;
  • широко применяется принцип DIP.

В своём проекте я использую только одну форму без аргументов, отказался от адаптера (следуя принципу YAGNI), так как мне достаточно будет IoC Lightinject и в меньшей мере применяю DIP, чтобы упростить проект.

Реализация MVP

MVP (Model-View-Presenter) — паттерн проектирования, придуманный для удобства разделения бизнес-логики от способа её отображения. Подробнее о теории можно прочитать в статье выше. Опишу составные части в моей реализации:

  • Model — это структура данных передаваемая между View и Presenter и содержит данные как для отображения так и для исполнения логики. В моём случае модель — это Settings. При старте проекта Settings загружаются во MainFormView, а при запуске загрузки MainFormView проверяет и передаёт Settigns в Presenter, для того чтобы Presenter выполнил логику на своей стороне.
  • View — это форма, в которой отображаются данные для пользователя. В моем случае это данные модели Settings, а так же View предоставляет события, для того, чтобы Presenter связал View с BL.

MainFormView реализует общий интерфейс IView, характерный для всех View

    public interface IView     {         void Show();          void Close();     } 

а так же частный интерфейс IMainFormView, характерный только для данного View. В начале я думал от него отказаться, но если связать Presenter непосредственно с формой, то при работе с таким View, будет доступен весь набор методов, характерных для Form, что не удобно.

    public interface IMainFormView: IView     {         void LoadSettings(Settings settings);          void UpdateSettings(Settings settings);          void ShowMessage(string message);          void LoadGroups(List<Group> groups);          void EnableVKUploadGroupBox();          bool Check();          event Action Login;          new event Action Close;          event Action VKUpload;     } 

Ещё одно нововведение MVP в том, что у формы заменён метод Show и через конструктор в форму передаётся ApplicationContext, таким образом, чтобы при переключении от формы к форме и закрытии — переназначалась главная форма.

        protected ApplicationContext _context;          public MainForm(ApplicationContext context)         {             _context = context;             InitializeComponent();              dateTimePickerBeginDate.Format = DateTimePickerFormat.Custom;             dateTimePickerBeginDate.CustomFormat = "MM/dd/yyyy hh:mm:ss";              buttonAuth.Click += (sender, args) => Invoke(Login);             this.FormClosing += (sender, args) => Invoke(Close);             buttonLoad.Click += (sender, args) => Invoke(VKUpload);         }          public new void Show()         {             _context.MainForm = this;             Application.Run(_context);         } 

— Presenter — это класс, который инкапсулирует в себе View, Services и бизнес-логику (BL), с помощью которой организует взаимодествие между View и Services. BL реализована в основном в обработчиках событий View. В отличии от ранее используемого CodeBehind, в MVP обработчики событий, выполняющих BL выведены в Presenter, а так же для простоты события у View выведены в виде Action без аргументов. Все необходимые данные для выполнения обработчики получают через модель, полученную из формы через публичный метод.

Presenter содержит метод Run, который вызывается ApplicationController-ом и который запускает форму:

    public interface IPresenter     {         void Run();     } 

ApplicationController — единая точка управления и выполнения всего приложения. Инкапсулирует в себе всю логику: IoC, Presenters, View, Services.

Управление происходит через метод Run, который вызывает соответствующий Presenter. Все Presenter-ы соединены друг с другом через ApplicationController, который Presenter получают в конструкторе. Таким образом Presenter может вызвать другой Presenter вызвав метод Run, который внутри себя обращается к IoC Container для получения нужного Presenter и его запуска.

    public class ApplicationController     {         ServiceContainer _container;          public ApplicationController(ServiceContainer serviceContainer)         {             _container = serviceContainer;             _container.RegisterInstance<ApplicationController>(this);         }          public void Run<TPresenter>() where TPresenter:class, IPresenter         {             var presenter = _container.GetInstance<TPresenter>();             presenter.Run();         }     } 

IoC container — это агрегатор всех «зависимостей» используемых в логике работы приложение. Он содержит в себе:

  • конструкторы View
  • конструкторы Presenters
  • инстансы сервисов
  • контекст приложения
  • ApplicationController

Все зависимости добавляются в контейнер во время запуска, это можно видеть в файле Program.cs:

         static void Main()         {             Application.EnableVisualStyles();             Application.SetCompatibleTextRenderingDefault(false);              ulong appid = ulong.Parse(ConfigurationManager.AppSettings["AppIdForTest"]);             VKGroupHelperWorker vk = new VKGroupHelperWorker(appid);               ServiceContainer container = new ServiceContainer();             container.RegisterInstance<VKGroupHelperWorker>(vk);             container.RegisterInstance<Settings>(Globals.Settings);             container.RegisterInstance<ApplicationContext>(Context);             container.Register<IMainFormView,MainForm>();             container.Register<MainFormPresenter>();              ApplicationController controller = new ApplicationController(container);             controller.Run<MainFormPresenter>();         } 

Для IoC я использовал компонент Lightinject, который перед использованием необходимо установить через NPM.

Таким образом контейнер может содержать как конструкторы объектов, так и сами объекты, как это сделано с Settings и VKGroupHelperWorker (клиент ВК API), образуя множество всех используемых ресурсов приложения. Полезной особенностью контейнера является то, что все эти внедрённые ресурсы, классы могут получить через аргументы конструктора. Например
ApplicationController, IMainFormView, VKGroupHelperWorker — ранее внедрённые зависимости, которые могут быть как конструкторами объектов так и инстансами. В случае если был внедрён инстанс, то все образуемые объекты будут работать с одним и тем же инстансом, что позволяет избавиться от паттерна синглтон, если он использовался.

public MainFormPresenter(ApplicationController applicationController, IMainFormView mainForm, Settings settings, VKGroupHelperWorker vk)         {             _view = mainForm;             _settings = settings;             _vk = vk;              _view.Login += () => Login();             _view.Close += () => Close();             _view.VKUpload += () => VKUpload();         } 

Внедрение MVP мне позволило:

  • частично избавиться от Singleton, который я использовал для работы с настройками приложения;
  • отделить BL от View, тем самым улучшив разделение кода (SRP);
  • выработать подход к дальнейшему расширению приложения, не загромождая View.

Подробнее о том что было сделано можно посмотреть в репозитории проекта.

ссылка на оригинал статьи https://habr.com/ru/post/502358/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *