Хабрахабр

Верхнеуровневая архитектура фронтенда. Лекция Яндекса

Выбор подходящей архитектуры — ключевая часть построения фронтенда сервиса. Разработчик Анна Карпелевич рассказала студентам Школы разработки интерфейсов, что такое архитектура, какие функции она выполняет и какие проблемы решает. Из лекции можно узнать о наиболее популярных архитектурных подходах во фронтенде: Model-View-Controller и Flux.

— Добрый вечер. Меня зовут Аня Карпелевич. Мы сегодня с вами будем говорить про архитектуру фронтенда верхнего уровня.

Мы делаем интерфейсы для рекламодателей. Я работаю в Директе. Это очень сложная, интересная система, в ней много взаимосвязанных компонент, они прорастают друг в друга, у них есть общие и свои функциональности. Они подают объявления, настраивают их. Все это приходится очень тщательно контролировать. «Брюки превращаются в элегантные шорты». Это одна из причин, почему сегодня эту лекцию читаю я. И архитектура в наших приложениях очень сложная. Я очень люблю эту тему.

Дело в том, что ответа на этот вопрос, наверно, нет. Что такое архитектура? Это очень спорная тема. Или есть, но у каждого свой. И много из того, что сегодня я буду рассказывать, — мое мнение. Она вызывает массу споров, массу холиваров. И каждый, когда он пишет архитектуру своего приложения, решает для себя, как и что делать. Частично его поддерживает моя рабочая группа, частично — не очень.

И поэтому сегодняшняя наша презентация тоже начнется с творчества. Именно поэтому архитектура — одно из самых, наверно, творческих мест в работе программиста.

Я буду очень рада, если кто-нибудь узнает здание, которое на ней изображено. Давайте посмотрим на левую картинку. Обратите внимание на башенки, ради них эту церковь сюда и поставили. Это церковь Сен-Сюльпис в Париже. Довольно сильно разные, и тому есть интересная причина. Надеюсь, видно, что они разные. Потом левую башню снесли и перестроили во время Франко-прусской войны. Между ними 130 лет разницы.

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

А вот правая картинка более интересная. Пример с созданием для темы про архитектуру достаточно очевиден. «Architektur ist gefrorene Musik», — сказал в XVIII веке Иоганн Вольфганг Гете. «Архитектура — это онемевшая музыка». И он гарантированно ничего не знал про архитектуру приложений. Гете, скорее всего, ничего не знал про архитектуру зданий, он был поэтом. Но высказал очень ценную и интересную мысль.

Это не что-то статичное. Музыка существует в динамике. И точно так же приложение является процессом. Это процесс. И у него, наконец, есть момент завершения. У него есть момент запуска, у него есть момент развития, когда мы с ним что-то делаем, работаем. В любой момент наше приложение, как музыкальная тема, должно быть четким, ясным, понятным, предсказуемым и т. Архитектура приложения — его срез в любой конкретный момент времени. Иначе все развалится. д.

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

Что такое архитектура и зачем она нужна?

Кода так много, что в нем можно потеряться. Во-первых, у нас бывает организация большого объема кодов, то, с чем мы в Директе — и не только в Директе — сталкиваемся постоянно. Мы не хотим теряться в коде.

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

Достаточно очевидно, что если у нас есть приложение, то его нужно как-то поддерживать, и желательно, чтобы на это не тратились все ресурсы команды. Третье — поддержка.

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

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

Чем понятнее, предсказуемей наша архитектура, тем легче искать ошибки, тем меньше багов. И, наконец, ошибки.

Это все можно обозвать — проблемы сложной системы. Как это все можно обозвать? Приложение — это сложная система, архитектура помогает нам решать проблему.

Вот от меня справа картинка с лапшой, и это то, что бывает, если за архитектурой не следить, если ее не выстраивать, не продумывать и не проектировать. Короче, как-то так. Это еще не Сен-Сюльпис, но хотя бы детский конструктор, он крепко стоит и не разваливается. А вторая картинка — это то, что бывает, если архитектуру все-таки хоть как-то продумать. В конструктор мы сегодня тоже будем много играться.

Архитектура — это способ решения проблем сложной системы путем абстракции реализации от интерфейса и разграничения полномочий между блоками кода. Формально обо всем этом. Дальше вот эту длинную фразу мы будем подробно разбирать.

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

Эту, надо сказать, в свое время очень неординарную мысль высказал в 1968 году Эдсгер Дийкстра, замечательный программист. История вопроса, когда, вообще, возникла эта мысль, что нужно делать архитектуру. Но у него масса, на самом деле, прорывных для своего времени идей. Он больше, наверно, известен как автор алгоритма Дийкстра, поиск самого короткого пути в графе. Звучит оно как «operator go to consider is harmful», в переводе «Оператор go to оператор безусловного перехода, это плохо». И одна из них — это статья, я вам дам потом ссылочку на материалы, можете прочитать, там всего два листа, коротенькое эссе. Это была первая мысль о том, что, давайте официально, скажем так, высказанное, что надо писать архитектуру, а не лапшу.

