Всем привет, меня зовут Аркадий, я студент НИУ ВШЭ и в данной статье мы с вами поговорим о задании PeerReview №6 NotePad++, а именно об архитектурах, которые подойдут для данного задания и некоторых паттернов.

Задание

В данном задании основной задачей является реализация приложения с графическим интерфейсом. Предлагаю далеко не отходить от темы и сразу же посмотреть на то, что такое архитектуры и как они могут нам помочь в данном случае.

Информация из этого поста не покрывает всех существующих архитектур и паттернов, но является вполне достаточной для реализации задания №6

Архитектуры приложений

Под архитектурой мы понимаем архитектурный шаблон проектирования, охватывающий всё приложение или какую-то его часть. Часто такую часть называют модулем. Из этих модулей и строится приложение. Под модулями в данном случае понимаются не C# модули, а архитектурные модули. Им может являться, например, один экран приложения или несколько связанных между собой экранов.

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

Я расскажу лишь о трёх архитектурных решениях, которые считаю самыми подходящими для данного проекта, к тому же они имеют достаточно низкий порок вхождения

1. Apple MVC

Первая архитектура, которую мы разберем – MVC. Мы сразу сталкиваемся с тем, что под одним названием могут скрываться разные подходы. Например я предлагаю рассмотреть архитектуру Apple MVC, которая чуть лучше стандартной Classic MVC

Шаблон проектирования Apple MVC
Шаблон проектирования Apple MVC

Как и во многих других архитектурах, название – это аббревиатура. Каждая буква обозначает один из компонентов. Рассмотрим их по порядку.

  • M – Model. Роль модели могут играть как простые классы или структуры, так и сложные сущности, запрашивающие информацию из базы данных или из сети. Модель не знает о существовании UI (user interface). Можно рассматривать ее как ядро приложения, которое можно переиспользовать с другим интерфейсом. Например, использовать одну и ту же модель для версии программы под разные устройства

  • V – View. Вью – это представление. То, что пользователь видит и с чем взаимодействует. Ответственности вью – это отрисовка себя на экране и реагирование на действия пользователя, но оно не содержит логики и просто передает событие в Controller. В качестве представления в С#-приложениях используются Form и его наследники. Одну и ту же вью можно использовать на разных экранах приложения или даже перенести в другое.

  • C – Controller. Это связующее звено между моделью и представлением. Его ответственность заключается в том, чтобы принимать действия пользователя от View, обрабатывать их и в случае необходимости обновлять модель, а при изменении модели обновлять вью. Т.к. контроллеры содержат логику, специфичную для приложения, то их сложно переиспользовать.

Один контроллер не всегда соответствует одному экрану. Их можно вкладывать друг в друга, разбив таким образом большой контроллер на несколько маленьких. Например, можно реализовать TabBarController который будет являться контейнером для нескольких контроллеров. При нажатии на вкладки он просто подменяет один на другой.

MVC – хороший выбор для данного приложения. Эта архитектура имеет очень низкий порог вхождения (количество знаний и сил для её реализации). Однако есть одна проблема в реализации MVC от Apple. Они наделили ViewController обязанностью следить за жизненным циклом View. Из-за этого они очень сильно связаны. В итоге контроллер содержит не только обработку модели, но и взаимодействие с вью. 

Для решения этой проблемы во всех остальных архитектурах ViewController считается частью вью. В нем остается обработка жестов, анимации и прочее взаимодействие с View. А всю остальную логику переносят в другое место. Таким образом мы избегаем так называемого «Massive View Controller»

Еще одна важная проблема, которая решается таким образом – сложность тестирования. Если вы хотите проверить какую-то логику, находящуюся в ViewController, то вам придется иметь дело с жизненным циклом вью.

2. MVP

Очевидным развитием MVC является MVP. Раз ViewController – это часть вью, то вместо него нужно добавить какую-то другую сущность, никак не связанную с View, в результате получаем Presenter.

Шаблон проектирования MVP
Шаблон проектирования MVP

