Доброе утро! Сегодня вторник, а значит поговорим об ООП на фронте.
Пока я заливаю в себя кофе, давайте проведём опрос. Как вы думаете, ООП и фронтенд:
🤔
26.8%
Хорошие друзья 😊🤔
12.1%
Заклятые враги 😈🤔
61.1%
Беру попкорн 🍿А пока идёт голосование, обсудим, чем ООП плохо и хорошо, а что его не любят и наоборот.
Начнём с хейта 😃
Сразу начну с того, что не каждому проекту ООП нужно.
Иногда гораздо проще написать пару функций с объектами, и никаких солидов не надо. Об этом подробнее в конце 🙂
Во-вторых, насколько я вижу, ООП прочно ассоциируется с мутацией данных, а это сейчас немодно.
Я подозреваю откуда это взялось, хотя и не уверен до конца.
Я согласен, что иммутабельные структуры данных — это круто и надёжно. Но сам лично не считаю мутации чем-то плохим, если они контролируемы.
Когда мутации становятся беспорядочными, всё и правда резко становится сложным и запутанным.
Но беспорядочные мутации — это не проблема ООП, это проблема плохо написанного кода 😃
Запутанный код можно написать и не мутируя структуры, если data-flow непонятный.
Для управления мутациями давно уже придумали много способов работы, тот же CQS:
- bespoyasov.ru/blog/commands-…
- en.wikipedia.org/wiki/Command–q…
Command-query separation — подразумевает разделение кода на «запросы» и «команды». Запросы возвращают данные, а команды меняют состояние.
С ним, кстати, вполне реально выстроить как 1-way, так и 2-ways data-flow, он достаточно универсален, чтобы быть удобным в обоих случаях.
Так вот, тот ООП энтерпрайз, который следовал CQS, был вполне себе понятным ¯_(ツ)_/¯
Так что, возможно, дело не в мутациях.
Ну ок, что есть ещё? А! Куча лишних сущностей!
Обмажутся своим ООП и начинают городить фабрики фабрик провайдеров фабрик провайдеров.
Но опять, это не проблема самого ООП 😃
Да, среди принципов SOLID есть SRP, single responsibility principle. Он говорит, что надо делить обязательства между модулями.
ota-solid.now.sh/srp
Но я видел фабрики фабрик не только в ООП коде.
Может, дело просто в самой его сложности?
Ну… ООП сложный, но не сложнее настоящей™ функциональщины.
Теорию групп я так и не могу сказать, что осилил 😃
github.com/fantasyland/fa…
Хотя я даже работал на проекте, где это использовалось.
Очень оказалось полезно как раз для ООП. Проще стало понимать ковариантность и контравариантность типов.
- ru.wikipedia.org/wiki/Ковариант…
Мне кажется, весь хейт в том, что ООП на фронте с JS просто не удобен.
Использовать его по-настоящему не получается из-за JS. У него многого не хватает, банально — нет интерфейсов.
Но есть TypeScript 🙂
Он всё ещё страдает от JS-райнтайма, но у него уже достаточное API, чтобы писать нормальный™ код.
Мне вот сильно не хватало нормального DI (без декораторов!), пока я не наткнулся на:
- github.com/wessberg/di
Другое дело, что это опять-таки не всем проектам нужно 🙂
Парадигма программирования, как и архитектура, — это инструмент для укрощения сложности. И, как с архитектурой, нам стоит исследовать выгоды и издержки перед применением.
Взять те же принципы SOLID.
ota-solid.now.sh
Мы можем (хоть с оговоркой и не все) использовать их в отрыве от ООП, как инструмент проектирования.
Я так Тяжеловато переписал:
bespoyasov.ru/blog/tzlvt-upg…
В коде Тяжеловато даже классов нет 😃
Я вообще считаю, что ООП — это больше не про классы, а про отношения между сущностями.
Но классы — это всё ещё наиболее удобный инструмент для написания трушного™ ООП.
В простейших случаях можно обойтись и объектом с парой функций.
Но вот то, как эти объекты будут друг с другом взаимодействовать, в каких отношениях они будут находиться, проще спроектировать в терминах ООП ¯_(ツ)_/¯
Я причём не говорю, что интерфейсы и реализации — это строго лишь ООП, нет.
Просто как их использовать для проектирования и написания кода, описано обширнее всего в книгах, так или иначе связанных с ООП.
— Ладно, вот простой вопрос: можно ли вообще писать в ООП-стиле фронтовый код? Не бекенд, а фронт?
Да. (Но нужен TypeScript 😃)
Есть ошибочное мнение, что фронт — типа ненастоящее программирование.
Но современный фронт сложный ¯_(ツ)/¯
А сложное надо проектировать ¯_(ツ)/¯
Кому-то это может не нравиться, кто-то хочет, чтобы вся сложность снова ушла на сервера, но сейчас мы имеем что имеем.
Со сложностью надо как-то справляться.
У нас есть фреймворки и библиотеки, это хорошая помощь, но они решают только часть проблем. Проектированием системы всё равно надо заниматься нам самим.
И вот тут ООП может помочь:
- Как разделить обязанности между модулями,
- как использовать композицию,
- что должно быть полиморфным,
- где и какие ставить предусловия,
- что от чего должно зависеть.
Из всего, что я пробовал у ООП наиболее богатый инструментарий и словарь для проектирования систем ¯_(ツ)_/¯
Круто, если есть или будет что-то ещё более удобное, но я не видел.
Кстати, ООП не запрещает использовать преимущества ФП! 🙂
Мы можем продолжать использовать чистые функции, иммутабельные структуры и вот это всё, даже пиша в ООП-стиле. (Такое слово есть, я проверил 😃)
У Марка Симанна есть отличная статья на эту тему, очень советую:
blog.ploeh.dk/2020/03/02/imp…