Первая книга подробная об архитектуре приложений в целом была написана в 1996 году Мэри Шоу и Дэвид Гэрлан. В 70-х годах эта идея развивалась уже Дийкстра в соавторстве с Парнасом, и сами по себе, по отдельности. И поскольку архитектура — процесс творческий, какие-то конкретные книги про как писать архитектуру, вы не найдете. После этого, на самом деле, подробных таких книг об архитектуре программного обеспечения не писалось именно из-за области применения, что в каждой сфере знаний есть свои архитектурные подходы, где-то одно, где-то более популярно другое, что-то, вообще, не применимо в каких-то местах. Может быть, после 1996 года ничего такого особо подробного на эту тему и не было.

Во-первых, и самое главное, что от него требуется, на самом деле, это расширяемость, потому что если ваш проект не расширяется, он мертв. Какие требования предъявляются к архитектуре проекта сейчас.

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

Про это мы тоже сегодня поговорим поподробнее, зачем это надо. Разделение полномочий между модулями кода. И эта функция должна быть вынесена в заголовок этого метода, класса, чем бы это ни было, модуля. Идея такова: каждый модуль, каждый блок, каждый кусочек кода должен выполнять одно свое конкретное действие, нести ровно одну функцию. Один модуль — одна функция.

Тут много вещей, которые хотелось бы сделать, − и безотказность, и обратная совместимость. И, наконец, качество приложений. Где-то нужна обратная совместимость так, чтобы ни в коем случае ничего не отъезжало. В реальности опять же, выбирается под задачу. Где-то нужно, чтобы это было безотказно, если это спутник или еще что-то. Где-то нужна надежность, чтобы, не дай Бог, пароли, пин-коды карточек или CVV не утекли никуда. Чем больше вы захотите поддержать, тем больше сложностей в архитектуре вы, скорее всего, встретите. В общем, выберете любые несколько.

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

Что такое класс? Самая простая вещь — это класс. Вот, например, класс Змея — class Snake. Это шаблон, это образец. Мы определили конструктор, в котором мы ставим эти самые головы, хвосты и длину в попугаях. У нее мы определили три приватных поля, то есть поле, которое недоступно никому, кроме методов самого класса, − количество голов, количество хвостов и длина в попугаях. Все просто. Получили класс Змея.

Объект. Едем дальше. Причем, опять же в классическом ООП подразумевается, что объект это объект класса. А объект — это экземпляр конкретной структуры. То есть мы сможем создать объект, литерал, который не будет объектом класса. В современном мире в JavaScript, который не всегда был ООП-языком, да и сейчас не всегда и не везде ООП, мы знаем, что могут быть абстрактные объекты. Вот у нас двухвостая змея длиной в 38 попугаев, − удав. Но здесь пример, как мы создаем объект класса Змея.

Модуль — это семантическая единица. Модуль. Это может быть набор классов, набор объектов, набор методов, не объединенных в классы. Это не всегда класс. Но, в принципе, модуль — это и папка, в которой они лежат, например, − файл и тесты к этому модулю, тоже модуль. Обычно, можно считать, что модуль это то, что вы записали в один файл. В данном случае модуль про то, как мы едим змей. Здесь важно то, что модуль — это то, что вы назвали модулем, то, что вы считаете единицей семантики. Не знаю, зачем мы едим змей, но мы это умеем делать, потому что мы так написали этот модуль. Результатом работы этого модуля является последний метод, eatSnake, как мы съели змей.

Интерфейс класса. Это было тривиально, дальше начнется несколько более интересная вещь. Вот этот класс реализует интерфейс getSnakeLength. Интерфейс класса — это, проще говоря, его публичные методы, то, чем он торчит наружу, то, что мы можем получить от объекта этого класса от объекта извне. Обратите внимание, что доступа извне к приватным полям нет. Он может нам вернуть длину змеи. Доступ извне есть только к публичному методу getSnakeLength.

Мы долго спорили, как эту штуку назвать, потому что термин «абстрактный интерфейс» я придумала, когда создавала эту лекцию. А вот дальше очень интересная вещь. Тем не менее, многие языки программирования позволяют создавать абстрактные интерфейсы, и обзывают их, как только не абстрактными классами, и абстрактными интерфейсами тоже, просто интерфейсами. И, честно говоря, я нигде не видела нормального определения этого подхода и метода. Идея такова, что абстрактный интерфейс — это набор методов, которые что-то делают. Получается омоним с интерфейсом класса. Она может просто отдавать свою длину. Когда мы создаем класс, мы идем от вопроса «что это?» Это змея и она что-то умеет делать, или не умеет.

