Выбор подходящей архитектуры — ключевая часть построения фронтенда сервиса. Разработчик Анна Карпелевич рассказала студентам Школы разработки интерфейсов, что такое архитектура, какие функции она выполняет и какие проблемы решает. Из лекции можно узнать о наиболее популярных архитектурных подходах во фронтенде: Model-View-* и Flux.
— Добрый вечер. Меня зовут Аня Карпелевич. Мы сегодня с вами будем говорить про архитектуру фронтенда верхнего уровня.
Я работаю в Директе. Мы делаем интерфейсы для рекламодателей. Они подают объявления, настраивают их. Это очень сложная, интересная система, в ней много взаимосвязанных компонент, они прорастают друг в друга, у них есть общие и свои функциональности. «Брюки превращаются в элегантные шорты». Все это приходится очень тщательно контролировать. И архитектура в наших приложениях очень сложная. Это одна из причин, почему сегодня эту лекцию читаю я. Я очень люблю эту тему.
Что такое архитектура? Дело в том, что ответа на этот вопрос, наверно, нет. Или есть, но у каждого свой. Это очень спорная тема. Она вызывает массу споров, массу холиваров. И много из того, что сегодня я буду рассказывать, — мое мнение. Частично его поддерживает моя рабочая группа, частично — не очень. И каждый, когда он пишет архитектуру своего приложения, решает для себя, как и что делать.
Именно поэтому архитектура — одно из самых, наверно, творческих мест в работе программиста. И поэтому сегодняшняя наша презентация тоже начнется с творчества.
Давайте посмотрим на левую картинку. Я буду очень рада, если кто-нибудь узнает здание, которое на ней изображено. Это церковь Сен-Сюльпис в Париже. Обратите внимание на башенки, ради них эту церковь сюда и поставили. Надеюсь, видно, что они разные. Довольно сильно разные, и тому есть интересная причина. Между ними 130 лет разницы. Потом левую башню снесли и перестроили во время Франко-прусской войны.
Почему она здесь? Посмотрите на эту картинку. У башен одинаковая архитектура, а все окружение, вот эти виньеточки, финтифлюшечки, арочные конструкции — разные. Почему так? Потому что назначение у этих башен одинаковое. Ни одна из них, например, колокольней не была. Это просто башни. В них что-то хранилось, а все остальное — разное. Почему? Потому что архитектура у этих башен одинаковая. У обоих есть свод, всего одно окно, и оно стрельчатое. Высота окон примерно одинаковая. И идея в том, что архитектура, как здания, так и приложения, — опорная конструкция. Это не виньеточки, не финтифлюшечки, не реализация. Это то, что стоит в основе. И эта основа, как правило, зависит от окружающей среды, от почвы, если речь идет о здании, от цели, которую ставит перед собой архитектор, но почти никогда не зависит от дизайнерских изысков.
Пример со зданием для темы про архитектуру достаточно очевиден. А вот правая картинка более интересная. «Архитектура — это онемевшая музыка». «Architektur ist gefrorene Musik», — сказал в XVIII веке Иоганн Вольфганг Гете. Гете, скорее всего, ничего не знал про архитектуру зданий, он был поэтом. И он гарантированно ничего не знал про архитектуру приложений. Но высказал очень ценную и интересную мысль.
Музыка существует в динамике. Это не что-то статичное. Это процесс. И точно так же приложение является процессом. У него есть момент запуска, у него есть момент развития, когда мы с ним что-то делаем, работаем. И у него, наконец, есть момент завершения. Архитектура приложения — его срез в любой конкретный момент времени. В любой момент наше приложение, как музыкальная тема, должно быть четким, ясным, понятным, предсказуемым и т. д. Иначе все развалится.
На этом с творческим вступлением мы заканчиваем, переходим к вещам более приземленным, более близким к практике построения приложений.
Что такое архитектура и зачем она нужна?
Во-первых, у нас бывает организация большого объема кодов, то, с чем мы в Директе — и не только в Директе — сталкиваемся постоянно. Кода так много, что в нем можно потеряться. Мы не хотим теряться в коде.
Во-вторых, дублирование функциональности. Тоже вечная проблема, с которой вы всегда будете встречаться, и сегодня эта тема про дублирование пройдет прямо красной линией через всю лекцию. Одна и та же функциональность может быть нам нужна в нескольких местах интерфейса. Если она нужна в нескольких местах, значит, это должен быть физически один и тот же код, который используется в нескольких местах, а не копии. Почему? Мы про это дальше поговорим. Но архитектура нам должна помогать избежать копипаста.
Третье — поддержка. Достаточно очевидно, что если у нас есть приложение, то его нужно как-то поддерживать, и желательно, чтобы на это не тратились все ресурсы команды.
Изменение состава команды. Тоже такая вещь, с которой мы встречаемся в реальной жизни чаще, чем нам бы хотелось. Кто-то приходит, кто-то уходит, и если человек тратит полгода на то, чтобы въехать в код, это плохо. Если знания о коде хранятся только в одной голове, и он будет полгода передавать эти знания в случае ухода, это еще хуже. В общем, здесь архитектура нам тоже помогает сделать все это более понятным и поддерживать шаринг знаний.
Добавление и расширение функциональности. Тоже достаточно очевидная вещь. Прибегает к нам менеджер и говорит, что срочно надо вот это вот. И если для того, чтобы сделать это срочно, придется потратить уйму сил и времени, то это плохое архитектурное решение. А нам надо хорошее.
И, наконец, ошибки. Чем понятнее, предсказуемей наша архитектура, тем легче искать ошибки, тем меньше багов.
Как это все можно обозвать? Это все можно обозвать — проблемы сложной системы. Приложение — это сложная система, архитектура помогает нам решать проблему.
Короче, как-то так. Вот от меня справа картинка с лапшой, и это то, что бывает, если за архитектурой не следить, если ее не выстраивать, не продумывать и не проектировать. А вторая картинка — это то, что бывает, если архитектуру все-таки хоть как-то продумать. Это еще не Сен-Сюльпис, но хотя бы детский конструктор, он крепко стоит и не разваливается. В конструктор мы сегодня тоже будем много играться.
Формально обо всем этом. Архитектура — это способ решения проблем сложной системы путем абстракции реализации от интерфейса и разграничения полномочий между блоками кода. Дальше вот эту длинную фразу мы будем подробно разбирать.
В чем особенности архитектуры приложения как области знания? У нее есть конкретная область, с которой мы работаем. То есть это не что-то отвлеченное, это очень конкретная вещь. Вот стоит задача, мы под нее подбираем архитектуру, а не так, что, у-у-у, интересный архитектурный подход, надо попробовать. Так вот, нет. Попробовать можно на чем-то маленьком, а для серьезного проекта архитектура подбирается, иногда сочиняется под конкретный проект.
История вопроса, когда, вообще, возникла эта мысль, что нужно делать архитектуру. Эту, надо сказать, в свое время очень неординарную мысль высказал в 1968 году Эдсгер Дейкстра, замечательный программист. Он больше, наверно, известен как автор алгоритма Дейкстра, поиск самого короткого пути в графе. Но у него масса, на самом деле, прорывных для своего времени идей. И одна из них — это статья, я вам дам потом ссылочку на материалы, можете прочитать, там всего два листа, коротенькое эссе. Звучит оно как «Operator GOTO considered harmful», в переводе «Оператор GOTO — оператор безусловного перехода — зло». Это была первая мысль, что давайте официально скажем, что надо писать архитектуру, а не лапшу.
В 70-х годах эта идея развивалась уже Дейкстра в соавторстве с Парнасом, и сами по себе, по отдельности. Первая книга подробная об архитектуре приложений в целом была написана в 1996 году Мэри Шоу и Дэвид Гэрлан. После этого, на самом деле, подробных таких книг об архитектуре программного обеспечения не писалось именно из-за области применения, что в каждой сфере знаний есть свои архитектурные подходы, где-то одно, где-то более популярно другое, что-то, вообще, не применимо в каких-то местах. И поскольку архитектура — процесс творческий, какие-то конкретные книги про как писать архитектуру, вы не найдете. Может быть, после 1996 года ничего такого особо подробного на эту тему и не было.
Какие требования предъявляются к архитектуре проекта сейчас. Во-первых, и самое главное, что от него требуется, на самом деле, это расширяемость, потому что если ваш проект не расширяется, он мертв.
Переиспользование кода. Это про ту самую копипасту. Если у вас есть два блока, которые используются в двух разных местах, нужна одна и та же функциональность, значит, нужно переиспользовать один и тот же код, и архитектура должна быть такой, что любой кусочек кода можно взять и переиспользовать, как только это понадобится.
Разделение полномочий между модулями кода. Про это мы тоже сегодня поговорим поподробнее, зачем это надо. Идея такова: каждый модуль, каждый блок, каждый кусочек кода должен выполнять одно свое конкретное действие, нести ровно одну функцию. И эта функция должна быть вынесена в заголовок этого метода, класса, чем бы это ни было, модуля. Один модуль — одна функция.
И, наконец, качество приложений. Тут много вещей, которые хотелось бы сделать, − и безотказность, и обратная совместимость. В реальности опять же, выбирается под задачу. Где-то нужна обратная совместимость так, чтобы ни в коем случае ничего не отъезжало. Где-то нужна надежность, чтобы, не дай Бог, пароли, пин-коды карточек или CVV не утекли никуда. Где-то нужно, чтобы это было безотказно, если это спутник или еще что-то. В общем, выберете любые несколько. Чем больше вы захотите поддержать, тем больше сложностей в архитектуре вы, скорее всего, встретите.
Дальше мы будем с вами говорить про некоторые определения, именно такие энциклопедические вещи. Почему это важно? Потому что терминология в архитектуре очень важна, и нам нужно с вами говорить на одном языке. Определения в массе своей взяты из парадигмы программирования под названием ООП. Но на самом деле они проросли в другие парадигмы, с терминами «класс, объект, интерфейс» оперируют не только в рамках ООП. Однако определения эти и понимание взяты именно из мира ООП.
Самая простая вещь — это класс. Что такое класс? Это шаблон, это образец. Вот, например, класс Змея — class Snake. У нее мы определили три приватных поля, то есть поле, которое недоступно никому, кроме методов самого класса, − количество голов, количество хвостов и длина в попугаях. Мы определили конструктор, в котором мы ставим эти самые головы, хвосты и длину в попугаях. Получили класс Змея. Все просто.
Едем дальше. Объект. А объект — это экземпляр конкретной структуры. Причем, опять же в классическом ООП подразумевается, что объект это объект класса. В современном мире в JavaScript, который не всегда был ООП-языком, да и сейчас не всегда и не везде ООП, мы знаем, что могут быть абстрактные объекты. То есть мы сможем создать объект, литерал, который не будет объектом класса. Но здесь пример, как мы создаем объект класса Змея. Вот у нас двухвостая змея длиной в 38 попугаев, − удав.
Модуль. Модуль — это семантическая единица. Это не всегда класс. Это может быть набор классов, набор объектов, набор методов, не объединенных в классы. Обычно, можно считать, что модуль это то, что вы записали в один файл. Но, в принципе, модуль — это и папка, в которой они лежат, например, − файл и тесты к этому модулю, тоже модуль. Здесь важно то, что модуль — это то, что вы назвали модулем, то, что вы считаете единицей семантики. В данном случае модуль про то, как мы едим змей. Результатом работы этого модуля является последний метод, eatSnake, как мы съели змей. Не знаю, зачем мы едим змей, но мы это умеем делать, потому что мы так написали этот модуль.
Это было тривиально, дальше начнется несколько более интересная вещь. Интерфейс класса. Интерфейс класса — это, проще говоря, его публичные методы, то, чем он торчит наружу, то, что мы можем получить от объекта этого класса от объекта извне. Вот этот класс реализует интерфейс getSnakeLength. Он может нам вернуть длину змеи. Обратите внимание, что доступа извне к приватным полям нет. Доступ извне есть только к публичному методу getSnakeLength.
А вот дальше очень интересная вещь. Мы долго спорили, как эту штуку назвать, потому что термин «абстрактный интерфейс» я придумала, когда создавала эту лекцию. И, честно говоря, я нигде не видела нормального определения этого подхода и метода. Тем не менее, многие языки программирования позволяют создавать абстрактные интерфейсы, и обзывают их, как только не абстрактными классами, и абстрактными интерфейсами тоже, просто интерфейсами. Получается омоним с интерфейсом класса. Идея такова, что абстрактный интерфейс — это набор методов, которые что-то делают. Когда мы создаем класс, мы идем от вопроса «что это?» Это змея и она что-то умеет делать, или не умеет. Она может просто отдавать свою длину.
А когда мы создаем интерфейс, мы идем от того, что он делает, что он должен уметь делать. И это оказывается очень мощным способом расширения классов. Мы можем приписывать классам какие-то возможности, расширяя его при помощи интерфейсов. Например, фреймворк I-BEM такую штуку умеет, встроена во фреймворк такая история с абстрактными интерфейсами. Многие фреймворки, к сожалению, не умеют, а штука мощная.
Вот в качестве примера мы создали интерфейс audiable, что-то, что умеет звучать. И определение у него — абстрактный пустой метод getNoise. Мы расширили нашу змею классом audiable, реализовали у нее метод getNoise, и наша змея зашипела. Вдохновение к этому сету примеров мне дала замечательная книжка Эрика Фримена и компании «Паттерны проектирования».
Сейчас мы попробуем эти примеры посмотреть немножко более конкретно.
Но сначала поговорим о том, зачем эти примеры были нужны. А нужны они были вот для этого большого слайда. То, что здесь написано, настолько важно, что я даже вынесла это в желтый титульник. Это, можно сказать, мантра. Это очень важный принцип, который вам нужно всегда про него думать, когда вы проектируете архитектуру. High cohesity, low coupling — сильное сцепление, слабая связность. Есть некая проблема с тем, что слово cohesity и слово coupling на русский и так, и так переводятся «связность», специально для этого принципа придумали слово сцепление.
Идея вот в чем. Ваши блоки должны быть очень компактны, очень сильно сцеплены. Они должны реализовывать ровно одну функцию. А между собой они должны быть связаны очень легко, чтобы их можно было легко комбинировать, собирать, как конструктор. И тогда ваша архитектура будет достаточно гибкой и достаточно надежной. А еще легко тестируемой.
Давайте посмотрим, как нам добиться сильного сцепления и слабой связанности по пунктам, что называется.
Специализация. Каждый блок решает только одну задачу. Вот у нас хорошая иллюстрация — детский конструктор. У нас каждый блок, или набор блоков. Они все своей формы, своего размера. И если нам нужно построить дом, мы возьмем длинные брусочки. Если нам нужно построить шарик, мы возьмем короткие брусочки. У каждого брусочка есть своя функция. И те, кто играли в конструкторы, знают: чем проще форма кусочков, тем больше из него можно построить. Из вот таких загогулин ничего не построится, или строится только то, что описано в инструкции. А кому оно надо?
То же самое, абстракция. Это про то, что абстракция интерфейса от реализации. Идея в том, что интерфейс внешний, то, как это наш класс, наш блок торчит наружу, то, как он взаимодействует с другими блоками, не должно влиять на его внутреннюю реализацию. Наоборот — бывает. В другую сторону — никогда. В хорошей архитектуре. Здесь в качестве примера — формирование вот этих пупырышков не влияет на форму самого блочка. Мы выбираем отдельно форму блока и уже на него пупырышки наклеиваем.
Инкапсуляция. Продолжение предыдущей темы. В приватных методах, то есть в том, что изнутри наших блоков, мы реализуем сам смысл нашего блока, реализацию. А интерфейс, то, как они связаны, находится в публичных. То есть, в данном случае, вот эти все крестики, черточки и сама форма, − это реализация. А пупырышки — это интерфейс. И хорошая архитектура выглядит как такой конструктор.
О, какой страшный монстр. Это про переиспользование кода. Изначально этот монстр, вообще-то, был для того, чтобы показать пример плохой архитектуры, но вот посмотрите на него внимательно. Он прекрасен. Более того, он явно доволен жизнью, вполне бодро бегает на своих странных ногах. Возможно, даже умеет летать, или, как минимум, у него красивые крылья от бабочки.
В чем идея? Если у вас есть реализация для верблюда и реализация для крокодила, и к вам приходит менеджер, и говорит, что срочно нужен верблюдо-крокодил. Вы не пишете отдельно верблюдо-крокодила. Вы берете тело верблюда, отделяете его от всей реализации верблюда. Берете голову крокодила, отделяете ее от крокодила, и переиспользуете блоки. Зачем это надо?
Затем, что когда менеджер снова к вам прибежит и скажет, что срочно мы расширяемся на Южную Америку, а там аллигаторы, нам нужно поддержать неправильную форму челюсти, или что там, у крокодила, четвертый зуб не такой, вы не будете шарить по всему проекту, где же у вас скопированы головы крокодилов. Потому что у вас там может быть рядом еще какой-нибудь зебро-бизоно-крокодил. Вы просто возьмете свой класс голова крокодила, сделаете у него расширение из серии голова аллигатора, передадите ему параметры, он будет сам определять, какие зубы ему рисовать. И все. В одном месте, а не во всех местах, где это использовано.
Здесь надежность повышается в разы, потому что вы гарантированно забудете какую-нибудь скопированную голову в каком-нибудь очень редком проекте. В общем, ничего страшного в таких кадаврах нет. Хороший кадавр, полезный.
Сейчас мы будем смотреть прямо-таки примеры плохого кода. Обратите внимание, это псевдокод. Псевдокод немножко похожий на TypeScript, но все равно псевдокод. Не пытайтесь это запустить, оно не работает. Повторить можно, запускать именно этот код не стоит, потому что здесь использованы синтаксические конструкции, которые TypeScript 2.7 не поддерживает, зато иллюстрации хорошие получились (сейчас существуют более актуальные примеры — прим. ред.).
Итак, у нас есть класс User. У него есть имя и возраст. Все хорошо. У нас есть User с фамилией, прошу прощения за разъехавшиеся шрифты. User с фамилией, у него есть имя, возраст и фамилия.
И у нас есть метод printLabel. Мы передаем ему User. Дальше смотрим, если у нас User класса User, мы рисуем имя и возраст. Если User класса User с фамилией, то имя, фамилия и возраст. Давайте все-таки попробуем посмотреть, что здесь плохо.
Дублирование кода, еще? Много разного дублирования кода, хорошо. Да, хорошо. Тут два дублирования кода, − одно про то, что мы дублируем UserWithSurname, второе, что мы дублируемся в методе printLabel. Еще что есть? Правильно, да, это все про то, что у нас много дублирования кода, потенциально еще больше. Что-нибудь еще тут есть? Наследование тоже здесь есть, и это тоже один из вариантов. Тут есть две проблемы, − нет переиспользования, нет специализации. Мы еще про две вещи говорили. PrintLabel лезет в приватные методы. Еще? Четвертого чего здесь не хватает? Да, все так.
Специализации нет, два блока делают одно и то же. Абстракции нет, у нас интерфейс и реализация смешаны. Инкапсуляции нет, действительно, доступ к приватным методам. Переиспользование кода, про бесконечные if, которых может стать очень много, сказали очень правильно. Давайте посмотрим, как это сделать получше, а так не будем.
Мы создадим интерфейс printLabel, это не потому, что iPrintLabel это не потому, что iPhone, а потому что интерфейс. И у него определим один-единственный абстрактный метод getText. Создадим класс User, который имплементирует iPrintLabel. У него появятся, действительно, приватные поля имя и возраст, и один-единственный публичный метод, тот самый getText из iPrintLabel, в котором мы уже честно обратимся из класса к его приватным полям, это разрешено и даже поощряется. UserWithSurname, действительно унаследуем от класса User, и нам нужно будет здесь только доопределить Surname и переопределить getText. А вот printLabel станет очень простым. Он станет принимать iPrintLabel и просто выводить getText.
Прелесть тут в том, что если абстракция появляется, интерфейс отдельно, реализация отдельно. Инкапсуляция появляется. Специализация, пожалуйста, мы сделали наследование для этого. И с переиспользованием кода все, вообще, прекрасно, потому что мы можем печатать что угодно, главное, расширить его интерфейс iPrintLabel, и мы можем не думать, напечатается оно, не напечатается, − напечатается. Метод printLabel мы больше трогать не будем. Вот такой хороший, очень простой короткий способ улучшить архитектуру за несколько лишних строчек кода.
На этом месте мы заканчиваем с теорией. С теорией всего, потому что то, что мы сейчас описывали, оно верно не только для front-end, для всего, вообще. И переходим к архитектурным подходам и отдельно к архитектурным подходам, которые применяются во front-end и полезны, используются, встречаются.
Как устроена среднестатистическое веб-приложение? Есть сервер. Внутри сервера реализована какая-то архитектура back-end. Наружу от него торчит какой-то API, например, это может быть REST API или не REST. Все вместе — клиент и сервер, − это тоже реализация архитектурного подхода клиент-серверного. Потому что у нас могут быть чисто серверные приложения, чисто клиентские приложения, какой-нибудь PowerPoint, с которого это все играется. Это же чисто клиентское приложение сервера.
Дальше мы подробнее посмотрим на front-end. Front-end состоит из каких-то крупных блоков. Каждый блок каким-то образом реализован, и эта реализация позволяет связывать крупные блоки между собой. Внутри модуль, он тоже как-то реализован. Внутри модуля метод. У этого метода тоже есть архитектура. И поэтому архитектура это, вообще-то, иерархия. На каждом уровне она существует, хоть на уровне объявления переменной, это тоже может быть кусочком архитектуры. Маленьким.
Мы же будем говорить сегодня о верхнем уровне архитектуры front-end, то есть о том, как устроены крупные модули, как путешествуют данные от пользователя к серверу, от сервера к пользователю, и немножко о реализации внутри модулей, как их реализовывать, чтобы они архитектуру эту создавали.
Вот такие есть архитектурные подходы. Некоторые из них мы сегодня упоминали. Клиент-серверная архитектура; компонентная архитектура, одна из ее вариаций вам знакома по React, надеюсь, знакома. Событийная, которая, как ни странно, тоже всем знакома, на ней основаны практически все операционные системы для персональных компьютеров. REST, то, что мы любим в сервере, и две последние, с которыми мы сегодня будем знакомиться подробно, самые фронт-эндовые, то, с чем мы работаем, это модель-представление* и однонаправленные потоки данных.
Начнем с MV*. Почему звездочка? История, что называется, полная боли и гнева. Был когда-то давно, еще в 80-х годах придуман замечательный архитектурный подход MVC. M — Model, V — View, C — Controller. Подход оказался очень удобным. Придумали его вообще для консольных приложений. Но когда начали развиваться веб-технологии, когда это все начали использовать, оказалось, что иногда нужно, вот модель MV хорошо, а Controller реализуем не так. В результате оказалось столько различных вариаций реализации Model-View-что-нибудь, что сначала возникла путаница из-за того, что все это называли MVC. Потому что, если модель MV есть, то третья — это Controller, неважно, что мы там, на самом деле, запихнули.
Потом оказалось, что люди путаются и подразумевают под MVC совершенно разные вещи. Примерно сейчас, не больше года назад, начали активно разделять эту терминологию, и делать для каждой реализации этого подхода свое название. Так или иначе, появилась вот эта MV*. Еще я видела в интернете термин MVW, где W — Whatever. Ну а мы переходим, собственно, к MVC-технологиям.
Как они устроены? Идея в том, что у нас есть модель, которая хранит данные. Их, как правило, много. Есть какой-то View, который показывает эти данные пользователю. Их тоже, как правило, много. И некий третий компонент, который между ними посредник, провязывает данные и отображение. Вот пользователь в правом верхнем углу со всем этим работает.
MVC, то, с чего все началось, это далекий 1980 год, Smalltalk. Но именно в таком виде он существует в некоторых фреймворках до сих пор. Не в некоторых, довольно во многих. В чем идея? Пользователь работает напрямую с вьюшкой и контроллером. Он вводит данные в какие-то поля во вьюшке, нажимает кнопку отправки и данные попадают на контроллер. Это отправка формы. Честная такая отправка формы по кнопке submit, всем знакомая давно, я надеюсь.
Смотрим. Желтая стрелочка от пользователя к контроллеру — это пользователь передал данные на контроллер по кнопке submit. Зеленая стрелочка, − туда же перешло управление. Контроллер смотрит на эти данные. Возможно, он их как-то обрабатывает, здесь уже тонкости реализации, и отправляет их на нужную модель. Контроллер сам выбирает, на какую модель отправить. Отправляет зеленой стрелочкой управления, отправляет желтой стрелочкой данные.
Модель тоже обрабатывает данные. Возможно, она их валидирует. Возможно, она их кладет в базу. Короче, модель знает, что с ними сделать. Как правило, в результате получаются новые данные. Например, мы можем сообщить пользователю, залогинился он или нет, а модель проверяла пароль с логином. После чего, модель передает управление на контроллер опять, чтобы контроллер выбрал, какую вьюшку отобразить. А данные идут непосредственно во View. Как можно такое сделать, вообще, как может модель данные во вьюшку отправить?
Очень просто. Если контроллер и модель находятся в 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 фреймворк.
Model-View-Presenter. Его схема уже гораздо более простая. Есть модели. Они между собой взаимодействуют. Отдают данные на Presenter, Presenter передает их во вьюшку, показывает пользователю. И обратно. Пользователь вбивает что-то во вьюшку, нажимает кнопку, Presenter это смотрит, AJAX отправляет на модель или кладет в модель, а модель AJAX отправляет на сервер. То есть здесь уже все гораздо боле просто и линейно, но без серверной шаблонизации здесь уже будут сложности. Если вы хотите серверную, вот такая система будет сложновата.
Давайте сравним. Посмотрим на первую картинку, где мы попытаемся реализовать очень простую вещь — отправку данных из input в модель. Мы что-то ввели, нажали кнопочку, оно должно в модели появиться, модель с этим что-то сделает и скажет нам, что что-то произошло. Мы вбили: «меня зовут Вася», нажали о’кей. Если мы хотим клиентскую валидацию, то она происходит вот здесь, чуть ли не перехватом, в особо тяжелых случаях, действительно, так, перехватом клика через event.preventDefault(). И где-то пунктом ноль сбоку прикручена клиентская валидация.
Потом честно отправляем через submit формы данные на контроллер. Данные уходят в модель, модель их в себя кладет, обрабатывает, смотрит. Говорит нам, что, хорошо, данные приняты, ты, действительно, Вася. Третья стрелочка — управление уходит на контроллер, модель сообщает контроллеру, что, отобрази, пожалуйста, лейбл «меня зовут Вася». Контроллер выбирает соответствующую вьюшку, отображает лейбл. А данные «меня зовут Вася», четвертая стрелочка, желтая, туда кладет модель. Вопрос, как это тестировать? Только snapshot. По-другому никак. Тут не на что даже функциональные тесты написать.
Второй вариант, уже с MVP. Мы вбили «меня зовут Вася», нажали о’кей. Стрелочка под номер один, зелененькая, − управление ушло на Presenter. Presenter сказали: кнопка нажата. Presenter смотрит, стрелочка номер два, синенькая, обратите внимание, это запрос данных. В классическом MVP не отправка данных от вьюшки на Presenter, а запрос с Presenter за данными. Это гораздо чище, потому что Presenter может уже заранее знать, например, что данные ему не нужны, все равно все плохо.
Дальше третьим пунктом на Presenter честная JS-валидация. Мы ее можем уже спокойно писать, это для нее специально место выделено. Четвертая стрелочка — данные уходят на модель, модель их, допустим, положила в базу, сказала: «Все в порядке, я положила». Пятая стрелочка, видите, она полосатенькая, надеюсь, это видно, что она полосатенькая желто-зеленая, − и управление, и данные пришли обратно на Presenter. Модель сказала «Я положила», Presenter сам понял, что раз данные положили в базу, значит, надо отобразить, что все в порядке, данные положены. И шестая стрелочка, − отправили это на вьюшку, возможно, уже на другую, но тут я не стала вторую вьюшку рисовать.
В чем у нас тут плюс. JS-валидация встала на свое законное место и с ней стало все хорошо, AJAX тоже встал на место, это может быть четвертая стрелка, например, если модель находится на сервере, или модель сама AJAX сама идет на сервер. И, наконец, мы можем спокойно тестировать Presenter, писать на него функциональные тесты.
Во-вторых, что мы еще получили в плюсе, кроме упрощенного тестирования? Мы получили разделение визуального отображения и его работы. То есть мы все еще можем написать snapshot на View, и мы отдельно можем написать тесты на Presenter. Мы можем исправить Presenter и не трогать View, и наоборот. У нас улучшилась специализация. Так устроены фреймворки типа Angular1, Backbone, Ember, Knockout ранних версий. Когда-то их было очень много, прямо яростная конкуренция.
В чем особенности. Presenter кладется уже на клиент, модель может быть и там, и там single page application спокойно делаются. Бывает лучше, но так сделано на этой истории много single page application, или, как минимум, было сделано раньше. Взаимодействие с сервером по AJAX хорошее. Клиентская валидация на месте. Казалось бы, все хорошо, зачем думать дальше?
Однако был придуман, как минимум, MVVM. Тоже интересная вещь.
По сути, это реализация Presenter средствами фреймворка. Очень часто оказывалось, когда ты написал первый Presenter, второй Presenter, пятый Presenter, что они все одинаковые. И они просто провязывают вьюшку и модель. Как видите, он устроен подобно MVP.
И поэтому многие фреймворки просто решили эти задачи, binding. В чем плюсы? Нам не надо писать лишний код. И это реально ускоряет скорость разработки. В чем минусы. Усиливается связность между Model и ViewModel.
То есть там возникают проблемы именно из-за сильной связанности, поэтому иногда бывает, что MVVM не используется. Например, я лично знакома с MVVM во фреймворке i-BEM, который мы иногда используем, а иногда не используем, потому что неудобно, слишком жесткая провязка. Однако есть, вот Microsoft Silverlight устроена по этой технологии, и говорят: хорошо. Не знаю, не пробовала.
Почему же так вышло, что кроме MVP и MVVM все-таки возникло что-то еще, всем вам знакомое по слову redux, зачем возникли однонаправленные потоки данных.
Смотрим на правую картинку. У нас с MVP регулярно такая проблема. Допустим, у нас сложная система, не одни к одному, − много вьюшек, много моделей. Они все взаимосвязаны. Вьюшка сверху, желтенькая, поменяла модель. Модель поменяла другую модель. Поменялась нижняя желтая вьюшка. Нижняя вьюшка тоже поменяла модель. Все они дружно поменяли центральную красную вьюшку, и в ней происходит что-то не понятное.
С таким столкнулся Facebook, когда у них постоянно возникал баг из-за всплывающих непрочитанных сообщений. То есть пользователь видит «У вас непрочитанное сообщение», открывает, а его нет. Потому что две вьюшки вместе исправили состояние вот этой… В общем, состояние вьюшки было исправлено из двух разных источников, и кто прав не понятно. Они это правили, баг опять возникал, они опять правили, баг опять возникал. В конце концов, им надоело, и они решили решить проблему радикально, извините за тавтологию, и просто сделать так, чтобы состояние вьюшки было детерминированным.
Проблема MVP именно в недетерминированности состояния системы. Мы не всегда можем предсказать, в каком она сейчас находится состоянии, и кто там первый пришел, кто что исправил. Flux эту проблему решал, что называется, генетически. У него такого быть не может. Мне тут долго говорили, что идея с однонаправленным потоком данных витала в воздухе, это правда. И эту концепцию придумали, конечно, задолго до Facebook, задолго до 2013 года, когда они это опубликовали. Но они, что называется, запатентовали, первые выпустили spreadshit, что мы придумали вот такую штуку, пользуйтесь.
Давайте рассмотрим Flux поподробнее. Идея тут вот в чем. У нас есть Store, и этот Store — хранилище данных, это единственный источник истины нашего приложения. Все остальное неправда. Как он работает. Сначала у нас, если посмотреть именно на цикл работы, он, обычно, начинается с того, что пользователь что-то сделал, то есть работает вьюшка. Вьюшка создает Action. Обратите внимание, что Action без заливки на картинке. Почему так? Потому что это структура. Это не класс, не объект, это не что-то умное. Это структура. В вебе, в JavaScript мы можем писать ее, она как раз тем самым абстрактным объектом.
Вьюшка структуру создает, передает на блок-диспетчер. Блок-диспетчер триггерит callback. То есть он говорит: «Вызови функцию, которую мне сказали вызвать, когда случится Action. Сказал вызвать Store». То есть вызывается метод Store из диспетчера. Метод вызывается. Метод вызывается, получается на Store. Store смотрит, что ему пришло, изменяет как-то сам себя. Он меняет свое состояние. И он единственный, кто может менять свое состояние. Никто другой этого не делает. То есть он является единственным источником правды. После чего броадкастит всем завязанным на него вьюшкам, всем завязанным на него компонентам: «Я изменился, сходи за данными».
Вьюшки ходят за данными, и дальше начинается интересный момент. В классическом Flux, в таком, в каком он представлен в Facebook, вьюшка перерисовывается полностью.
Вот наша формочка с лейблом и кнопочкой. Как она работает? Смотрим пункт ноль. Пункт ноль здесь тоже есть. Он — синяя стрелка в самом низу, регистрация callback. Сначала происходит вот что.
Store в диспетчере вызывает: «Зарегистрируй, пожалуйста, мой callback, что я буду делать, когда на тебя придет Action». Произошло. После чего можем работать с приложением. Мы нажали кнопочку, создали структуру. Обратите внимание, что у Action, кроме данных, которые ввел пользователь, например, Вася, у него есть еще метаданные, тип. Очень важный момент, что Action сам передает, что он за Action, а диспетчеру все равно. Он все Action broadcast кидает. Первая стрелочка, вызывается метод.
Диспетчер вызывает метод, по сути, триггер Action и передает туда этот самый Action. На триггер Action происходит вызов callback, который мы зарегистрировали в пункте ноль. Вот красная стрелочка, это вызов callback с обратного вызова. Store берет эти данные, смотрит, что, ага, тип change name, значит, я меняю себя в поле name на Вася, и отправляю его на back-end, и как-нибудь валидируется, наверно, в общем, Store знает, что делать. Дальше фиолетовая стрелочка брэдкастится событие change. Мы поменялись. Все знают, что у нас изменился Store.
Дальше маленькая особенность классического Flux, который, возможно, незнакомым окажется неожиданным для тех, кто работал с Redux, точнее, даже с React, а не с Redux. Вьюшки идут за данными. Они идут в Store и говорят: «Мне вот это поле, вот это поле и вот это поле». Мы привыкли к тому, что, наоборот, к вьюшкам все приходит, если работали с React, Redux или чем-то таким. И шестой пункт, полная перерисовка.
Давайте посмотрим на эту схему и найдем узкое место, из-за чего? Перерисовка. Полная перерисовка, именно поэтому Flux активно начал использоваться после 2013 года, когда возникло что? Что позволяло это сделать? Виртуальный дом. Виртуальный дом, который позволяет перерисовывать только тогда, когда это, действительно, надо.
Отойдем немножко в сторону и расскажем про React, который вот так, очень удачно совместился с Flux, сделал тот мир, который мы знаем сейчас, когда именно эта технология наиболее популярна.
Тот же самый 2013 год, тот же самый 2013 год, тот же самый Facebook. Изначально React придумывался вообще, как замета вьюшек в MVC, MVP, вариации. И его там, действительно, можно использовать. В чем его мощность. Во-первых, виртуальный дом, как правильно сказали, позволяет не перерисовывать реальный дом, потому что это очень тяжелая операция, а перерисовывать виртуальный. И только если, действительно, было изменение, мы перерисовываем компонент, в результате чего все работает гораздо быстрее, чем могло бы быть.
И — чистые иммутабельные компоненты. Это механизм properties. Реализация тоже реактовская, позволяет создавать компоненты, у которых нет собственного состояния. И если писать в этой архитектуре, то очень правильно создавать компоненты чистыми, без стейта, без состояния. У них есть только данные, которые пришли от Store, и он их отрисовывает. Их удобно тестировать, они очень редко ломаются. То, что статично, ломать довольно сложно, а тестировать — легко.
Приложения в сочетании с Flux-архитектурой получаются мощные. Наверно, многим известно, что это действительно мощная штука. В чем некоторая важность, которую обязательно надо упомянуть? Кроме React Redux существует масса других связок. И, наверно, вам известно, что есть второй Angular. Это тоже сочетание реактивного фреймворка и Flux-архитектуры. Есть Vue, есть другие реализации Flux кроме Redux — Fluxxor, MobX и т. д. Не надо зацикливаться на React Redux. Тот же Vue, например, более удобен для создания маленьких приложений, чем React Redux. Он гораздо более легковесный.
Как выбирать между всем этим многообразием? Казалось бы, сейчас только React Redux и все хорошо. Ну Vue, ладно. На самом деле — нет. Если у вас есть простой сайт со статичными страницами или очень простым клиентским вводом — гораздо проще быстро запустить MVC-фреймворк. Потому что у вас наверняка есть админка с кучей данных и отображение. И никакое взаимодействие вам не требуется. На каком-нибудь React Redux вы убьетесь это писать.
MVP/MVVM-фреймворки тоже имеют свою нишу. Она сейчас редкая, потому что приложения нужны чаще — многостраничные, с динамическими, но достаточно простыми данными. Не single page application, а multiple page application. Какие-то данные от пользователя все-таки приходят. Например, так было бы удобно делать простые вики-страницы, без какого-то суперсложного форматирования и интерактивности. Простенькие, на MVP, было бы делать достаточно удобно.
Сейчас самый частый для нас кейс — single page application и сложная логика, много взаимодействия между компонентами, всякие умные вводы и т. д. Это Flux с виртуальным домом React Redux, View, Angular, MobX, Fluxxor и т. д.
Заключение. Литературка.
Про MVC, MVP, MVVM можно почитать много всего. Понятно, что в первую очередь есть документация к соответствующим программным приложениям. Про Flux в интернете есть много объяснений. Почитайте, возможно, будет понятнее. Наверно, самая интересная строка — последняя. Она вообще про все. JavaScript. Шаблоны. Если вдруг вам придется жить в мире ES5, то в первой книжке «JavaScript. Шаблоны» вы найдете очень много про то, как удобно строить архитектуру без всех вот этих ES6-возможностей — которые, конечно, есть, но иногда приходится жить без них.
Эрик Фримен, «Паттерны проектирования». Очень полезная книжка. Там примеры на Java, но это не должно вас пугать. Многое из того, что там написано, использовано и во Flux, как вы потом заметите. А вот это вот использовано в MVP, а вот это — там-то. А вот такой паттерн я могу использовать в этом блоке, который рисует у меня картиночки на экране. Очень полезная книжка и легко читается.
Та самая книжка, Дэвид Герлан и Мэри Шоу, «Введение в архитектуру ПО». Она, конечно, устарелая, но, что называется, надежная. И очень рекомендую ту самую статью Эдсгера Дейкстры «GOTO operator considered harmful». Это как арифметика. Наверно, без нее никуда.
Домашнее задание. Оно будет интересным. Мы напишем свой фреймворк. Я думаю, любой более-менее опытный программист скажет, что рано или поздно эта мысль ему приходила в голову. Вам будет предложено написать, как минимум, маленький Flux, ту самую историю с лейблом, кнопкой и input на Flux. Реализацию серверных компонент писать не надо — достаточно просто в консоль вывести, что мы сделали вид, будто отправили что-то на сервер. Реализацию полной перерисовки экрана виртуального дома тоже писать не обязательно. Можно перерисовать только кусочек и сделать вид, что компонента перерисовалась полностью. Вопросы, пожелания можно писать сюда. Большое спасибо.
— Добрый вечер. Меня зовут Аня Карпелевич. Мы сегодня с вами будем говорить про архитектуру фронтенда верхнего уровня.
Я работаю в Директе. Мы делаем интерфейсы для рекламодателей. Они подают объявления, настраивают их. Это очень сложная, интересная система, в ней много взаимосвязанных компонент, они прорастают друг в друга, у них есть общие и свои функциональности. «Брюки превращаются в элегантные шорты». Все это приходится очень тщательно контролировать. И архитектура в наших приложениях очень сложная. Это одна из причин, почему сегодня эту лекцию читаю я. Я очень люблю эту тему.
Что такое архитектура? Дело в том, что ответа на этот вопрос, наверно, нет. Или есть, но у каждого свой. Это очень спорная тема. Она вызывает массу споров, массу холиваров. И много из того, что сегодня я буду рассказывать, — мое мнение. Частично его поддерживает моя рабочая группа, частично — не очень. И каждый, когда он пишет архитектуру своего приложения, решает для себя, как и что делать.
Именно поэтому архитектура — одно из самых, наверно, творческих мест в работе программиста. И поэтому сегодняшняя наша презентация тоже начнется с творчества.
Давайте посмотрим на левую картинку. Я буду очень рада, если кто-нибудь узнает здание, которое на ней изображено. Это церковь Сен-Сюльпис в Париже. Обратите внимание на башенки, ради них эту церковь сюда и поставили. Надеюсь, видно, что они разные. Довольно сильно разные, и тому есть интересная причина. Между ними 130 лет разницы. Потом левую башню снесли и перестроили во время Франко-прусской войны.
Почему она здесь? Посмотрите на эту картинку. У башен одинаковая архитектура, а все окружение, вот эти виньеточки, финтифлюшечки, арочные конструкции — разные. Почему так? Потому что назначение у этих башен одинаковое. Ни одна из них, например, колокольней не была. Это просто башни. В них что-то хранилось, а все остальное — разное. Почему? Потому что архитектура у этих башен одинаковая. У обоих есть свод, всего одно окно, и оно стрельчатое. Высота окон примерно одинаковая. И идея в том, что архитектура, как здания, так и приложения, — опорная конструкция. Это не виньеточки, не финтифлюшечки, не реализация. Это то, что стоит в основе. И эта основа, как правило, зависит от окружающей среды, от почвы, если речь идет о здании, от цели, которую ставит перед собой архитектор, но почти никогда не зависит от дизайнерских изысков.
Пример со зданием для темы про архитектуру достаточно очевиден. А вот правая картинка более интересная. «Архитектура — это онемевшая музыка». «Architektur ist gefrorene Musik», — сказал в XVIII веке Иоганн Вольфганг Гете. Гете, скорее всего, ничего не знал про архитектуру зданий, он был поэтом. И он гарантированно ничего не знал про архитектуру приложений. Но высказал очень ценную и интересную мысль.
Музыка существует в динамике. Это не что-то статичное. Это процесс. И точно так же приложение является процессом. У него есть момент запуска, у него есть момент развития, когда мы с ним что-то делаем, работаем. И у него, наконец, есть момент завершения. Архитектура приложения — его срез в любой конкретный момент времени. В любой момент наше приложение, как музыкальная тема, должно быть четким, ясным, понятным, предсказуемым и т. д. Иначе все развалится.
На этом с творческим вступлением мы заканчиваем, переходим к вещам более приземленным, более близким к практике построения приложений.
Что такое архитектура и зачем она нужна?
Во-первых, у нас бывает организация большого объема кодов, то, с чем мы в Директе — и не только в Директе — сталкиваемся постоянно. Кода так много, что в нем можно потеряться. Мы не хотим теряться в коде.
Во-вторых, дублирование функциональности. Тоже вечная проблема, с которой вы всегда будете встречаться, и сегодня эта тема про дублирование пройдет прямо красной линией через всю лекцию. Одна и та же функциональность может быть нам нужна в нескольких местах интерфейса. Если она нужна в нескольких местах, значит, это должен быть физически один и тот же код, который используется в нескольких местах, а не копии. Почему? Мы про это дальше поговорим. Но архитектура нам должна помогать избежать копипаста.
Третье — поддержка. Достаточно очевидно, что если у нас есть приложение, то его нужно как-то поддерживать, и желательно, чтобы на это не тратились все ресурсы команды.
Изменение состава команды. Тоже такая вещь, с которой мы встречаемся в реальной жизни чаще, чем нам бы хотелось. Кто-то приходит, кто-то уходит, и если человек тратит полгода на то, чтобы въехать в код, это плохо. Если знания о коде хранятся только в одной голове, и он будет полгода передавать эти знания в случае ухода, это еще хуже. В общем, здесь архитектура нам тоже помогает сделать все это более понятным и поддерживать шаринг знаний.
Добавление и расширение функциональности. Тоже достаточно очевидная вещь. Прибегает к нам менеджер и говорит, что срочно надо вот это вот. И если для того, чтобы сделать это срочно, придется потратить уйму сил и времени, то это плохое архитектурное решение. А нам надо хорошее.
И, наконец, ошибки. Чем понятнее, предсказуемей наша архитектура, тем легче искать ошибки, тем меньше багов.
Как это все можно обозвать? Это все можно обозвать — проблемы сложной системы. Приложение — это сложная система, архитектура помогает нам решать проблему.
Короче, как-то так. Вот от меня справа картинка с лапшой, и это то, что бывает, если за архитектурой не следить, если ее не выстраивать, не продумывать и не проектировать. А вторая картинка — это то, что бывает, если архитектуру все-таки хоть как-то продумать. Это еще не Сен-Сюльпис, но хотя бы детский конструктор, он крепко стоит и не разваливается. В конструктор мы сегодня тоже будем много играться.
Формально обо всем этом. Архитектура — это способ решения проблем сложной системы путем абстракции реализации от интерфейса и разграничения полномочий между блоками кода. Дальше вот эту длинную фразу мы будем подробно разбирать.
В чем особенности архитектуры приложения как области знания? У нее есть конкретная область, с которой мы работаем. То есть это не что-то отвлеченное, это очень конкретная вещь. Вот стоит задача, мы под нее подбираем архитектуру, а не так, что, у-у-у, интересный архитектурный подход, надо попробовать. Так вот, нет. Попробовать можно на чем-то маленьком, а для серьезного проекта архитектура подбирается, иногда сочиняется под конкретный проект.
История вопроса, когда, вообще, возникла эта мысль, что нужно делать архитектуру. Эту, надо сказать, в свое время очень неординарную мысль высказал в 1968 году Эдсгер Дейкстра, замечательный программист. Он больше, наверно, известен как автор алгоритма Дейкстра, поиск самого короткого пути в графе. Но у него масса, на самом деле, прорывных для своего времени идей. И одна из них — это статья, я вам дам потом ссылочку на материалы, можете прочитать, там всего два листа, коротенькое эссе. Звучит оно как «Operator GOTO considered harmful», в переводе «Оператор GOTO — оператор безусловного перехода — зло». Это была первая мысль, что давайте официально скажем, что надо писать архитектуру, а не лапшу.
В 70-х годах эта идея развивалась уже Дейкстра в соавторстве с Парнасом, и сами по себе, по отдельности. Первая книга подробная об архитектуре приложений в целом была написана в 1996 году Мэри Шоу и Дэвид Гэрлан. После этого, на самом деле, подробных таких книг об архитектуре программного обеспечения не писалось именно из-за области применения, что в каждой сфере знаний есть свои архитектурные подходы, где-то одно, где-то более популярно другое, что-то, вообще, не применимо в каких-то местах. И поскольку архитектура — процесс творческий, какие-то конкретные книги про как писать архитектуру, вы не найдете. Может быть, после 1996 года ничего такого особо подробного на эту тему и не было.
Какие требования предъявляются к архитектуре проекта сейчас. Во-первых, и самое главное, что от него требуется, на самом деле, это расширяемость, потому что если ваш проект не расширяется, он мертв.
Переиспользование кода. Это про ту самую копипасту. Если у вас есть два блока, которые используются в двух разных местах, нужна одна и та же функциональность, значит, нужно переиспользовать один и тот же код, и архитектура должна быть такой, что любой кусочек кода можно взять и переиспользовать, как только это понадобится.
Разделение полномочий между модулями кода. Про это мы тоже сегодня поговорим поподробнее, зачем это надо. Идея такова: каждый модуль, каждый блок, каждый кусочек кода должен выполнять одно свое конкретное действие, нести ровно одну функцию. И эта функция должна быть вынесена в заголовок этого метода, класса, чем бы это ни было, модуля. Один модуль — одна функция.
И, наконец, качество приложений. Тут много вещей, которые хотелось бы сделать, − и безотказность, и обратная совместимость. В реальности опять же, выбирается под задачу. Где-то нужна обратная совместимость так, чтобы ни в коем случае ничего не отъезжало. Где-то нужна надежность, чтобы, не дай Бог, пароли, пин-коды карточек или CVV не утекли никуда. Где-то нужно, чтобы это было безотказно, если это спутник или еще что-то. В общем, выберете любые несколько. Чем больше вы захотите поддержать, тем больше сложностей в архитектуре вы, скорее всего, встретите.
Дальше мы будем с вами говорить про некоторые определения, именно такие энциклопедические вещи. Почему это важно? Потому что терминология в архитектуре очень важна, и нам нужно с вами говорить на одном языке. Определения в массе своей взяты из парадигмы программирования под названием ООП. Но на самом деле они проросли в другие парадигмы, с терминами «класс, объект, интерфейс» оперируют не только в рамках ООП. Однако определения эти и понимание взяты именно из мира ООП.
Самая простая вещь — это класс. Что такое класс? Это шаблон, это образец. Вот, например, класс Змея — class Snake. У нее мы определили три приватных поля, то есть поле, которое недоступно никому, кроме методов самого класса, − количество голов, количество хвостов и длина в попугаях. Мы определили конструктор, в котором мы ставим эти самые головы, хвосты и длину в попугаях. Получили класс Змея. Все просто.
Едем дальше. Объект. А объект — это экземпляр конкретной структуры. Причем, опять же в классическом ООП подразумевается, что объект это объект класса. В современном мире в JavaScript, который не всегда был ООП-языком, да и сейчас не всегда и не везде ООП, мы знаем, что могут быть абстрактные объекты. То есть мы сможем создать объект, литерал, который не будет объектом класса. Но здесь пример, как мы создаем объект класса Змея. Вот у нас двухвостая змея длиной в 38 попугаев, − удав.
Модуль. Модуль — это семантическая единица. Это не всегда класс. Это может быть набор классов, набор объектов, набор методов, не объединенных в классы. Обычно, можно считать, что модуль это то, что вы записали в один файл. Но, в принципе, модуль — это и папка, в которой они лежат, например, − файл и тесты к этому модулю, тоже модуль. Здесь важно то, что модуль — это то, что вы назвали модулем, то, что вы считаете единицей семантики. В данном случае модуль про то, как мы едим змей. Результатом работы этого модуля является последний метод, eatSnake, как мы съели змей. Не знаю, зачем мы едим змей, но мы это умеем делать, потому что мы так написали этот модуль.
Это было тривиально, дальше начнется несколько более интересная вещь. Интерфейс класса. Интерфейс класса — это, проще говоря, его публичные методы, то, чем он торчит наружу, то, что мы можем получить от объекта этого класса от объекта извне. Вот этот класс реализует интерфейс getSnakeLength. Он может нам вернуть длину змеи. Обратите внимание, что доступа извне к приватным полям нет. Доступ извне есть только к публичному методу getSnakeLength.
А вот дальше очень интересная вещь. Мы долго спорили, как эту штуку назвать, потому что термин «абстрактный интерфейс» я придумала, когда создавала эту лекцию. И, честно говоря, я нигде не видела нормального определения этого подхода и метода. Тем не менее, многие языки программирования позволяют создавать абстрактные интерфейсы, и обзывают их, как только не абстрактными классами, и абстрактными интерфейсами тоже, просто интерфейсами. Получается омоним с интерфейсом класса. Идея такова, что абстрактный интерфейс — это набор методов, которые что-то делают. Когда мы создаем класс, мы идем от вопроса «что это?» Это змея и она что-то умеет делать, или не умеет. Она может просто отдавать свою длину.
А когда мы создаем интерфейс, мы идем от того, что он делает, что он должен уметь делать. И это оказывается очень мощным способом расширения классов. Мы можем приписывать классам какие-то возможности, расширяя его при помощи интерфейсов. Например, фреймворк I-BEM такую штуку умеет, встроена во фреймворк такая история с абстрактными интерфейсами. Многие фреймворки, к сожалению, не умеют, а штука мощная.
Вот в качестве примера мы создали интерфейс audiable, что-то, что умеет звучать. И определение у него — абстрактный пустой метод getNoise. Мы расширили нашу змею классом audiable, реализовали у нее метод getNoise, и наша змея зашипела. Вдохновение к этому сету примеров мне дала замечательная книжка Эрика Фримена и компании «Паттерны проектирования».
Сейчас мы попробуем эти примеры посмотреть немножко более конкретно.
Но сначала поговорим о том, зачем эти примеры были нужны. А нужны они были вот для этого большого слайда. То, что здесь написано, настолько важно, что я даже вынесла это в желтый титульник. Это, можно сказать, мантра. Это очень важный принцип, который вам нужно всегда про него думать, когда вы проектируете архитектуру. High cohesity, low coupling — сильное сцепление, слабая связность. Есть некая проблема с тем, что слово cohesity и слово coupling на русский и так, и так переводятся «связность», специально для этого принципа придумали слово сцепление.
Идея вот в чем. Ваши блоки должны быть очень компактны, очень сильно сцеплены. Они должны реализовывать ровно одну функцию. А между собой они должны быть связаны очень легко, чтобы их можно было легко комбинировать, собирать, как конструктор. И тогда ваша архитектура будет достаточно гибкой и достаточно надежной. А еще легко тестируемой.
Давайте посмотрим, как нам добиться сильного сцепления и слабой связанности по пунктам, что называется.
Специализация. Каждый блок решает только одну задачу. Вот у нас хорошая иллюстрация — детский конструктор. У нас каждый блок, или набор блоков. Они все своей формы, своего размера. И если нам нужно построить дом, мы возьмем длинные брусочки. Если нам нужно построить шарик, мы возьмем короткие брусочки. У каждого брусочка есть своя функция. И те, кто играли в конструкторы, знают: чем проще форма кусочков, тем больше из него можно построить. Из вот таких загогулин ничего не построится, или строится только то, что описано в инструкции. А кому оно надо?
То же самое, абстракция. Это про то, что абстракция интерфейса от реализации. Идея в том, что интерфейс внешний, то, как это наш класс, наш блок торчит наружу, то, как он взаимодействует с другими блоками, не должно влиять на его внутреннюю реализацию. Наоборот — бывает. В другую сторону — никогда. В хорошей архитектуре. Здесь в качестве примера — формирование вот этих пупырышков не влияет на форму самого блочка. Мы выбираем отдельно форму блока и уже на него пупырышки наклеиваем.
Инкапсуляция. Продолжение предыдущей темы. В приватных методах, то есть в том, что изнутри наших блоков, мы реализуем сам смысл нашего блока, реализацию. А интерфейс, то, как они связаны, находится в публичных. То есть, в данном случае, вот эти все крестики, черточки и сама форма, − это реализация. А пупырышки — это интерфейс. И хорошая архитектура выглядит как такой конструктор.
О, какой страшный монстр. Это про переиспользование кода. Изначально этот монстр, вообще-то, был для того, чтобы показать пример плохой архитектуры, но вот посмотрите на него внимательно. Он прекрасен. Более того, он явно доволен жизнью, вполне бодро бегает на своих странных ногах. Возможно, даже умеет летать, или, как минимум, у него красивые крылья от бабочки.
В чем идея? Если у вас есть реализация для верблюда и реализация для крокодила, и к вам приходит менеджер, и говорит, что срочно нужен верблюдо-крокодил. Вы не пишете отдельно верблюдо-крокодила. Вы берете тело верблюда, отделяете его от всей реализации верблюда. Берете голову крокодила, отделяете ее от крокодила, и переиспользуете блоки. Зачем это надо?
Затем, что когда менеджер снова к вам прибежит и скажет, что срочно мы расширяемся на Южную Америку, а там аллигаторы, нам нужно поддержать неправильную форму челюсти, или что там, у крокодила, четвертый зуб не такой, вы не будете шарить по всему проекту, где же у вас скопированы головы крокодилов. Потому что у вас там может быть рядом еще какой-нибудь зебро-бизоно-крокодил. Вы просто возьмете свой класс голова крокодила, сделаете у него расширение из серии голова аллигатора, передадите ему параметры, он будет сам определять, какие зубы ему рисовать. И все. В одном месте, а не во всех местах, где это использовано.
Здесь надежность повышается в разы, потому что вы гарантированно забудете какую-нибудь скопированную голову в каком-нибудь очень редком проекте. В общем, ничего страшного в таких кадаврах нет. Хороший кадавр, полезный.
Сейчас мы будем смотреть прямо-таки примеры плохого кода. Обратите внимание, это псевдокод. Псевдокод немножко похожий на TypeScript, но все равно псевдокод. Не пытайтесь это запустить, оно не работает. Повторить можно, запускать именно этот код не стоит, потому что здесь использованы синтаксические конструкции, которые TypeScript 2.7 не поддерживает, зато иллюстрации хорошие получились (сейчас существуют более актуальные примеры — прим. ред.).
Итак, у нас есть класс User. У него есть имя и возраст. Все хорошо. У нас есть User с фамилией, прошу прощения за разъехавшиеся шрифты. User с фамилией, у него есть имя, возраст и фамилия.
И у нас есть метод printLabel. Мы передаем ему User. Дальше смотрим, если у нас User класса User, мы рисуем имя и возраст. Если User класса User с фамилией, то имя, фамилия и возраст. Давайте все-таки попробуем посмотреть, что здесь плохо.
Дублирование кода, еще? Много разного дублирования кода, хорошо. Да, хорошо. Тут два дублирования кода, − одно про то, что мы дублируем UserWithSurname, второе, что мы дублируемся в методе printLabel. Еще что есть? Правильно, да, это все про то, что у нас много дублирования кода, потенциально еще больше. Что-нибудь еще тут есть? Наследование тоже здесь есть, и это тоже один из вариантов. Тут есть две проблемы, − нет переиспользования, нет специализации. Мы еще про две вещи говорили. PrintLabel лезет в приватные методы. Еще? Четвертого чего здесь не хватает? Да, все так.
Специализации нет, два блока делают одно и то же. Абстракции нет, у нас интерфейс и реализация смешаны. Инкапсуляции нет, действительно, доступ к приватным методам. Переиспользование кода, про бесконечные if, которых может стать очень много, сказали очень правильно. Давайте посмотрим, как это сделать получше, а так не будем.
Мы создадим интерфейс printLabel, это не потому, что iPrintLabel это не потому, что iPhone, а потому что интерфейс. И у него определим один-единственный абстрактный метод getText. Создадим класс User, который имплементирует iPrintLabel. У него появятся, действительно, приватные поля имя и возраст, и один-единственный публичный метод, тот самый getText из iPrintLabel, в котором мы уже честно обратимся из класса к его приватным полям, это разрешено и даже поощряется. UserWithSurname, действительно унаследуем от класса User, и нам нужно будет здесь только доопределить Surname и переопределить getText. А вот printLabel станет очень простым. Он станет принимать iPrintLabel и просто выводить getText.
Прелесть тут в том, что если абстракция появляется, интерфейс отдельно, реализация отдельно. Инкапсуляция появляется. Специализация, пожалуйста, мы сделали наследование для этого. И с переиспользованием кода все, вообще, прекрасно, потому что мы можем печатать что угодно, главное, расширить его интерфейс iPrintLabel, и мы можем не думать, напечатается оно, не напечатается, − напечатается. Метод printLabel мы больше трогать не будем. Вот такой хороший, очень простой короткий способ улучшить архитектуру за несколько лишних строчек кода.
На этом месте мы заканчиваем с теорией. С теорией всего, потому что то, что мы сейчас описывали, оно верно не только для front-end, для всего, вообще. И переходим к архитектурным подходам и отдельно к архитектурным подходам, которые применяются во front-end и полезны, используются, встречаются.
Как устроена среднестатистическое веб-приложение? Есть сервер. Внутри сервера реализована какая-то архитектура 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-технологиям.
Как они устроены? Идея в том, что у нас есть модель, которая хранит данные. Их, как правило, много. Есть какой-то View, который показывает эти данные пользователю. Их тоже, как правило, много. И некий третий компонент, который между ними посредник, провязывает данные и отображение. Вот пользователь в правом верхнем углу со всем этим работает.
MVC, то, с чего все началось, это далекий 1980 год, Smalltalk. Но именно в таком виде он существует в некоторых фреймворках до сих пор. Не в некоторых, довольно во многих. В чем идея? Пользователь работает напрямую с вьюшкой и контроллером. Он вводит данные в какие-то поля во вьюшке, нажимает кнопку отправки и данные попадают на контроллер. Это отправка формы. Честная такая отправка формы по кнопке submit, всем знакомая давно, я надеюсь.
Смотрим. Желтая стрелочка от пользователя к контроллеру — это пользователь передал данные на контроллер по кнопке submit. Зеленая стрелочка, − туда же перешло управление. Контроллер смотрит на эти данные. Возможно, он их как-то обрабатывает, здесь уже тонкости реализации, и отправляет их на нужную модель. Контроллер сам выбирает, на какую модель отправить. Отправляет зеленой стрелочкой управления, отправляет желтой стрелочкой данные.
Модель тоже обрабатывает данные. Возможно, она их валидирует. Возможно, она их кладет в базу. Короче, модель знает, что с ними сделать. Как правило, в результате получаются новые данные. Например, мы можем сообщить пользователю, залогинился он или нет, а модель проверяла пароль с логином. После чего, модель передает управление на контроллер опять, чтобы контроллер выбрал, какую вьюшку отобразить. А данные идут непосредственно во View. Как можно такое сделать, вообще, как может модель данные во вьюшку отправить?
Очень просто. Если контроллер и модель находятся в 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 фреймворк.
Model-View-Presenter. Его схема уже гораздо более простая. Есть модели. Они между собой взаимодействуют. Отдают данные на Presenter, Presenter передает их во вьюшку, показывает пользователю. И обратно. Пользователь вбивает что-то во вьюшку, нажимает кнопку, Presenter это смотрит, AJAX отправляет на модель или кладет в модель, а модель AJAX отправляет на сервер. То есть здесь уже все гораздо боле просто и линейно, но без серверной шаблонизации здесь уже будут сложности. Если вы хотите серверную, вот такая система будет сложновата.
Давайте сравним. Посмотрим на первую картинку, где мы попытаемся реализовать очень простую вещь — отправку данных из input в модель. Мы что-то ввели, нажали кнопочку, оно должно в модели появиться, модель с этим что-то сделает и скажет нам, что что-то произошло. Мы вбили: «меня зовут Вася», нажали о’кей. Если мы хотим клиентскую валидацию, то она происходит вот здесь, чуть ли не перехватом, в особо тяжелых случаях, действительно, так, перехватом клика через event.preventDefault(). И где-то пунктом ноль сбоку прикручена клиентская валидация.
Потом честно отправляем через submit формы данные на контроллер. Данные уходят в модель, модель их в себя кладет, обрабатывает, смотрит. Говорит нам, что, хорошо, данные приняты, ты, действительно, Вася. Третья стрелочка — управление уходит на контроллер, модель сообщает контроллеру, что, отобрази, пожалуйста, лейбл «меня зовут Вася». Контроллер выбирает соответствующую вьюшку, отображает лейбл. А данные «меня зовут Вася», четвертая стрелочка, желтая, туда кладет модель. Вопрос, как это тестировать? Только snapshot. По-другому никак. Тут не на что даже функциональные тесты написать.
Второй вариант, уже с MVP. Мы вбили «меня зовут Вася», нажали о’кей. Стрелочка под номер один, зелененькая, − управление ушло на Presenter. Presenter сказали: кнопка нажата. Presenter смотрит, стрелочка номер два, синенькая, обратите внимание, это запрос данных. В классическом MVP не отправка данных от вьюшки на Presenter, а запрос с Presenter за данными. Это гораздо чище, потому что Presenter может уже заранее знать, например, что данные ему не нужны, все равно все плохо.
Дальше третьим пунктом на Presenter честная JS-валидация. Мы ее можем уже спокойно писать, это для нее специально место выделено. Четвертая стрелочка — данные уходят на модель, модель их, допустим, положила в базу, сказала: «Все в порядке, я положила». Пятая стрелочка, видите, она полосатенькая, надеюсь, это видно, что она полосатенькая желто-зеленая, − и управление, и данные пришли обратно на Presenter. Модель сказала «Я положила», Presenter сам понял, что раз данные положили в базу, значит, надо отобразить, что все в порядке, данные положены. И шестая стрелочка, − отправили это на вьюшку, возможно, уже на другую, но тут я не стала вторую вьюшку рисовать.
В чем у нас тут плюс. JS-валидация встала на свое законное место и с ней стало все хорошо, AJAX тоже встал на место, это может быть четвертая стрелка, например, если модель находится на сервере, или модель сама AJAX сама идет на сервер. И, наконец, мы можем спокойно тестировать Presenter, писать на него функциональные тесты.
Во-вторых, что мы еще получили в плюсе, кроме упрощенного тестирования? Мы получили разделение визуального отображения и его работы. То есть мы все еще можем написать snapshot на View, и мы отдельно можем написать тесты на Presenter. Мы можем исправить Presenter и не трогать View, и наоборот. У нас улучшилась специализация. Так устроены фреймворки типа Angular1, Backbone, Ember, Knockout ранних версий. Когда-то их было очень много, прямо яростная конкуренция.
В чем особенности. Presenter кладется уже на клиент, модель может быть и там, и там single page application спокойно делаются. Бывает лучше, но так сделано на этой истории много single page application, или, как минимум, было сделано раньше. Взаимодействие с сервером по AJAX хорошее. Клиентская валидация на месте. Казалось бы, все хорошо, зачем думать дальше?
Однако был придуман, как минимум, MVVM. Тоже интересная вещь.
По сути, это реализация Presenter средствами фреймворка. Очень часто оказывалось, когда ты написал первый Presenter, второй Presenter, пятый Presenter, что они все одинаковые. И они просто провязывают вьюшку и модель. Как видите, он устроен подобно MVP.
И поэтому многие фреймворки просто решили эти задачи, binding. В чем плюсы? Нам не надо писать лишний код. И это реально ускоряет скорость разработки. В чем минусы. Усиливается связность между Model и ViewModel.
То есть там возникают проблемы именно из-за сильной связанности, поэтому иногда бывает, что MVVM не используется. Например, я лично знакома с MVVM во фреймворке i-BEM, который мы иногда используем, а иногда не используем, потому что неудобно, слишком жесткая провязка. Однако есть, вот Microsoft Silverlight устроена по этой технологии, и говорят: хорошо. Не знаю, не пробовала.
Почему же так вышло, что кроме MVP и MVVM все-таки возникло что-то еще, всем вам знакомое по слову redux, зачем возникли однонаправленные потоки данных.
Смотрим на правую картинку. У нас с MVP регулярно такая проблема. Допустим, у нас сложная система, не одни к одному, − много вьюшек, много моделей. Они все взаимосвязаны. Вьюшка сверху, желтенькая, поменяла модель. Модель поменяла другую модель. Поменялась нижняя желтая вьюшка. Нижняя вьюшка тоже поменяла модель. Все они дружно поменяли центральную красную вьюшку, и в ней происходит что-то не понятное.
С таким столкнулся Facebook, когда у них постоянно возникал баг из-за всплывающих непрочитанных сообщений. То есть пользователь видит «У вас непрочитанное сообщение», открывает, а его нет. Потому что две вьюшки вместе исправили состояние вот этой… В общем, состояние вьюшки было исправлено из двух разных источников, и кто прав не понятно. Они это правили, баг опять возникал, они опять правили, баг опять возникал. В конце концов, им надоело, и они решили решить проблему радикально, извините за тавтологию, и просто сделать так, чтобы состояние вьюшки было детерминированным.
Проблема MVP именно в недетерминированности состояния системы. Мы не всегда можем предсказать, в каком она сейчас находится состоянии, и кто там первый пришел, кто что исправил. Flux эту проблему решал, что называется, генетически. У него такого быть не может. Мне тут долго говорили, что идея с однонаправленным потоком данных витала в воздухе, это правда. И эту концепцию придумали, конечно, задолго до Facebook, задолго до 2013 года, когда они это опубликовали. Но они, что называется, запатентовали, первые выпустили spreadshit, что мы придумали вот такую штуку, пользуйтесь.
Давайте рассмотрим Flux поподробнее. Идея тут вот в чем. У нас есть Store, и этот Store — хранилище данных, это единственный источник истины нашего приложения. Все остальное неправда. Как он работает. Сначала у нас, если посмотреть именно на цикл работы, он, обычно, начинается с того, что пользователь что-то сделал, то есть работает вьюшка. Вьюшка создает Action. Обратите внимание, что Action без заливки на картинке. Почему так? Потому что это структура. Это не класс, не объект, это не что-то умное. Это структура. В вебе, в JavaScript мы можем писать ее, она как раз тем самым абстрактным объектом.
Вьюшка структуру создает, передает на блок-диспетчер. Блок-диспетчер триггерит callback. То есть он говорит: «Вызови функцию, которую мне сказали вызвать, когда случится Action. Сказал вызвать Store». То есть вызывается метод Store из диспетчера. Метод вызывается. Метод вызывается, получается на Store. Store смотрит, что ему пришло, изменяет как-то сам себя. Он меняет свое состояние. И он единственный, кто может менять свое состояние. Никто другой этого не делает. То есть он является единственным источником правды. После чего броадкастит всем завязанным на него вьюшкам, всем завязанным на него компонентам: «Я изменился, сходи за данными».
Вьюшки ходят за данными, и дальше начинается интересный момент. В классическом Flux, в таком, в каком он представлен в Facebook, вьюшка перерисовывается полностью.
Вот наша формочка с лейблом и кнопочкой. Как она работает? Смотрим пункт ноль. Пункт ноль здесь тоже есть. Он — синяя стрелка в самом низу, регистрация callback. Сначала происходит вот что.
Store в диспетчере вызывает: «Зарегистрируй, пожалуйста, мой callback, что я буду делать, когда на тебя придет Action». Произошло. После чего можем работать с приложением. Мы нажали кнопочку, создали структуру. Обратите внимание, что у Action, кроме данных, которые ввел пользователь, например, Вася, у него есть еще метаданные, тип. Очень важный момент, что Action сам передает, что он за Action, а диспетчеру все равно. Он все Action broadcast кидает. Первая стрелочка, вызывается метод.
Диспетчер вызывает метод, по сути, триггер Action и передает туда этот самый Action. На триггер Action происходит вызов callback, который мы зарегистрировали в пункте ноль. Вот красная стрелочка, это вызов callback с обратного вызова. Store берет эти данные, смотрит, что, ага, тип change name, значит, я меняю себя в поле name на Вася, и отправляю его на back-end, и как-нибудь валидируется, наверно, в общем, Store знает, что делать. Дальше фиолетовая стрелочка брэдкастится событие change. Мы поменялись. Все знают, что у нас изменился Store.
Дальше маленькая особенность классического Flux, который, возможно, незнакомым окажется неожиданным для тех, кто работал с Redux, точнее, даже с React, а не с Redux. Вьюшки идут за данными. Они идут в Store и говорят: «Мне вот это поле, вот это поле и вот это поле». Мы привыкли к тому, что, наоборот, к вьюшкам все приходит, если работали с React, Redux или чем-то таким. И шестой пункт, полная перерисовка.
Давайте посмотрим на эту схему и найдем узкое место, из-за чего? Перерисовка. Полная перерисовка, именно поэтому Flux активно начал использоваться после 2013 года, когда возникло что? Что позволяло это сделать? Виртуальный дом. Виртуальный дом, который позволяет перерисовывать только тогда, когда это, действительно, надо.
Отойдем немножко в сторону и расскажем про React, который вот так, очень удачно совместился с Flux, сделал тот мир, который мы знаем сейчас, когда именно эта технология наиболее популярна.
Тот же самый 2013 год, тот же самый 2013 год, тот же самый Facebook. Изначально React придумывался вообще, как замета вьюшек в MVC, MVP, вариации. И его там, действительно, можно использовать. В чем его мощность. Во-первых, виртуальный дом, как правильно сказали, позволяет не перерисовывать реальный дом, потому что это очень тяжелая операция, а перерисовывать виртуальный. И только если, действительно, было изменение, мы перерисовываем компонент, в результате чего все работает гораздо быстрее, чем могло бы быть.
И — чистые иммутабельные компоненты. Это механизм properties. Реализация тоже реактовская, позволяет создавать компоненты, у которых нет собственного состояния. И если писать в этой архитектуре, то очень правильно создавать компоненты чистыми, без стейта, без состояния. У них есть только данные, которые пришли от Store, и он их отрисовывает. Их удобно тестировать, они очень редко ломаются. То, что статично, ломать довольно сложно, а тестировать — легко.
Приложения в сочетании с Flux-архитектурой получаются мощные. Наверно, многим известно, что это действительно мощная штука. В чем некоторая важность, которую обязательно надо упомянуть? Кроме React Redux существует масса других связок. И, наверно, вам известно, что есть второй Angular. Это тоже сочетание реактивного фреймворка и Flux-архитектуры. Есть Vue, есть другие реализации Flux кроме Redux — Fluxxor, MobX и т. д. Не надо зацикливаться на React Redux. Тот же Vue, например, более удобен для создания маленьких приложений, чем React Redux. Он гораздо более легковесный.
Как выбирать между всем этим многообразием? Казалось бы, сейчас только React Redux и все хорошо. Ну Vue, ладно. На самом деле — нет. Если у вас есть простой сайт со статичными страницами или очень простым клиентским вводом — гораздо проще быстро запустить MVC-фреймворк. Потому что у вас наверняка есть админка с кучей данных и отображение. И никакое взаимодействие вам не требуется. На каком-нибудь React Redux вы убьетесь это писать.
MVP/MVVM-фреймворки тоже имеют свою нишу. Она сейчас редкая, потому что приложения нужны чаще — многостраничные, с динамическими, но достаточно простыми данными. Не single page application, а multiple page application. Какие-то данные от пользователя все-таки приходят. Например, так было бы удобно делать простые вики-страницы, без какого-то суперсложного форматирования и интерактивности. Простенькие, на MVP, было бы делать достаточно удобно.
Сейчас самый частый для нас кейс — single page application и сложная логика, много взаимодействия между компонентами, всякие умные вводы и т. д. Это Flux с виртуальным домом React Redux, View, Angular, MobX, Fluxxor и т. д.
Заключение. Литературка.
> MVC: Smalltalk-80, General MVC, ASP.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. Шаблоны», Эрик Фримен и др., «Паттерны проектирования», D.Garlain, M.Shaw, ”An introduction to Software Architecture” (1994), E.Dijkstra ”GOTO statement considered harmful” (1968)
Про MVC, MVP, MVVM можно почитать много всего. Понятно, что в первую очередь есть документация к соответствующим программным приложениям. Про Flux в интернете есть много объяснений. Почитайте, возможно, будет понятнее. Наверно, самая интересная строка — последняя. Она вообще про все. JavaScript. Шаблоны. Если вдруг вам придется жить в мире ES5, то в первой книжке «JavaScript. Шаблоны» вы найдете очень много про то, как удобно строить архитектуру без всех вот этих ES6-возможностей — которые, конечно, есть, но иногда приходится жить без них.
Эрик Фримен, «Паттерны проектирования». Очень полезная книжка. Там примеры на Java, но это не должно вас пугать. Многое из того, что там написано, использовано и во Flux, как вы потом заметите. А вот это вот использовано в MVP, а вот это — там-то. А вот такой паттерн я могу использовать в этом блоке, который рисует у меня картиночки на экране. Очень полезная книжка и легко читается.
Та самая книжка, Дэвид Герлан и Мэри Шоу, «Введение в архитектуру ПО». Она, конечно, устарелая, но, что называется, надежная. И очень рекомендую ту самую статью Эдсгера Дейкстры «GOTO operator considered harmful». Это как арифметика. Наверно, без нее никуда.
Домашнее задание. Оно будет интересным. Мы напишем свой фреймворк. Я думаю, любой более-менее опытный программист скажет, что рано или поздно эта мысль ему приходила в голову. Вам будет предложено написать, как минимум, маленький Flux, ту самую историю с лейблом, кнопкой и input на Flux. Реализацию серверных компонент писать не надо — достаточно просто в консоль вывести, что мы сделали вид, будто отправили что-то на сервер. Реализацию полной перерисовки экрана виртуального дома тоже писать не обязательно. Можно перерисовать только кусочек и сделать вид, что компонента перерисовалась полностью. Вопросы, пожелания можно писать сюда. Большое спасибо.