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

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


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

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

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

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

bv2mkka_ztkeyh6qgwvizb_oqsk.jpeg

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

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

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

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

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

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

yc9ke9y2n-ywlc5u8esvrfhhe4k.jpeg

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

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

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

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

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

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

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

fedmo7no6wpl7dcco8nn-o8ktiu.jpeg

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

kekg57hzr3erft5cfqrelnfgjwa.jpeg

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

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

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

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

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

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

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

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

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

whdt05wzktnd2lfx3clnz2t5hq0.jpeg

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

btr6dhizspc6nag8fusfusqb5ti.jpeg

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

208jjb0c46u5ap_6yhdvjjdb0-k.jpeg

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

3r0ulrmuomtsrmbuuqz0x9mbfye.jpeg

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

pwk-eqd0mzubaatv3mkauzqtdmq.jpeg

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

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

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

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

rmpbdf8kcpdxy3n-o7elkjqzuf8.jpeg

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

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

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

tjiida1_0svgswa1j7at7e36wto.jpeg

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

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

mzjvcphyv-ww_qxak1dkhrayj04.jpeg

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

mnfjszrrwxblgrmhty4jcxchpf4.jpeg

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

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

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

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

hmmnrqebwtywkhb8c5o35gv7n6a.jpeg

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

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

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

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

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

lzu5btqwmkn4qlxej0cwvrpq4xk.jpeg

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

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

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

pnfzmbqi3a7_xxro1ocxrnfelf4.jpeg

Как устроена среднестатистическое веб-приложение? Есть сервер. Внутри сервера реализована какая-то архитектура back-end. Наружу от него торчит какой-то API, например, это может быть REST API или не REST. Все вместе — клиент и сервер, − это тоже реализация архитектурного подхода клиент-серверного. Потому что у нас могут быть чисто серверные приложения, чисто клиентские приложения, какой-нибудь 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-х годах придуман замечательный архитектурный подход MVC. M — Model, V — View, C — Controller. Подход оказался очень удобным. Придумали его вообще для консольных приложений. Но когда начали развиваться веб-технологии, когда это все начали использовать, оказалось, что иногда нужно, вот модель MV хорошо, а Controller реализуем не так. В результате оказалось столько различных вариаций реализации Model-View-что-нибудь, что сначала возникла путаница из-за того, что все это называли MVC. Потому что, если модель MV есть, то третья — это Controller, неважно, что мы там, на самом деле, запихнули.

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

mvycfuvbvwewv3dyc3jzfqnxgdo.jpeg

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

hn32tkw40hvqtfjylhpbguvvu40.jpeg

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

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

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

sjklf5h9jknh0zzhtdhfio2pkvm.jpeg

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

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

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

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

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

4yw2geofqyb5tmvj1fpv6o-q__e.jpeg

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

byk2spod-5nthacsrxtjmbcxvwg.jpeg

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

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

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

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

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

hwithnio6pftr2qf-5ogkg7x4vc.jpeg

Во-вторых, что мы еще получили в плюсе, кроме упрощенного тестирования? Мы получили разделение визуального отображения и его работы. То есть мы все еще можем написать snapshot на View, и мы отдельно можем написать тесты на&n

© Habrahabr.ru