И это оказывается очень мощным способом расширения классов. А когда мы создаем интерфейс, мы идем от того, что он делает, что он должен уметь делать. Например, фреймворк I-BEM такую штуку умеет, встроена во фреймворк такая история с абстрактными интерфейсами. Мы можем приписывать классам какие-то возможности, расширяя его при помощи интерфейсов. Многие фреймворки, к сожалению, не умеют, а штука мощная.

И определение у него — абстрактный пустой метод getNoise. Вот в качестве примера мы создали интерфейс audiable, что-то, что умеет звучать. Вдохновение к этому сету примеров мне дала замечательная книжка Эрика Фримена и компания, известной, как «Банда четырех» «Паттерны проектирования». Мы расширили нашу змею классом audiable, реализовали у нее метод getNoise, и наша змея зашипела. И расширение уток при помощи интерфейсов породило тот самый мем про то, что если оно летает про утку и крякает, как утка, значит, оно утка. У них там летали вот так вот утки. То, что шипит — это змея, и нам, на самом деле, все равно, что там внутри. Вот у нас то, что умеет звучать, оно звучащее.

Сейчас мы попробуем эти примеры посмотреть немножко более конкретно.

А нужны они были вот для этого большого слайда. Но сначала поговорим о том, зачем эти примеры были нужны. Это, можно сказать, мантра. Он настолько важный, то, что здесь написано, что я даже вынесла это в желтый титульник. High cohesiti, low coupling — сильное сцепление, слабая связность. Это очень важный принцип, который вам нужно всегда про него думать, когда вы проектируете архитектуру. Есть некая проблема с тем, что слово cohesiti и слово coupling на русский и так, и так переводятся «связность», специально для этого принципа придумали слово сцепление.

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

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

Каждый блок решает только одну задачу. Специализация. У нас каждый блок, или набор блоков. Вот у нас хорошая иллюстрация — детский конструктор. И если нам нужно построить дом, мы возьмем длинные брусочки. Они все своей формы, своего размера. У каждого брусочка есть своя функция. Если нам нужно построить шарик, мы возьмем короткие брусочки. Из вот таких загогулин ничего не построится, или строится только то, что описано в инструкции. И те, кто играли в конструкторы, знают: чем проще форма кусочков, тем больше из него можно построить. А кому оно надо?

Это про то, что абстракция интерфейса от реализации. То же самое, абстракция. Наоборот — бывает. Идея в том, что интерфейс внешний, то, как это наш класс, наш блок торчит наружу, то, как он взаимодействует с другими блоками, не должно влиять на его внутреннюю реализацию. В хорошей архитектуре. В другую сторону — никогда. Мы выбираем отдельно форму блока и уже на него пупырышки наклеиваем. Здесь в качестве примера — формирование вот этих пупырышков не влияет на форму самого блочка.

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

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

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

Потому что у вас там может быть рядом еще какой-нибудь зебро-бизоно-крокодил. Затем, что когда менеджер снова к вам прибежит и скажет, что срочно мы расширяемся на Южную Америку, а там аллигаторы, нам нужно поддержать неправильную форму челюсти, или что там, у крокодила, четвертый зуб не такой, вы не будете шарить по всему проекту, где же у вас скопированы головы крокодилов. И все. Вы просто возьмете свой класс голова крокодила, сделаете у него расширение из серии голова аллигатора, передадите ему параметры, он будет сам определять, какие зубы ему рисовать. В одном месте, а не во всех местах, где это использовано.

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

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

У него есть имя и возраст. Итак, у нас есть класс User. У нас есть User с фамилией, прошу прощения за разъехавшиеся шрифты. Все хорошо. User с фамилией, у него есть имя, возраст и фамилия.

Мы передаем ему User. И у нас есть метод printLabel. Если User класса User с фамилией, то имя, фамилия и возраст. Дальше смотрим, если у нас User класса User, мы рисуем имя и возраст. Давайте все-таки попробуем посмотреть, что здесь плохо.

Много разного дублирования кода, хорошо. Дублирование кода, еще? Тут два дублирования кода, − одно про то, что мы дублируем UserWithSurname, второе, что мы дублируемся в методе printLabel. Да, хорошо. Правильно, да, это все про то, что у нас много дублирования кода, потенциально еще больше. Еще что есть? Наследование тоже здесь есть, и это тоже один из вариантов. Что-нибудь еще тут есть? Мы еще про две вещи говорили. Тут есть две проблемы, − нет переиспользования, нет специализации. Еще? PrintLabel лезет в приватные методы. Да, все так. Четвертого чего здесь не хватает?

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