Мне в целом воинствующее разделение на ООП / ФП не нравится. Для меня срачи на тему парадигмы выглядят вот так 😅

Но я немного отступил от темы. Можно ли писать нормальный ООП-код на фронте?
Я недавно написал пост о том, как совместить принципы чистой архитектуры, ООП, DDD и всё такое прочее:
bespoyasov.ru/blog/generatin…
Внутри ссылаюсь на офигенную статью @hgraca
herbertograca.com/2017/11/16/exp…
там настолько круто всё разжёвано!
Я перечитываю её разок в месяц-два, чтобы рефрешнуть в памяти, как нормально проектировать.
Для Реакта вон люди тоже придумали стартовые шаблоны:
- github.com/eduardomoroni/…
- github.com/bailabs/react_…
Но опять же, тащить огромную инфраструктуру в небольшой проект я не стану — научен горьким опытом 🙂
Сейчас я стараюсь приносить в проект инструменты по мере их необходимости.
Обычно всё начинается с домена, внутри которого лежит пара функций да типы. Применяю S, O, I, а L и D на полшишечки. Чем сложнее становится управлять, тем больше инструментов буду использовать.
Как и обещал, начнём с наброса 😃 Что такое чистая архитектура, зачем нужна, плюсы, издержки. Если вы работали c ytq, расскажите о своём опыте? Что было круто, что было неудобно? Будем разбираться, действительно ли это полезный инструмент, или просто переусложнённый хайп.
Именно поэтому мы вчера столько времени уделяли проектированию:
twitter.com/jsunderhood/st…
Мы не хотим засорять код с самого начала, но мы хотим быть в состоянии добавить и расширить функциональность и инструментарий при необходимости.
(В Тяжеловато, например, я так и сделал: у меня есть пространство для манёвра с новыми инструментами, но самих инструментов пока нет — они не нужны.
fuckgrechka.ru/tzlvt/)
Отследить, в какой момент пора наращивать инструментарий мне помогает ощущение «Чё-та сложна».
Иду по индукции с малого, если становится сложно (или там много писать, или повторяться приходится), добавляю инструменты.
Мне ещё иногда кажется, проблема и преимущество JS в том, что «нам не объяснили, как на нём писать правильно».
Поэтому, думаю, и выбор парадигмы — больше вопрос удобства, привычки и вкуса.
Так-с, ухожу работать!
Потом продолжим 🙂
Продолжим 🙂
Чем ООП полезен?
Мне очень нравится, как ООП помогает делать мой код масштабируемым и тестируемым.
Под масштабирумеостью я понимаю возможность дописать, переписать, удалить какой-то модуль без необходимости переписывать соседние модули.
Под тестируемостью — возможность удобно подменить зависимость при тесте на стаб или мок.
Пример.
Я как-то писал пост о DI:
bespoyasov.ru/blog/di-ts-in-…
В нём был логгер. Давайте рассмотрим, как он устроен.
В типах я описываю публичный интерфейс:
github.com/bespoyasov/di-…
Интерфейс — это контракт на поведение, он говорит, как с этим модулем можно общаться, что этот модуль гарантирует предоставить как API:

Реализация интерфейса описана классом:
github.com/bespoyasov/di-…
Реализация инкапсулирует в себе детали, которые внешнему миру не важны.
Всем потребителям публичного API по барабану, куда уходит сообщение. Им лишь важно, что они могут дёрнуть метод
log
.
Обратим внимание, что интерфейс называется более абстрактно, чем реализация.
Нам важно сохранять инкапсуляцию и в названии сущности, потому что это снижает зацепление.
ru.wikipedia.org/wiki/Зацеплени…
Чем меньше разные модули знают об устройстве друг друга — тем лучше.
Затем я в DI-контейнере указываю, какой именно класс реализует интерфейс
Logger
:
github.com/bespoyasov/di-…
Таким образом я сбрасываю с себя ответственность за выбор нужной сущности на контейнер — соблюдаю принцип инверсии зависимостей:
ota-solid.vercel.app/dip
В сущности, которой требуется логер, я указываю интерфейс как зависимость:
github.com/bespoyasov/di-…
То есть мне здесь уже не важно, что реализует
Logger
.
Я просто знаю, что есть некая сущность, которая гарантирует метод log
, который я могу тут использовать.
Это значит, что если я решу заменить консольный логер на какой-то другой, то единственное, что надо будет заменить: реализацию и композицию.

А чтобы протестировать модуль, который использует этот модуль как зависимость, мне надо замокать интерфейс Logger:

Этот мок я «подсуну» в регистрацию при тестировании:

И ничего другого не поменялось!
Если мы ещё не будем забывать о LSP и OCP, то масштабировать будет ещё легче.
- ota-solid.vercel.app/ocp
- ota-solid.vercel.app/lsp
Теперь поговорим о том, чем ООП неудобен 🙂
Расскажите о своём опыте тоже? пробовали ли? что не понравилось?
DI, за который я сейчас топлю, когда-то был для меня непреодолимым барьером 😃
Я помню, пришёл в проект на первом Ангуляре, а там DI.
Вот смотрю в код: вижу, используются какие-то сервисы. А ОТКУДА?! При вызове же ничего такого нет, что за магия?
Поэтому для меня основное неудобство — это в первую очередь порог входа.
Перед тем, как затащить какой-то доп. инструмент, я думаю, а это будет поддерживаемым, если я сразу же уйду? Есть кто на подхвате?
@jsunderhood Ну самый очевидный это многословность, интерфейсы, абстрактные классы, дочерние классы, дочерние дочерних, билдер который собирает все эти классы, интерфейс билдера
Ещё, как правильно заметили в комментариях, иногда — избыточная многословность:
twitter.com/zavodnoyapl/st…
Я видел проект, где чёрт ногу сломит 😃
Писать в ООП-стиле так, чтобы было читаемо — сложно. (Хотя, наверное, в принципе писать так, чтобы было читаемо — сложно.)
Нужно постоянно лавировать между «кучей сущностей» и «дырявыми абстракциями» :–/
У меня есть подозрение, что навык писать понятно приходит только с опытом и насмотренностью.
А проблема с насмотренностью в том, что не весь код, который мы видим каждый день — понятный.
Мы начинаем думать, что это норма™, когда это на самом деле не так.
Оттуда часто появляется ощущение, что «ООП 💩», «фронтенд 💩», «JS 💩».
Я пока лишь могу порекомендовать читать книжки:
bespoyasov.ru/tag/books/
...и пробовать руками.
Ещё хочется порекомендовать читать исходники, но как-то не могу вспомнить, какие бы проекты произвели хорошее впечатление 🤔
Ну разве что Sentry ничего:
github.com/getsentry/sent…
Дальше, не очень понятно, как это применять к нынешнему фронтенду — React, Vue, вот это всё.
Есть шаблоны:
- github.com/eduardomoroni/…
Там фреймворки на своём почётном месте во внешнем слое.
Теперь немного о собственно проектировании. Допустим, мы знаем, что нашему проекту нужна суровая масштабируемость. Что делать? Первым делом стоит взять ручку, бумажку и пойти «программировать ногами» 😃 twitter.com/lizuschiykotik…
Это и правда требует больше времени и ресурсов.
Но мы помним, что при проектировании мы уже взвешивали издержки и выгоды:
twitter.com/jsunderhood/st…
Сейчас уйду ещё поработать, а потом обсудим, почему строить грамотную архитектуру проще, но не обязательно с ООП.
ООП в своих принципах подразумевает деление сложного на части.
Инкапсуляция, например — размещение данных и методов для работы с ними в одном месте. Она избавляет от необходимости знать, как устроены детали модуля.
Полиморфизм тоже абстрагирует от деталей, позволяя использовать один механизм для работы с разными сущностями.
medium.com/devschacht/pol…
Композиция (нет, не наследование!) учит выделять функциональность так, чтобы потом её было удобнее сочетать.
en.wikipedia.org/wiki/Compositi…
В принципах SOLID также заложено разделение сложного на части, а ещё — барьеры на распространение изменений:
ota-solid.vercel.app
Например, SRP и ISP требуют, чтобы модуль занимался только одной задачей.
OCP и LSP ограничивает изменения «коробочкой» модуля, а ещё заранее заставляют думать о том, как код будет меняться.
LSP и DIP обращают внимание на зависимости модулей и их направление.
Всё это — какие-то части проектирования.
У ООП и проектирования похожи терминология и инструментарий, поэтому, мне кажется, принципы проще применять во время рисования квадратиков на бумажке.
А теперь — о том, почему наследование как концепт должно умереть 😃
Наследование классов — это прямой путь к антипаттерну God-Object.
Проблема наследования в том, что будущее нельзя предсказать, и мы не можем заранее спроектировать такую иерархию, которая бы отвечала всем требования.
Нам стоит, наоборот, собирать сложное из простого — использовать композицию.
Давайте на примере воспользуемся наследованием и композицией и посмотрим, в чём разница.
Допустим, мы пишем приложение, в котором описываем живые организмы.
Что нам нужно, чтобы описать человека, используя композицию? Надо выстроить иерархию сущностей со своими свойствами:
Животные →
Млекопитающие →
Приматы →
Человек.
Что нам нужно, чтобы описать человека, используя композицию интерфейсов?
Собрать те свойства и методы, которые нам потребуются:
Человек =
Скелет +
Нервная система +
Имунная система +
Сердечно-сосудистая система + ...
Ну окей, пока выглядит одинаково.
...До тех пор пока не приходит задача научить человека летать.
Пусть в нашем приложении появляется Супермен. Он умеет стрелять лазерами из глаз и летать.
Как это впихнуть в иерархию сущностей? 😃
Животные →
Летающие животные?
Летающие приматы?
Суперчеловек?
Человек.
Нипанятна. Нам в какой-то момент придётся создать такой объект, который умеет всё, знает всё, делает слишком много.
en.wikipedia.org/wiki/God_object
В композиции мы добавим дополнительные интерфейсы:
Супергерой = <...Интерфейсы человека> + LaserShooter + Flyable.
Я, кстати, сейчас не говорю об абстрактных классах. Там несколько другое поведение, и используются они иначе. Сейчас речь именно о построении иерархии типов сущностей.
Как защититься от наследования? 😅
- Забыть о слове
extends
(или что там в вашем языке используется 🙂)
- Если есть возможность, использовать sealed
-классы.
docs.microsoft.com/en-us/dotnet/c…Кстати, а накидайте, пожалуйста, случаев, когда без наследования никак не обойтись?
Я что-то пытался сейчас вспомнить, не могу найти подобных. (Абстрактные классы и трейты не считаются! 😃)