🔥

Тред (@bespoyasov)


Доброе утро! Сегодня вторник, а значит поговорим об ООП на фронте. Пока я заливаю в себя кофе, давайте проведём опрос. Как вы думаете, ООП и фронтенд:
🤔 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…
notion image

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

Но я немного отступил от темы. Можно ли писать нормальный ООП-код на фронте? Я недавно написал пост о том, как совместить принципы чистой архитектуры, ООП, DDD и всё такое прочее: bespoyasov.ru/blog/generatin…

Внутри ссылаюсь на офигенную статью @hgracaherbertograca.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:
notion image

Реализация интерфейса описана классом: github.com/bespoyasov/di-… Реализация инкапсулирует в себе детали, которые внешнему миру не важны. Всем потребителям публичного API по барабану, куда уходит сообщение. Им лишь важно, что они могут дёрнуть метод log.
notion image

Обратим внимание, что интерфейс называется более абстрактно, чем реализация. Нам важно сохранять инкапсуляцию и в названии сущности, потому что это снижает зацепление. ru.wikipedia.org/wiki/Зацеплени… Чем меньше разные модули знают об устройстве друг друга — тем лучше.

Затем я в DI-контейнере указываю, какой именно класс реализует интерфейс Logger: github.com/bespoyasov/di-… Таким образом я сбрасываю с себя ответственность за выбор нужной сущности на контейнер — соблюдаю принцип инверсии зависимостей: ota-solid.vercel.app/dip
notion image

В сущности, которой требуется логер, я указываю интерфейс как зависимость: github.com/bespoyasov/di-… То есть мне здесь уже не важно, что реализует Logger. Я просто знаю, что есть некая сущность, которая гарантирует метод log, который я могу тут использовать.
notion image

Это значит, что если я решу заменить консольный логер на какой-то другой, то единственное, что надо будет заменить: реализацию и композицию.
notion image

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

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

И ничего другого не поменялось!

Если мы ещё не будем забывать о 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…

Кстати, а накидайте, пожалуйста, случаев, когда без наследования никак не обойтись? Я что-то пытался сейчас вспомнить, не могу найти подобных. (Абстрактные классы и трейты не считаются! 😃)