И у него определим один-единственный абстрактный метод getText. Мы создадим интерфейс printLabel, это не потому, что iPrintLabel это не потому, что iPhone, а потому что интерфейс. У него появятся, действительно, приватные поля имя и возраст, и один-единственный публичный метод, тот самый getText из iPrintLabel, в котором мы уже честно обратимся из класса к его приватным полям, это разрешено и даже поощряется. Создадим класс User, который имплементирует iPrintLabel. А вот printLabel станет очень простым. UserWithSurname, действительно унаследуем от класса User, и нам нужно будет здесь только доопределить Surname и переопределить getText. Он станет принимать iPrintLabel и просто выводить getText.

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

С теорией всего, потому что то, что мы сейчас описывали, оно верно не только для front-end, для всего, вообще. На этом месте мы заканчиваем с теорией. И переходим к архитектурным подходам и отдельно к архитектурным подходам, которые применяются во front-end и полезны, используются, встречаются.

Есть сервер. Как устроена среднестатистическое веб-приложение? Наружу от него торчит какой-то API, например, это может быть Rest-ip, может быть не Rest. Внутри сервера реализована какая-то архитектура back-end. Потому что у нас могут быть чисто серверные приложения, чисто клиентские приложения, какой-нибудь PowerPoint, с которого это все играется. Все вместе — клиент и сервер, − это тоже реализация архитектурного подхода клиент-серверного. Это же чисто клиентское приложение сервера.

Front-end состоит из каких-то крупных блоков. Дальше мы подробнее посмотрим на front-end. Внутри модуль, он тоже как-то реализован. Каждый блок каким-то образом реализован, и эта реализация позволяет связывать крупные блоки между собой. У этого метода тоже есть архитектура. Внутри модуля метод. На каждом уровне она существует, хоть на уровне объявления переменной, это тоже может быть кусочком архитектуры. И поэтому архитектура это, вообще-то, иерархия. Маленьким.

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

> Клиент-сервер (Client-server)
> Компонентная (Component-based)
> Событийная (Event-driven)
> REST (Representational state transfer)
> Модель-представление-*(MVC, MVP, MVVM)
> Однонаправленные потоки данных (Flux)

Вот такие есть архитектурные подходы. Некоторые из них мы сегодня упоминали. Клиент-серверная архитектура; компонентная архитектура, одна из ее вариаций вам знакома по React, надеюсь, знакома. Событийная, которая, как ни странно, тоже всем знакома, на ней основаны практически все операционные системы для персональных компьютеров. REST, то, что мы любим в сервере, и две последние, с которыми мы сегодня будем знакомиться подробно, самые фронт-эндовые, то, с чем мы работаем, это модель-представление* и однонаправленные потоки данных.

Почему звездочка? Начнем с MV*. Был когда-то давно, еще в 80-х годах придуман замечательный архитектурный подход MBC. История, что называется, полная боли и гнева. Подход оказался очень удобным. M — Model, V — View, C — Controller. Но когда начали развиваться веб-технологии, когда это все начали использовать, оказалось, что иногда нужно, вот модель MV хорошо, а Controller реализуем не так. Придумали его вообще для консольных приложений. Потому что, если модель MV есть, то третья — это Controller, неважно, что мы там, на самом деле, запихнули. В результате оказалось столько различных вариаций реализации Model-View-что-нибудь, что сначала возникла путаница из-за того, что все это называли MVC.

Примерно сейчас, не больше года назад, начали активно разделять эту терминологию, и делать для каждой реализации этого подхода свое название. Потом оказалось, что люди путаются и подразумевают под MVC совершенно разные вещи. Еще я видела в интернете термин MVW, где W — Whatever. Так или иначе, появилась вот эта MV*. Ну а мы переходим, собственно, к MVC-технологиям.

Идея в том, что у нас есть модель, которая хранит данные. Как они устроены? Есть какой-то View, который показывает эти данные пользователю. Их, как правило, много. И некий третий компонент, который между ними посредник, провязывает данные и отображение. Их тоже, как правило, много. Вот пользователь в правом верхнем углу со всем этим работает.

Но именно в таком виде он существует в некоторых фреймворках до сих пор. MVC, то, с чего все началось, это далекий 1980 год, Smalltalk. В чем идея? Не в некоторых, довольно во многих. Он вводит данные в какие-то поля во вьюшке, нажимает кнопку отправки и данные попадают на контроллер. Пользователь работает напрямую с вьюшкой и контроллером. Честная такая отправка формы по кнопке submit, всем знакомая давно, я надеюсь. Это отправка формы.