Получившаяся схема очень похожа на MVC от Apple. Разница в том, что вся логика, не связанная с представлением, перемещается из ViewController в Presenter.

Обратите также внимание на то, что вью, как и в MVC, пассивная. Presenter передает в нее данные, но сама она их не запрашивает, хотя обращение к презентеру не запрещено. Оно может происходить, например, для вызова обработчика нажатия на кнопку.

3. MVVM

MVVM очень похоже на MVP. Разница в том, что вместо презентера в нем используется ViewModel. Вью модель хранит в себе состояние представления. Например, текст на экране или значение поля для ввода. Но в отличие от MVP, вью сама берет данные для отображения. Это можно реализовать разными способами, но часто используется data binding (паттерн в следующей главе). Это подход, при котором один объект следит за состоянием другого. Т.е. вью будет следить за своим значением, хранящимся во ViewModel, и изменять свое отображение при его изменении.

Шаблон проектирования MVVM
Шаблон проектирования MVVM

MVVM и MVP решают проблему большого контроллера и облегчают тестирование. Однако порог вхождения в проекты,

написанные на них, немного выше. Особенно это касается MVVM с биндингами, т.к. часто в таких проектах связывание данных используется не только между View и ViewModel. Такой код сложнее понять новичку, а еще сложнее отлаживать.


В данном проекте можно использовать эти или любые другие архитекрутрные решения. Лично я бы советовал использовать MVVM, так как он упростит реализацию настроек и вкладок.

Паттерны

Существует великое множество паттернов проектирования. Другое их название – шаблоны проектирования.

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

Мы рассмотрим самые распространенные паттерны, которые можно использовать в данном проекте. Давайте перечислим их:

  1. Синглтон (Singleton)

  2. Target-Action

  3. Наблюдатель (Observer)

  4. Команда (Command)

Я намеренно не включил в данные список Делегирование, т.к. считаю, что в данном проекте его использование будет излишним, в силу простоты программы.

В нескольких примерах кода буду использовать Swift, т.к. во-первых я его учу и надо практиковаться, а во-вторых **** я ваш C#, лучше **** *** *** ***** *** **** , чем буду на нём писать не на оценку. Спасибо за понимание.

1. Синглтон (Singleton)

Синглтон-паттерн широко используется при разработке программного обеспечения. Он гарантирует, что будет создан только один экземпляр класса и обеспечивает глобальную точку доступа для ресурсов, которые он предоставляет.

Применяется, когда нужно создать один и только один объект какого-либо класса на весь жизненный цикл приложения и доступ к которому необходимо получать из разных частей кода.

Примером применения этого паттерна является создания класса настроек приложения. Очевидно, что настройки приложения являются единственными в своём роде на всё приложение.

Другой пример применения данного паттерна – сервис GPS-навигации. Допустим, нам в приложении нужно отслеживать местоположение пользователя. В телефоне установлен единственный модуль GPS, и определять текущие координаты умеет только он. Ресурсы, которые предоставляет этот модуль, можно использовать совместно. В этом случае можно создать один глобальный синглтон-объект. И этот объект уже внутри себя будет использовать экземпляр класса LocationManager, предоставляющий единственную точку входа ко всем сервисам GPS-модуля.

class NetworkManager {
  private(set) static var sharedInstance: NetworkManager = {
    let manager = NetworkManager()
    // additional setup code 
     return manager
  }() 
  private init () {
  }
  func sendRequest() {
    print("sending request")
  }
}
// main program
let networkManager = NetworkManager.sharedInstance
networkManager.sendRequest() // sending request

2. Target-Action

