- Эванс - Предметно-ориентированное проектирование
- Андрей Аксенов — Снесите это немедленно
- Simple Made Easy
- No silver bullet
- Проектирование Функций
- Не дайте Астронавтам Архитектуры вас запугать
- Цикломатическая сложность
сегодня я хочу поговорить про важность одних приницпов не важность других
по тому как выбирать чего придерживаться
как быть в случаях, когда разные принципы конфилктуют
как видеть первопричину и фиксить ее, а не следствия
ну и заодно немного порефлексируем над процессом улучшений и обсуждений улучшений
сначала вопрос, как в ваших проектах с архитектурой и правильно ли с вашей точки зрения они развиваются?
какие основные штуки вам не нравятся в проектах с точки зрения того “как принято“?
генеральная идея такая, хороша архитектура это когда код простой (настолько насколько это возможно в конкретной ситуации) и понятный. Звучит типа абстрактно, но щас мы об этом и поговорим
есть еще два важных понятия simple и easy
вот их путают прямо на раз два
чтобы ответить на вопрос, надо понимать какие у этих слов антонимы (так вроде?)
easy/hard и simple/complex
легкость не означает простоту, а простота не означает легкость
рекурсия это легко или просто?
simple/complex это количественная характеристика, оона говорит о том как много всего вовлечено в процесс
комплексная вещь, значит там комплекс всего
много всяких штук
простая вещь, значит там мало всего
константы переменные
что из этого simple что complex?
кстати рекурсия это hard + simple
то есть рекурсия очень простая вещь с точки зрения задейстовваных штук, но очень сложная с точки зрения осознания и анализа
в общем константа это simple и easy
переменная обычно easy и при этом complex
хотя может превращаться в hard в конкретном коде
например асинхронном
цикл по сравнению с рекурсией complex
он включает в себя довольно много понятий без которых никак
например переменная входит в понятие цикл
очень важно уметь рефлексировать на тему “мне это сложно” и “мне это легко”
на хекслете регулярно возникают подобные обсуждения на тему “решение учителя сложное”
хотя фактически проблема именно втом, что человек так просто не мыслит и не может легко смотреть на задачу с этой стороны
Когда я делал последний лайв, там один чувак в чате написал “нас препод учил так, мол ЧТО делает программа и так понятно, главное описать КАК она делает”
Так вот по поводу этого высказывания, это не так. Как раз самая проблема это понять что делает код
даже если понятно что делает программа
давайте простой пример, вы замечали что иногда бывает три строчки кода, а сложно аж жуть
а иногда куча кода и норм
не берем в рассчет новые конструкции, такое бывает что человек просто сам синтаксис или концепции не понимает
В подавляющем большинстве случаев, проблема именно в количестве состояний
чем больше состояний тем больше complexity
и решение становится complex
Особенно такого много в алгоритмах
знаете про черепашку?
алгоритм поиска циклов в списке? (edited)
там кода кот наплакал
но чтобы его понять, надо очень сильно напрячься
simple/complex это шкала
что-то более простое, что то более составное
вообще мир не черный белый
тоже самое с тестами
тесты крайне редко бывают полностью модульными
и не всегда понятно где заканчивается интеграционность
то есть это шкала, в которой каждый сам выбирает к какому краю он ближе
вот ооп, это simple/complex easy/hard?
да и тут тоже забавно, ведь использование классов это еще не ооп
и как раз таки часто их используют совсем не верно
вот куда это отнести?
как про это говорить? А вот ниже поговорим
при этом человек может думать что и ооп понимает и архитектура у него классная
окей, давайте разберем последнюю тему перед подходами
есть еще два понятия, необходимая сложность и случайная
essential complexity и accidental complexity
дело тут вот в чем
разные задачи требуют разных инструментов и подходов очевидно
использование неправильных инструментов или подходов (например выбрана плохая структура данных, или алгоритм) - называется случайной сложностью. то есть сами себе усложнили жизнь
это сплошь и рядом случается, как раз тут проявляется левел программиста
с другой стороны, есть необходимая сложность и вот тут понимание может помирить вас в голове с самими собой
фишка такая, есть сложность которую физически убрать нельзя, она отражение бизнес логики
если этого не понимать, то получается борьба с ветрянными мельницами
программист начинает хотеть того что невозможно страдает, делает код хреновым и так далее
банальный пример, в коде есть 4 разных вида доставки товара, которые требуют разную логику поведения
можно ли сделать так, что в коде не будет 4 ифов (не важно как они реализованы, хоть полиморфизм)
я имею ввиду концептуально, а не реально, понятно что можно накатать полиморфный код
только это не изменит ситуацию концептуально
ответ тут из самого вопроса понятен, это невозможно если 4 разных поведения, то будет 4 разных куска кода
вроде очевидно, но нет, когда доходит до реальности, я регулярно встречаюсь с разговорами на тему
“как еще тут убрать вот это”
хотя убирать не надо да и невозможно
эта сложность определяется бизнес правилами
в пределе, бизнес правила настолько сложные (когда мы пытапемся реализовать полностью реальное поведение в коде)
что и код получается бесконечно сложным
даже если вы пишите на божественном хаскеле
вот эта мысль для вас очевидна или нет была раньше?
что попытка тащить реальный мир в код это безумие
чем проще модель и при этом она достаточна, тем лучше
достаточна то есть покрывает все кейсы?
да, решает поставленную бизнес задачу, минимально возможным способом
хотите удивительную вещь, чем гибче архитектура тем она более хрупкая и не гибкая?
Если конечно реальный мир не программа мистер нео
Архитектура всегда построена вокруг каких-то предположений, того в какую сторону будет развиваться софт
чем больше гибкости с точки зрения подмены, тем более заточенным под конкретные кейсы становится архитектура
тем сложнее ее менять
больше абстракций – почти всегда хуже
рефакторинг становится почти невыполнимой задачей
равносильной “выкидываем все”
что я наблюдаю постоянно, в простом коде программист фигачит десятки файлов директорий кучи функций которые как матрешка друг в друге
все запрятано по самое не хочу
12:25 на выходе даже смотреть такой код боль прыагя туда сюда и забывая по пути что там было
12:25 а отрефакторить его будет почти нереально
у меня были забавные случаи, когда человек делал проект, навернул абстракций и потом переписывал его столько, что все остальные его обогнали (когда проекты по неделям были)
при том что из всего того потока, он был единственный опытный разрабочтик
я не устаю повторять эту мысль, не делать чего-то пока не болит
12:26 не создавать директории
12:26 не выносить функции в другие модули
12:26 не добавлять индексы в базу
если всегда перебздевать, вы никогда не поймете ту точку, где надо было остановиться
правильно учиться через недостаток чего-то, чем через избыток
тоже сачмое с тестами
не уверены что тест нужен, не пишите
большинство людей вообще их не пишет и живут как-то
буквально сегодня у меня был такой разговор
сразу могу сказать, сколько бы мы тут не сотрясали воздух, начать правильно писать самому нереально
нужен кто-то со стороны, но вот чтобы тот со стороны реально был здравым чуваком, это прямо сложно
как не имея опыта найти его? Вон выше препод там советует человек покрывать все негативные кейсы
что совершенное безумие
Очень просто. Все кто давали толковые советы знали не один язык, всегда совещались с командой и позволяли мне нести бред, а потом аккуратно объясняли где я попутал
И так, выше я уже сказал про одну важную штуку – состояния
это действительно сложно
менеджмент состояния пожалуй самая сложная штука в программировании
сюда входит и инвалидация кеша, и обновление шаренных данных в многопоточном или асинхонном коде
и обновление дома в чистом доме
в общем все где есть данные, которые со временем меняются
как с ними работать? Находить правильные модели, так чтобы состояния отражали задачу и их было не больше чем нужно
использовать явно выделенные состояния там где это возможно
это к теме про конечные автоматы
есть люди которые говорят что мол это сложно или непонятно
но конечный автомат есть во многих процессах независимо от желания человека
это модель, в которую укладывается почти все
давай пример
вот у меня есть данные const state = { errors: [] }
и мне надо выводить ошибки на экранг
как это сделать?
вопрос без подвоха
понятно что будет проверка типа if (errors.length > 0)
хороший ли это код? В простой ситуации почему бы и нет, вполне норм
Но чисто формально, здесь косвенная проверка, а не прямая (edited)
мы пытаемся оценить состояние валидации по вторичному признаку
Хотя вообще говоря какая-то штука либо валидна либо нет
а если она не валидна то у нее есть ошибки
почему это важно? Потому что реальное использование может быть больше одного раза
понадобится где-то в другом месте
тогда нам придется опять смотреть на наличие ошибок
я эту тему раскрывал во втором ментальном
с ошибками еще сложно придумать ситуацию, где это станет серьезной проблемой, но есть места где это уже реальная проблема
представьте что у нас есть заказ и нам надо отслеживать что происходит
даже лучше плажеи
платеж
вот есть сделка, она может быть оплаченной
может быть не оплаченной
может быть выполненной н евыполненной
может быть задержана отменена и еще тыща вариантов
важно что уже выше, я рассуждая о ней выделяю состояние явно
это то как мы думаем о задаче и так она должна быть и записчана
но программисты могут начать руководствоваться другой логикой
скажем наделать флагов
или вообще без флагов6 просто на основании каких-то других данных
типа если значение в поле paidAmaount > 0 то значит деньги пришли
а теперь представьте что у вас везде по коду такая проверка
и вы смотрите на нее спустя неделю (вы уже забудете что там было)
что она рождает в голове?
ничего скорее всего
вам придется думать и напрягаться чтобы вспомнить
это первый признак плохого кода, нужно много контекста для его понимания
он не говорящий
а на самом деле это проверка “договор был оплачен”
а это состояние и его надо выделять явно
state_of_payment = [‘paid’, ‘new’, cancelled’]….
напомню, что в хорошем коде, глядя на код можно прочитав его восстановить исходную задачу в тех же терминах
в которых она была определена
это и будет означать, что код отвечает на вопрос ЧТО (грубовато, но примерно так)
буквально недавно я сюда скидывал пример, у человека в коде есть переменная keys
я спрашивал что она может содержать судя по названию?
подавляющее большинство ответило что там массив ключей
логично чтобы это было так
а там на самом деле был не массив, а строка, и не ключ, а путь
и спрашивается, почему нам так сложно делать то что мы думаем?
тут кстати приходим ко второй истории – именование
я никогда не устану про это говорить
регулярно вижу такие названия функций diffGen
щас дальше больше будет
в общем да, про именование вы много раз слышали повторяться не буду, все равно катастрофа
и это архитектура еще какая
потому что код должен быть понятный
и именования играют тут чуть ли не главную роль
https://twitter.com/mokevnin/status/923833221850320897
архитектура это понятность кода и простота работы с ним
понятность кода в первую очередь про именование, затем абстракции и управление состоянием
именование это больше чем просто ничего не значащие имена
доп источники которые очень подробно объясняют важность этих концепций
DDD про единый язык (у эванса в книге большое предисловие про это)
Тот доклад что выше
именование это не только семантика кода, но и связь с бизнесом
если у вас в коде написано Course а в реальной жизни используется термин Продукт
То это огромная проблема
возникает маппинг между языком бизнеса и языком программистов
приходится постоянно переключаться
терминология, это база
в одной компании я был на интересной встрече, спустя полтора года после сбора команды, у нее был тренинг по выработке общего понятийного словаря
потому что они постоянно путались не понимали друг друга и ругались
Там был такой прикол, что в той команде слово Модуль использовалось для 5 разных вещей
начиная от модулей в js и ангуляр, заканчивая модулем в каких-то телекомуникационных системах
это настолько ухудшало процесс, что им понадобился специальный тренинг с фасилитатором
который помогал им там не подраться
в том докладе что я выше скинул (аксенова)
он именно про это и говорит
что ребята, именование это то про что обычно взрослые дяди и тети не говорят, но жизнь показывает что там полная беда (edited)
и вместо солидов займитесь именами
на одной конференции говорят был такой прикол, выступал человек который рассказывал про то что они делают порно сервис для геев и у них была такая табличка в базе
потом добавили девушек и пришлось ввести поле sex
но имя таблицы осталось то же
и если почитать эванса, он говорит: вы должны поменять код, чтобы он соответствовал
и я с ним согласен
терминология должна совпадать
это то куда уходит много энергии
предлагаю начать с эванса (ту часть что про единый язык)
может натолкнет
там рассказаны стратегии
на словах я капитан, но на деле эти вещи, которые мы проговариваем, они очень глубоки и когда доходит до дела, то выясняется что вроде говорили об одном, а в коде потом все совсем не так
теперь про все эти принципы
давайте поговорим про солиды кисы и тому подобное
В чем я вижу с ними большую проблему
увлекаясь этим добром, забывается цель
особенно это видно в прикладном коде, например во фреймвокрах
у людей там то толстые контролеры то толстые модели
и все значит это обсуждают выступают с докладами предлагают решения
что меня в этой истории удивляет больше всего
все прекрасно знают, что управление сложностью связано с введением абстракций
то есть если с именами все ок, со стейтом все ок (в обычных бекендах обычно с этим просто, там база и усе, но в более сложных приложениях уже не так конечно)
И вот тут, вообще не важно модели контроллеры, делаем мы библиотеку или скрипт или фреймворк
любая проблема сводится к тому как хорошо навернуть абстракций, так чтобы не перестараться
а абстракции грубо говоря это функции (и состояние иногда, но лучше без него)
Почему это важно? Потому что у многих наступает какой-то параличь
вот у них повторяется код и его пихают в модели чтобы не повторяться
в итоге толстые модели и непонятно что делать
единственный выход – строить абстракцию поверх моделей куда увозить этот код
а потом внезапно окажется что подобный подход называют сервис обжекст/сервис лейер/и еще тыща умных слов
и всем начинает казаться что это какая то Архитектура (с большой буквы)
что вот прямо надо понимать
а в реальности просто слой кода, который прячет за собой готовые сценарии
код станет еще сложнее, появятся еще слои
упростит это систему? Конечно нет, она станет сложнее
просто эта сложность станет предсказуемая, в отличии от системы где абстракции не вводятся нормально
Вот давайте на примере этого вопроса разберемся с архитектурой уже в таком виде
в том как компоновать код
скажем с именами все ок, стейт вроде победили (хотя нет)
и вот у нас логика
почему вообще надо разносить код по разным местам?
например, что будет плохого если модель шлет письмо?
все же так делают?
вон даже во втором проекте, где 300 строк кода дай бог
доводят до такого состояния, что я физически не могу этот код понять
у людей в реальном коде модели по 10 тыщ строк кода
и ничо
а тут 15 файлов и папок на 300 строк
Программисты пытаются быть логичными
в том же реакте, вот эта штука может быть отдельным компонентом
может ? может
значит выносим
но это очень узкий взгляд
крайне важно понимать что любое распределение усложняет понимание, усложняет взаимодействие, требует больше кода
естественно до определенной границы
но ее надо чувствовать
так вот, вот у нас много кода, как его разделять
и зачем вообще разделять?
идеально когда каждый кусок выполняет какую-то свою задачу, которая не связана напрямую с другими
что это дает?
это дает высокую степень переиспользуемости
простой рефакторинг
меньше вариантов что в одном месте поправил в другом поломалось
это мое любимое
ну и как следствие такой код при правильном разделении проще понимать
тут тоже есть тысячи но, нельзя взять и просто поделить
и выносить все
переиспользуемость страшная вещь, она опять вас завывает
есть ситуации когда дублирование намного лучше
но щас это опустим
так вот, как делить
главный принцип который я выделяю, называется “отделяем побочные эффекты от чистого кода”
побочные эффекты это в первую очередь любое IO
простой пример, представьте что у нас есть класс который работает с отчетом
сам отчет лежит на диске
какой может быть конструктор у этого класса?
покажите пример на вашем языке
вот, если он принимает на вход путь
то нарушается тот самый главный принцип что я описал выше
почему побочные эффекты нужно изолировать, рассказано тут: https://ru.hexlet.io/blog/posts/sovershennyy-kod-proektirovanie-funktsiy
забавно что в статье это подробно описано, но в проектах все равно даже прочитав ее делают ошибки
в идеальном коде, побочные эффекты делаются на верхнем уровне программы в начале и конце
вот тут оказывается не все понимают что такое верхний уровень
верхний уровень, это входная точка, там где начинается программа (и ее стек вызовов основной)
грубо говоря это функция main
main => читаем файлы => чистый код => записываем файлы
идеальный флоу
далеко не везде так бывает, но к этому надо стремиться
но часто чтение файлов наоборот суют глубоко глубоко
да и вообще любые вызовы
функция => функция => функция => функция => побочный эффект
да конечно не везде
но много где их можно как минимум изолировать
как в примере выше
само чтение может быть в обработчике запроса (он уже не на верхнем уровне)
но класс для работы с отчетом не знает ничего про чтение и запись файлов
он работает с данными
что это сразу дает сходу?
то есть одно простое и понятное (в отличии от SRP) правило
оно дает значительно более простое тестирование (чистый код тестируется легче всего)
оно дает более сильную абстракцию, легче расширить варианты использования
при этом заметьте, в данном случае лучше код != равно больше кода
!= больше абстракций
это тоже важно, есть мнение что хороший код требует времени
требует усилий
и он всегда замороченнее
это хрень полная
локальный код (на уровне простых функций и классов) делается нормальным с полпинка
при условии понимании принципов
и он в подавляющем большинстве случаев будет проще
и короче
при этом расширяемее
(пока абстракции не слишком замороченные)
теперь когда отделили побочные эффекты от чистого кода
следующий шаг это композиция
вместо матрешки
и еще раз статья про проектирование https://ru.hexlet.io/blog/posts/sovershennyy-kod-proektirovanie-funktsiy
идея в том, чтобы правильно представить любой процесс
нужно проектирвоать не от сущностей, а от процессов
матрешка это когда функции вызывают друг друга
композиция когда данные пропускаются как в пайплайне
при этом появляется больше возможнсотей по комбинированию
меньше зависимость между частями
пример с побочным эффектом кстати тоже сюда входит, так как мы получили композицию вместо вложенности
чтение => работа, а не работа (чтение и работа внутри)
несмотря на то что я только начал, уже пора закруглятся и я хочу одну штуку еще сказать
часто бывает такое, что программист смотрит на код и у него в голове несколько принципов между собой борятся
он такой думает если сделаю это то нарушу это если сделаю то нарушу то
так вот, когда у вас в голове борятся разные принципы
очень важно сделать правильный шаг
во первых надо опираться на действительно важное (многие принципы высосены из пальца)
во вторых есть приоритет
всегда есть что-то что первично и нужно придерживаться этого потом окажется, что и другой принцип можно применить уже в рамках изменений по первому
перефразируя, если код изначально какашка, то он останется какашкой что с ним не делай
и какую красоту там не наводи
тот же пример выше с файлом и побочными эффектами
на ревью могли бы его увидеть и сказать о, тут внутри вот такой треш и вот такой
ну и стали бы его там править, но главного бы так и не сделали
вынос побочного эффекта
потом в тестах бы начали моки цеплять и пошло поехало, а уж если там запись файлов то сразу приплыли
можно ли типа сразу понимать что важно что нет что причина что следствие?
ваще без шансов
нужны годы
да, мне понадобилось около 6 лет до момента пока я понял что пишу нормальный код
главная сложность в программировании (необходимая) изменяемое состояние и побочные эффекты
не было бы ни того ни другого, любой код был бы математическими чистыми и прекрасными функциями
простые понятные
легко тестируемые легко применяемые
когда там сложно начинается?
как только состояние useState и побочные эффекты useEffect
без этих штук, любой реакт компонент был бы тупой как дрова
давайте скопирую
Главная проблема в том, что код, взаимодействующий с внешней средой (выполняющий побочный эффект) axios.get(link) переместился с внешнего уровня на более глубокий во внутреннюю функцию, сделав её асинхронной. Почему это плохо? Побочные эффекты автоматически усложняют любой код, в котором они встречаются:
Любая функция с побочными эффектами перестаёт быть чистой. Поведение функции становится менее предсказуемым, она начинает порождать разнообразные ошибки. В нашем случае это ошибки, связанные с проблемами сети (десятки сетевых ошибок!).
Функции с побочными эффектами намного сложнее переиспользовать и тестировать. Для работы функции getPageInfo(link) нужна стабильная сеть. Но это ещё не всё, она опирается на то, что сам Хекслет работает без перебоев и отвечает быстро. Это конечно почти всегда так : но жизнь сложная штука.
Любой код, вызывающий функцию с побочными эффектами, автоматически становится "грязным" и начинает обладать теми же недостатками. Чем глубже по стеку вызовов помещается побочный эффект, тем хуже.
Это касается не только сетевых запросов, но и любых других побочных эффектов. В первую очередь любых файловых операций (ввод/вывод, IO): чтение, запись файлов, взаимодействие с базой данных, отправка писем и так далее. Во вторую, изменение окружения внутри программы, например, модификация глобальных переменных (любого общего состояния).
Главный архитектурный принцип звучит так: "Изолируйте побочные эффекты от чистого кода". Соответственно, всё, что связано с вводом/выводом, должно быть не внутри, а, желательно, на самом верхнем уровне. Причём, чаще всего в начале работы программы происходит чтение необходимых данных, потом большой блок основной логики (чистый код) и на выходе снова побочный эффект, например, запись в файл. Это не всегда возможно, но к этому нужно стремиться.
И еще, я не знаю что вы думаете про разработчиков с кучей лет опыта
которые пока для многих тут кажутся богами
но скажу так, ребят которые пишут классно и имеют правильные модели в голове мало
поэтому не совру если скажу что большая часть проектов внутри это адище
только не путайте плохой код и легаси код
легаси есть в любом проекте, который вышел в продакшен
легаси не значит плохо, хотя бывает очень плохое легаси
всем спасибо кто дотерпел, надеюсь хоть как-то устаканило смысли в голове
понимаю что писать лучше после этого врядли кто-то сможет
но по крайней мере спать спокойно не получится
ну и хорошие абстракции != много абстракций
хорошие абстракции, достаточные для того чтобы сделать жизнь комфортнее, но не создающие случайной сложности
кстати да, во время обучения надо прыгать из одной крайности в другую
я не уверен что без этого возможно
у меня был год или два когда я был архитектурным астронавтом
заморачивался по архитектурам, строил воздушные замки
но как-то вовремя свернул на верную дорожку, как ни странно мне помог erlang и дальше clojure
и хаскель (при том что я его только в базе изучил) (edited)
после этого в голове щелкнуло и с тех пор не отпускает
https://github.com/Hexlet/hexlet-exercise-kit/blob/master/import-documentation/src/index.js вот ребят, это как раз не простой код
потому что там много всего
но написанный с учетом всех штук что я вам говорил
он плоский, у него невысокая цикломатическая сложность
там правильно выделены побочные эффекты
посмотрите на основную функцию, побочные эффекты в самом начале и в конце
Вот еще пример про архитектуру
как правильно md = new Markdown(data); md.render() или md = new Markdown(); md.render(data)
ответ есть в этом уроке https://ru.hexlet.io/courses/php-object-oriented-design/lessons/configuration/theory_unit скоро такой же будет в js