Желтая стрелочка от пользователя к контроллеру — это пользователь передал данные на контроллер по кнопке submit. Смотрим. Контроллер смотрит на эти данные. Зеленая стрелочка, − туда же перешло управление. Контроллер сам выбирает, на какую модель отправить. Возможно, он их как-то обрабатывает, здесь уже тонкости реализации, и отправляет их на нужную модель. Отправляет зеленой стрелочкой управления, отправляет желтой стрелочкой данные.

Возможно, она их валидирует. Модель тоже обрабатывает данные. Короче, модель знает, что с ними сделать. Возможно, она их кладет в базу. Например, мы можем сообщить пользователю, залогинился он или нет, а модель проверяла пароль с логином. Как правило, в результате получаются новые данные. А данные идут непосредственно во View. После чего, модель передает управление на контроллер опять, чтобы контроллер выбрал, какую вьюшку отобразить. Как можно такое сделать, вообще, как может модель данные во вьюшку отправить?

Если контроллер и модель находятся в back-end, а шаблонизация View серверная. Очень просто. NET, Django, в общем, везде, где вы пишете серверную шаблонизацию, а на клиент вам приходит собранная шлеймелька, и также уходят данные обратно, с большой вероятностью, это вот этот подход. Так устроены фреймворки Ruby on Rails, ASP. В single page application такой штуки не построить. В чем у нас здесь проблемы. Во-вторых, совершенно не понятно, куда здесь пихать клиентскую валидацию, и, вообще, клиентский JavaScript, AJAX и все вот это вот? У нас постоянно данные на сервер пришли, на сервер ушли, страница перезагружается. Оно просто не работает в этом подходе, или работает так, чтобы лучше не работало. Потому что, если мы хотим что-то быстренькое, − некуда.

Вопрос был такой: где хранить бизнес-логику — на модели или в контроллере? Последняя строчка здесь, это такая интересная история, корнями уходящая, кажется, в 2008 год. Контроллер сам отвалидирует, перепроверить, если что, и ошибку отправит». Были те, кто говорил: «Храним бизнес-логику в контроллере, потому что это же удобно, на модель отправляются сразу чистые данные. Они, действительно, получались огромными. Были те, кто говорил, что «В результате получается fat stupid ugly controllers, толстые тупые, уродливые контроллеры». А то в первом варианте модель, вообще, получается, просто API к базе данных. И говорили о том, что бизнес-логика должна находиться в модели, а контроллер должен быть тоненьким, легеньким, данные передал, модель сама обработала.

На самом деле, надо смотреть их задачи. Как, на мой взгляд, на самом деле? Если у вас вьюшки и модели могут пересекаться, и одна вьюшка зависит от многих моделей, модель работает со многими вьюшками, вам удобно иметь много тонких контроллеров и множить их в любой прогрессии, вам все равно, сколько их, они все равно маленькие. Если у вас связь между вьюшкой и моделью всегда один к одному, один View — одна модель, то вам удобно делать бизнес-логику в контроллерах, и сделать простую чистую модель, которая, действительно, будет API к базе данных.

То есть вот эти fat stupid ugly controllers вроде бы уже не так активно используются. Надо сказать, что в мире, кажется, победила вторая точка зрения, с бизнес-логикой в моделях. NET, фреймворку еще в 2013 году предлагалось бизнес-логику в контроллерах. Сигналы, можно смотреть то, что в документации к ASP. Очень интересный был момент, когда это поменялось. А в последних версиях в 2014-м — в моделях.

Мы их уже проговорили, но проговорим. Какие у MVC есть проблемы. Придумали решение. Тестировать как не понятно, как реализовывать клиентскую валидацию — можно, но сложно, AJAX прикручивается сбоку, надо чего-то делать. Например, Backbone MVP фреймворк. Решение назвали MVP, и, да, вы можете встретить MVP в фреймворке с текстом, что они MVC. Про него долго в документации в том же 2011-2012-2013 году было написано, что это MVC фреймворк.

Его схема уже гораздо более простая. Model-View-Presenter. Они между собой взаимодействуют. Есть модели. И обратно. Отдают данные на Presenter, Presenter передает их во вьюшку, показывает пользователю. То есть здесь уже все гораздо боле просто и линейно, но без серверной шаблонизации здесь уже будут сложности. Пользователь вбивает что-то во вьюшку, нажимает кнопку, Presenter это смотрит, AJAX отправляет на модель или кладет в модель, а модель AJAX отправляет на сервер. Если вы хотите серверную, вот такая система будет сложновата.