Следующий паттерн, который мы рассмотрим, называется Target-Action. Обычно пользовательский интерфейс приложения состоит из нескольких графических объектов, и зачастую в роли таких объектов используются т.н. элементы управления. Это могут быть кнопки, переключатели, поля для ввода текста. Роль элемента управления в пользовательском интерфейсе довольно проста: он воспринимает намерение пользователя сделать какое-либо действие и дает указание другому объекту обработать этот запрос. Для связи между элементом управления и объектом, который может обработать запрос, и используется паттерн Target-Action. Target-Action – паттерн, в котором объект содержит информацию, необходимую для вызова метода у другого объекта при возникновении некоторого события, например, нажатия кнопки. Информация состоит из двух типов данных: селектора, который идентифицирует вызываемый метод (action), и объекта, чей метод вызывается (target). Остановимся чуть подробнее на том, что такое селекторы.

Фактически, селектор – это имя метода объекта или структуры, или уникальный идентификатор, который заменяет имя метода при компиляции исходного кода. Селектор сам по себе ничего не делает. Он просто идентифицирует какой-то метод.

Данный паттерн встроен в язык WindowsForm проект, так что в самостоятельной реализации не нуждается

3. Наблюдатель

В паттерне «Наблюдатель» один объект уведомляет другие объекты об изменениях своего состояния. Объектам, связанным таким образом, не нужно знать друг о друге – это и есть слабо связанный (а значит, гибкий) код. Этот паттерн чаще всего используют, когда надо уведомить «наблюдателя» об изменении свойств нашего объекта или о наступлении каких-либо событий в этом объекте. Обычно наблюдатель «регистрирует» свой интерес о состоянии другого объекта. 

Когда состояние меняется, все объекты-наблюдатели будут уведомлены об изменении.

Уведомление – это сообщение, отправленное наблюдателям, чтобы оповестить их о каком-либо событии. Уведомления основаны на модели «подписка—публикация». Согласно ей, объект «издатель» (publisher) рассылает сообщения подписчикам (subscribers). Издатель ничего не должен знать о подписчиках.

interface IObservable {
  void AddObserver(IObserver o);
  void RemoveObserver(IObserver o);
  void NotifyObservers();
 } 

class ConcreteObservable : IObservable {
  private List<IObserver> observers;
  public ConcreteObservable(){
    observers = new List<IObserver>();
  }
  public void AddObserver(IObserver o){
    observers.Add(o);
  }
  public void RemoveObserver(IObserver o){
    observers.Remove(o);
  }
  public void NotifyObservers(){
    foreach (IObserver observer in observers) 
      observer.Update();
  }
}
interface IObserver {
  void Update();
 }
 class ConcreteObserver :IObserver {
   public void Update() { 
     // Some Action
   } 
 }

В данном примере ( к сожалению написанном на c#, т.к. реализация с другими языками отличается существенно ):

  • IObservable: представляет наблюдаемый объект. Определяет три метода: AddObserver() (для добавления подписчика(наблюдателя))RemoveObserver() (удаление набюдателя) и NotifyObservers()(уведомление наблюдателей)

  • ConcreteObservable: конкретная реализация интерфейса IObservable. Определяет коллекцию объектов наблюдателей.

  • IObserver: представляет наблюдателя, который подписывается на все уведомления наблюдаемого объекта. Определяет метод Update(), который вызывается наблюдаемым объектом для уведомления наблюдателя.

  • ConcreteObserver: конкретная реализация интерфейса IObserver

При этом наблюдаемому объекту не надо ничего знать о наблюдателе кроме того, что тот реализует метод Update(). С помощью отношения агрегации реализуется слабосвязанность обоих компонентов. Изменения в наблюдаемом объекте не виляют на наблюдателя и наоборот.

В определенный момент наблюдатель может прекратить наблюдение. И после этого оба объекта - наблюдатель и наблюдаемый могут продолжать существовать в системе независимо друг от друга.

Команда

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

т.к. В WindowsForms уже есть реализация обработчиков, то этот паттерн нам особо не понадобится, но я всё же оставлю ссылку на него

https://refactoring.guru/ru/design-patterns/command


подробная информация о паттернах :

https://refactoring.guru/ru/design-patterns/

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

На этом я заканчиваю эту статью. Если она была вам полезна и поможет чем-нибудь, я буду очень рад.

Не игнорируйте паттерны и архитектуры, они вас спасут!