Посмотрим на первую картинку, где мы попытаемся реализовать очень простую вещь — отправку данных из input в модель. Давайте сравним. Мы вбили: «меня зовут Вася», нажали о’кей. Мы что-то ввели, нажали кнопочку, оно должно в модели появиться, модель с этим что-то сделает и скажет нам, что что-то произошло. Если мы хотим клиентскую валидацию, то она происходит вот здесь, чуть ли не перехватом, в особо тяжелых случаях, действительно, так, перехватом клика через Venta, Preventa, дефолт, и где-то пунктом ноль сбоку прикручена клиентская валидация.

Данные уходят в модель, модель их в себя кладет, обрабатывает, смотрит. Потом честно отправляем через submit формы данные на контроллер. Третья стрелочка — управление уходит на контроллер, модель сообщает контроллеру, что, отобрази, пожалуйста, лейбл «меня зовут Вася». Говорит нам, что, хорошо, данные приняты, ты, действительно, Вася. А данные «меня зовут Вася», четвертая стрелочка, желтая, туда кладет модель. Контроллер выбирает соответствующую вьюшку, отображает лейбл. Только snapshot. Вопрос, как это тестировать? Тут не на что даже функциональные тесты написать. По-другому никак.

Мы вбили «меня зовут Вася», нажали о’кей. Второй вариант, уже с MVP. Presenter сказали: кнопка нажата. Стрелочка под номер один, зелененькая, − управление ушло на Presenter. В классическом MVP не отправка данных от вьюшки на Presenter, а запрос с Presenter за данными. Presenter смотрит, стрелочка номер два, синенькая, обратите внимание, это запрос данных. Это гораздо чище, потому что Presenter может уже заранее знать, например, что данные ему не нужны, все равно все плохо.

Мы ее можем уже спокойно писать, это для нее специально место выделено. Дальше третьим пунктом на Presenter честная JS-валидация. Пятая стрелочка, видите, она полосатенькая, надеюсь, это видно, что она полосатенькая желто-зеленая, − и управление, и данные пришли обратно на Presenter. Четвертая стрелочка — данные уходят на модель, модель их, допустим, положила в базу, сказала: «Все в порядке, я положила». И шестая стрелочка, − отправили это на вьюшку, возможно, уже на другую, но тут я не стала вторую вьюшку рисовать. Модель сказала «Я положила», Presenter сам понял, что раз данные положили в базу, значит, надо отобразить, что все в порядке, данные положены.

JS-валидация встала на свое законное место и с ней стало все хорошо, AJAX тоже встал на место, это может быть четвертая стрелка, например, если модель находится на сервере, или модель сама AJAX сама идет на сервер. В чем у нас тут плюс. И, наконец, мы можем спокойно тестировать Presenter, писать на него функциональные тесты.

Мы получили разделение визуального отображения и его работы. Во-вторых, что мы еще получили в плюсе, кроме упрощенного тестирования? Мы можем исправить Presenter и не трогать View, и наоборот. То есть мы все еще можем написать snapshot на View, и мы отдельно можем написать тесты на Presenter. Так устроены фреймворки типа Angular1, Backbone, Ember, Knockout. У нас улучшилась специализация. Когда-то их было очень много, прямо яростная конкуренция.

Presenter кладется уже на клиент, модель может быть и там, и там single page application спокойно делаются. В чем особенности. Взаимодействие с сервером по AJAX хорошее. Бывает лучше, но так сделано на этой истории много single page application, или, как минимум, было сделано раньше. Казалось бы, все хорошо, зачем думать дальше? Клиентская валидация на месте.

Тоже интересная вещь. Однако был придуман, как минимум, MVVM.

Очень часто оказывалось, когда ты написал первый Presenter, второй Presenter, пятый Presenter, что они все одинаковые. По сути, это реализация Presenter средствами фреймворка. Как видите, он устроен подобно MVP. И они просто провязывают вьюшку и модель.

В чем плюсы? И поэтому многие фреймворки просто решили эти задачи, binding. И это реально ускоряет скорость разработки. Нам не надо писать лишний код. Усиливается связность между Model и ViewModel. В чем минусы.

Например, я лично знакома с MVVM во фреймворке i-BEM, который мы иногда используем, а иногда не используем, потому что неудобно, слишком жесткая провязка. То есть там возникают проблемы именно из-за сильной связанности, поэтому иногда бывает, что MVVM не используется. Не знаю, не пробовала. Однако есть, вот Microsoft Silverlight устроена по этой технологии, и говорят: хорошо.

Почему же так вышло, что кроме MVP и MVVM все-таки возникло что-то еще, всем вам знакомое по слову redux, зачем возникли однонаправленные потоки данных.

У нас с MVP регулярно такая проблема. Смотрим на правую картинку. Они все взаимосвязаны. Допустим, у нас сложная система, не одни к одному, − много вьюшек, много моделей. Модель поменяла другую модель. Вьюшка сверху, желтенькая, поменяла модель. Нижняя вьюшка тоже поменяла модель. Поменялась нижняя желтая вьюшка. Все они дружно поменяли центральную красную вьюшку, и в ней происходит что-то не понятное.

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

Мы не всегда можем предсказать, в каком она сейчас находится состоянии, и кто там первый пришел, кто что исправил. Проблема MVP именно в недетерминированности состояния системы. У него такого быть не может. Flux эту проблему решал, что называется, генетически. И эту концепцию придумали, конечно, задолго до Facebook, задолго до 2013 года, когда они это опубликовали. Мне тут долго говорили, что идея с однонаправленным потоком данных витала в воздухе, это правда. Но они, что называется, запатентовали, первые выпустили spreadshit, что мы придумали вот такую штуку, пользуйтесь.

Идея тут вот в чем. Давайте рассмотрим Flux поподробнее. Все остальное неправда. У нас есть Store, и этот Store — хранилище данных, это единственный источник истины нашего приложения. Сначала у нас, если посмотреть именно на цикл работы, он, обычно, начинается с того, что пользователь что-то сделал, то есть работает вьюшка. Как он работает. Обратите внимание, что Action без заливки на картинке. Вьюшка создает Action. Потому что это структура. Почему так? Это структура. Это не класс, не объект, это не что-то умное. В вебе, в JavaScript мы можем писать ее, она как раз тем самым абстрактным объектом.

Блок-диспетчер триггерит callback. Вьюшка структуру создает, передает на блок-диспетчер. Сказал вызвать Store». То есть он говорит: «Вызови функцию, которую мне сказали вызвать, когда случится Action. Метод вызывается. То есть вызывается метод Store из диспетчера. Store смотрит, что ему пришло, изменяет как-то сам себя. Метод вызывается, получается на Store. И он единственный, кто может менять свое состояние. Он меняет свое состояние. То есть он является единственным источником правды. Никто другой этого не делает. После чего броадкастит всем завязанным на него вьюшкам, всем завязанным на него компонентам: «Я изменился, сходи за данными».

В классическом Flux, в таком, в каком он представлен в Facebook, вьюшка перерисовывается полностью. Вьюшки ходят за данными, и дальше начинается интересный момент.

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

Произошло. Store в диспетчере вызывает: «Зарегистрируй, пожалуйста, мой callback, что я буду делать, когда на тебя придет Action». Мы нажали кнопочку, создали структуру. После чего можем работать с приложением. Очень важный момент, что Action сам передает, что он за Action, а диспетчеру все равно. Обратите внимание, что у Action, кроме данных, которые ввел пользователь, например, Вася, у него есть еще метаданные, тип. Первая стрелочка, вызывается метод. Он все Action broadcast кидает.

На триггер Action происходит вызов callback, который мы зарегистрировали в пункте ноль. Диспетчер вызывает метод, по сути, триггер Action и передает туда этот самый Action. Store берет эти данные, смотрит, что, ага, тип change name, значит, я меняю себя в поле name на Вася, и отправляю его на back-end, и как-нибудь валидируется, наверно, в общем, Store знает, что делать. Вот красная стрелочка, это вызов callback с обратного вызова. Мы поменялись. Дальше фиолетовая стрелочка брэдкастится событие change. Все знают, что у нас изменился Store.

Вьюшки идут за данными. Дальше маленькая особенность классического Flux, который, возможно, незнакомым окажется неожиданным для тех, кто работал с Redux, точнее, даже с React, а не с Redux. Мы привыкли к тому, что, наоборот, к вьюшкам все приходит, если работали с React, Redux или чем-то таким. Они идут в Store и говорят: «Мне вот это поле, вот это поле и вот это поле». И шестой пункт, полная перерисовка.

Перерисовка. Давайте посмотрим на эту схему и найдем узкое место, из-за чего? Что позволяло это сделать? Полная перерисовка, именно поэтому Flux активно начал использоваться после 2013 года, когда возникло что? Виртуальный дом, который позволяет перерисовывать только тогда, когда это, действительно, надо. Виртуальный дом.

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

Изначально React придумывался вообще, как замета вьюшек в MVC, MVP, вариации. Тот же самый 2013 год, тот же самый 2013 год, тот же самый Facebook. В чем его мощность. И его там, действительно, можно использовать. И только если, действительно, было изменение, мы перерисовываем компонент, в результате чего все работает гораздо быстрее, чем могло бы быть. Во-первых, виртуальный дом, как правильно сказали, позволяет не перерисовывать реальный дом, потому что это очень тяжелая операция, а перерисовывать виртуальный.

Это механизм properties. И — чистые иммутабельные компоненты. И если писать в этой архитектуре, то очень правильно создавать компоненты чистыми, без стейта, без состояния. Реализация тоже реактовская, позволяет создавать компоненты, у которых нет собственного состояния. Их удобно тестировать, они очень редко ломаются. У них есть только данные, которые пришли от Store, и он их отрисовывает. То, что статично, ломать довольно сложно, а тестировать — легко.

Наверно, многим известно, что это действительно мощная штука. Приложения в сочетании с Flux-архитектурой получаются мощные. Кроме React Redux существует масса других связок. В чем некоторая важность, которую обязательно надо упомянуть? Это тоже сочетание реактивного фреймворка и Flux-архитектуры. И, наверно, вам известно, что есть второй Angular. д. Есть View, есть другие реализации Flux кроме Redux — Fluxxor, MobX и т. Тот же View, например, более удобен для создания маленьких приложений, чем React Redux. Не надо зацикливаться на React Redux. Он гораздо более легковесный.

Казалось бы, сейчас только React Redux и все хорошо. Как выбирать между всем этим многообразием? На самом деле — нет. Ну View, ладно. Потому что у вас наверняка есть админка с кучей данных и отображение. Если у вас есть простой сайт со статичными страницами или очень простым клиентским вводом — гораздо проще быстро запустить MVC-фреймворк. На каком-нибудь React Redux вы убьетесь это писать. И никакое взаимодействие вам не требуется.

Она сейчас редкая, потому что приложения нужны чаще — многостраничные, с динамическими, но достаточно простыми данными. MVP/MVVM-фреймворки тоже имеют свою нишу. Какие-то данные от пользователя все-таки приходят. Не single page application, а multiple page application. А простенькие, на MVP, было бы делать достаточно удобно. Например, так было бы неудобно делать простые вики-страницы, без какого-то суперсложного форматирования и интерактивности.

д. Сейчас самый частый для нас кейс — single page application и сложная логика, много взаимодействия между компонентами, всякие умные вводы и т. д. Это Flux с виртуальным домом React Redux, View, Angular, MobX, Fluxxor и т.

Литературка. Заключение.

NET, MVC on Web
> MVP: MVP vs MVC, GUI Architecture, Backbone, Angular1
> MVVM: MS Silverlight, БЭМ и i-BEM
> Flux: блог компании Hexlet, Flux for stupid people, Flux official, ReactJS, VueJS
> Прочее: Стоян Стефанов, «Javascript. > MVC: Smalltalk-80, General MVC, ASP. Garlain, M. Шаблоны», Эрик Фримен и др., «Паттерны проектирования», D. Dijkstra ”GOTO statement considered harmful” (1968)
Shaw, ”An introduction to Software Architecture” (1994), E.

Про MVC, MVP, MVVM можно почитать много всего. Понятно, что в первую очередь есть документация к соответствующим программным приложениям. Про Flux в интернете есть много объяснений. Почитайте, возможно, будет понятнее. Наверно, самая интересная строка — последняя. Она вообще про все. JavaScript. Шаблоны. Если вдруг вам придется жить в мире ES5, то в первой книжке «JavaScript. Шаблоны» вы найдете очень много про то, как удобно строить архитектуру без всех вот этих ES6-возможностей — которые, конечно, есть, но иногда приходится жить без них.

Очень полезная книжка. Эрик Фримен, «Паттерны проектирования». Многое из того, что там написано, использовано и во Flux, как вы потом заметите. Там примеры на Java, но это не должно вас пугать. А вот такой паттерн я могу использовать в этом блоке, который рисует у меня картиночки на экране. А вот это вот использовано в MVP, а вот это — там-то. Очень полезная книжка и легко читается.

Она, конечно, устарелая, но, что называется, надежная. Та самая книжка, Дэвид Герлан и Мэри Шоу, «Введение в архитектуру ПО». Это как арифметика. И очень рекомендую ту самую статью Эдсгера Дийкстры «GO TO: Operator consider is harmful». Наверно, без нее никуда.

Оно будет интересным. Домашнее задание. Я думаю, любой более-менее опытный программист скажет, что рано или поздно эта мысль ему приходила в голову. Мы напишем свой фреймворк. Реализацию серверных компонент писать не надо — достаточно просто в консоль вывести, что мы сделали вид, будто отправили что-то на сервер. Вам будет предложено написать, как минимум, маленький Flux, ту самую историю с лейблом, кнопкой и input на Flux. Можно перерисовать только кусочек и сделать вид, что компонента перерисовалась полностью. Реализацию полной перерисовки экрана виртуального дома тоже писать не обязательно. Большое спасибо. Вопросы, пожелания можно писать сюда.

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть