Comments 36
Мне кажется, что смысл статьи можно свести к утверждению, что профессионализм, как и вкус - это прежде всего чувство меры. Меры в строгании микросервисов на каждый чих, затаскивания полновесной гексагоналки на тот чих, который действительно понадобился в отдельном сервисе, мере, до которой можно держать все в монорепе и т.д. А в части теории заговоров - ну ведь все изыскания в архитектуре всех уровней появились и появляются не только, как ответ на вызов времени, но и в силу опыта коммуникаций с бизнесом, люди из которого, зачастую состоят в секте любителей приходить с решениями, а не с проблемами и стратегиями, строго в пику XY-problem ) Вот и рождаются на свет чудовища, пока разум спит.
Насчёт тестов могу сказать так - подобная логика имеет смысл, только если достаточно полный интеграционный тест работает быстро. На известных мне проектах, как правило, интеграционными тестами покрывался штатный случай ("всё работает, если ему не мешать"), а всякие краевые ошибочные случаи - unit-тестами, которых при том же выхлопе можно сделать больше.
Интеграционными тестами можно и корнеркейсы проверять на котнракт API.
У нас на проекте сейчас примерно так выглядит (упростил синтаксис). Юнит-тестов на проекте практически нет.
@before("path/to/before")
@after("path/to/after")
fun `test /link`() {
invokeRoute("/link", params, headers, OK)
checkParterRouteInvokacaions(1)
}
@before("path/to/before")
@after("path/to/400_after")
fun `test /link with bad params`() {
invokeRoute("/link", paramsWithNoUUID, headers, BAD_REQUEST)
invokeRoute("/link", paramsWithNoDevice, headers, BAD_REQUEST)
checkParterRouteInvokacaions(0)
}
@before("path/to/before")
@after("path/to/408_after")
fun `test /link when duplicated`() {
invokeRoute("/link", duplicatedParams, headers, ALREADY_REPORTED)
checkParterRouteInvokacaions(0)
}
С 1975 года и "Мифического человеко-месяца" как раз корпораты, наверное, впереди всех по части софтварного луддизма и архитектурной пакостей. Во многом с их подачи чел-мес постепенно стали человеко-десятилетиями.
Самые яркие квазары формата Linux или Docker запущены анархистами и бунтарями, которые только после попадания в чисто бизнесовую синекуру начинали генерить Bootcamp-манифесты и KISS/DRY/etc принципы. Тот же Agile вместо ускорения и гибкости плотно взял немало команд на блесну kangoo-разработки, конвейеров а/б-тестов, при отсутствии покрытия документацией.
Архитектурные требования определяются задачами и "болями" ключевых пользователей. Тиктоку не нужен TTM или Экселю - RPM. Но им в сто раз важнее понимать какой ключевой набор метрик является определяющим для своих 100-милионноых аудиторий.
Знай своего клиента, будь быстрее рынка, пиши код б..д, действительно чистый и безопасный - вот этого не хватило, возможно. А с общим вектором согласен, бесконечные goto, return, "все везде" в коде, слоняния от теннисного стола к мини-футболу с вялотекущими воспоминаниями о том, что раньше кофе был "рафтее", а бананы в кают-компании больше. При этом постоянные утечки, откровенно пустые дейлики, стэк-солянки из PHP/Ruby, C#, Delphi, JS, Python, PG/MySQL, MongoDB, Airflow, Kafka+RabbitM, раскиданных по дюжине VDS - вот такие кланы не вчера и массово-успешно формируют FAANG's schools. Жаль.
ps. кстати, если в слово Google добавить тире, то дословным переводом станет "гуляй-глазей" ))
Но им в сто раз важнее понимать какой ключевой набор метрик является определяющим для своих 100-милионноых аудиторий.
Кажется, что вот эта часть не про архитектуру, а про фичи, которые нужно реализовать.
Тиктоку не нужен TTM или Экселю - RPM.
Даже если самому Тиктоку не нужен ТТМ, он нужен менеджеру, чтобы быстрее получить обратную связь и показать результаты менеджменту выше и получить свою премию.
А аналогом SLA для Excel будет не RPM, а crashfree, например.
Любые тесты мешают работать, это факт. И переписывать их - мало удовольствия. Но тесты нужны, когда две трети команды - мартышки с гранатой - могут закоммитить что угодно. Я лично видел закоммиченый йух ascii-артом в шаблоне странички, коммит только из пробелов (чёрт потом признался, что набивал количество коммитов, зачем - хз, KPI нет вообще никаких), коммит, когда чел удалил часть бизнес-логики, потому что "там какая-то непонятная фигня", и так далее. Ревьювить на 100% на активном проекте практически нереально, дерьмо всё равно проскочит мимо ревьювера... Зато если ты коммитом сломал тесты - ты закоммитил кал, и точка. Если твой коммит не прошел кодстайл-чек - снова бери и приводи свой код в порядок. Сонар ругнулся - иди и исправляй. 80% косяков отлавливаются автоматизированными проверками, серьезно разгружая ревьювера. Да, это время CI\CD, но время машины тут дешевле времени кожаного ревьювера, потому ок. Гнать мартышек нельзя - ибо нагружать рутиной немногочисленных крутых спецов очень плохая идея, от этого сильно отрастает TTM сложных фич, которые они пилят.
Аннотации в спринге. Имхо, удобно и вполне читаемо, если принести нормальное автоформатирование. Вменяемой альтернативы им просто не вижу. Да, получаются жуткие простыни, особенно когда сваггер притащен - но это плата за удобство и отсутствие xml.
Аспекты. Крутая штука для логгирования и трекинга вызовов в отладочных сборках. Не менее круто аспектами собирать тонкую статистику нагрузочного тестирования. А вот если у вас аспект влияет на основной код хоть как-то - вы ССЗБ, не надо забивать болты молотком в гайки.
Кодогенерация везде. Зловещий артефакт времен Древних, которые писали в vim 0.0.1 на разрешении 80х22 на оранжевом плазменном дисплее лампового же вычислительного устройства. Сейчас кодогенерация встроена в приличную IDE, а еще есть чатгпт, который уж что-то, а типовые шаблоны генерит идеально. Но адепты Культа Древних продолжают совать везде тот же ломбок...
Спасибо за комментарий, со многим согласен.
Про тесты я тоже считаю, что нужны, просто я чаще пишу интеграционные тесты. Раздражает, когда пытаются тестировать каждую функцию просто для галочки или для покрытия. Хотя тот же сонар видит покрытие и по интеграционным тестам — куда заходил код во время выполнения.
Гнать мартышек нельзя - ибо нагружать рутиной немногочисленных крутых спецов очень плохая идея
А вот тут я бы согласился, если бы сейчас был 2022 год. Но с появлением ИИ-инструментов мне проще решать рутину через них, а не через джунов.
Вменяемой альтернативы им просто не вижу.
А вы не писали бекенд на Go? Там подход такой, чтобы контролировать, что проиходит, «без магии», как любят говорить. Сейчас поддерживаю бекенд на Kotlin + Ktor, тоже без магии живем. И до этого на Clojure был бекенд — там такой же подход (но проблемы тоже были, в статье как раз описал).
А вот тут я бы согласился, если бы сейчас был 2022 год. Но с появлением ИИ-инструментов мне проще решать рутину через них, а не через джунов.
ИИ не готов к проду, и еще лет 10-15 не будет готов. Во-первых, владение своим ИИ и его обучение на рабочем контексте пока стоит конски дорого даже для корпораций. Публичный ИИ вашего контекста не знает, да и тырит данные (любой халявный сервис это делает). Во-вторых, за текущим ИИ надо ревьювить под лупой - всирает только в путь. В итоге кожаный тратит на ревью и фиксы столько же времени, сколько кодил бы сам... В-третьих, среды разработки не эволюционировали еще до активного участия в кодинге ИИ - они едва корректно умное автодополнение и шаблоны освоили. Использовать ИИ сейчас в любой IDE - боль и страдание.
Я не имел в виду, что мы даём ИИ задачу, и он идет сам в проект что-то дописывать, а еще и к менеджеру ходит за уточнениями. Всё это еще я буду делать, просто когда до кода дойдет, я буду его просить рутину генерить, тесты писать. В целом, какие-то задачи уже так и решаю. А мой опыт мне позволяет сразу оценить, нормальный код или нет, и где надо самому написать, а где можно ИИ поручить.
Есть кстати уже редакторы, где всё встроено (не знаю насчет их будущего, сам не пользовался):
Еще попадался доклад, где меинтейнер Clojure-плагина для Идеи переписывал его с Clojure на Kotlin с помощью ИИ. Что я из этого доклада подметил — не надо обучать/дообучать на своей кодовой базе. Все что нужно — большой промпт с примерами, как надо. Редакторы, вроде тех, что скинул, и плагинов, что у разрабтчиков идеи внутри есть уже позволяют брать файлы из контекста и добавлять их в промпт.
В целом со статьей согласен и со всей этой болью. Но
С Kotlin, например, возможностей меньше, а значит, и саботаж устраивать сложнее, но ситуация не идеальная.
Java кажется более предпочтительным вариантом, так как менее экспрессивна
Поэтому в большой команде, где будут джуны, мидлы и техно-анархисты, я бы хотел писать на Go.
К сожалению не все так просто. Это скорей самообман сознания желающего найти простоту радикальным образом.
Нельзя просто так взять и повыкидывать все из ЯП.
Возьмем тот же Kotlin, который излишне экспрессивный и Java, философия которой изначально строилась в стиле Go - повыкидывать все из языка, все ограничить и запретить.
Что мы имеем в Java?
Отсутствие перегрузки операторов приводит к огромной лапше в коде в местах где это бы пригодилось бы. Отсутствие именованных параметров - к повсеместному паттену builder.
А главное, отсутствие выразительности языка привело к обилию паттернов и самое ужасное куче костылей от Lombok до взлета популярности проклятого AOP, который превратил Java из, в теории простого и читабельного языка в какое то неявное адище, в проектах с AOP.
Если в языке не хватает выразительности чтобы решить проблему лаконично то часто это будет приводить либо к огромным портянках кода для простых вещей, либо разработчики быстро придумаю как прикрутить туда какую-нибудь вундервафлю и может стать еще хуже.
Есть очень тонкая грань, между выразительностью и простотой и в нее очень сложно попасть.
Go не панацея. Во-первых, да из языка много чего повыкидывали, проблема что забыли выкинуть грабли на которые тут и там можно наступить. Во-вторых, я думаю к сожалению все пойдет по тому же пути развития - будет расти комьюнити, будут расти проекты и будут чаще находиться люди которые будут писать библиотеки закрывающие по их мнению дыры языка, понапридумывают что-то в стиле AOP и Spring. Язык не забанит их за это.
А все потому что проблема в людях а не инструментах. Никакой ЯП даже самый минималистичны не запретит людям страдать фигней.
Все так, при том что в wikipedia алфавитный список ЯП уже содержит 350+ записей. И горшочек продолжает варить ))
проблема что забыли выкинуть грабли на которые тут и там можно наступить
Вы про то, что надо знать детали реализации slice, чтобы им пользоваться? В остальном вроде всё терпимо.
все пойдет по тому же пути развития - будет расти комьюнити, будут расти проекты и будут чаще находиться люди которые будут писать библиотеки закрывающие по их мнению дыры языка
Возможно, будет лучше. На многие грабли уже наступили. В ООП как в панацею мало кто верит, concurrency сразу спроектировали простым, комьюнити ценит ограничения и простоту — читаю Golang сабреддит, общее направление ощущается строгим. Например, не рекомендуют DRY злоупотреблять. Вновь пришедшие из Java ценят, что в отличие от спринга магии нет. Негодавали даже из-за появления дженериков — аля в библиотеках используйте, пожалуйста, но не надо в бизнес-логику тащить.
Навеняка проблемы будут, как и плохие решения этих проблем, и в какой-то момент появится очередной язык, который многое на уровне дизайна решает.
Вы про то, что надо знать детали реализации slice, чтобы им пользоваться? В остальном вроде всё терпимо.
Ну да к примеру неочевидность slices. Далее, на вскидку, неочевидность работы с памятью. К примеру повергает в недоумение, когда при возврате из функции указателя на структуру происходит неявный боксинг, что по моему мнению контринтуитивно.
То что при всей претенциозности обработки ошибок в Go - их можно просто не обработать или сделать это неправильно малозаметным способом.
Работа со строками которые под капотом вроде бы должны быть UTF-8 но нет, там может быть все что угодно. len от строки выдаст количество байт а не символов.
Дикие вещи вроде возврата из функции по имени.
nil указатель, который рассыпает ворох граблей тут и там.
Ну и т.д.
concurrency сразу спроектировали простым
Не простым а минималистичным. Нельзя сделать сложную задачу простой. Мы либо усложняем язык и делаем более сложные но более безопасные решения(привет Rust) либо заметаем сложность под ковер как в Go а потом наступаем. Сложность будет либо в изучении языка либо в решении проблемы. К тому же минимализм, при всей его привлекательности приносит также много проблем. К примеру в Java большая хорошая библиотека для многопоточки, где почти на все случаи жизни что-то имеется, да больше да сложнее и разнообразнее, но под конкретную проблемы мы просто возьмем подходящий проверенный инструмент. В случае Go это приведет либо к костылям либо к чужим либам. Это как предложить хирургу, в арсенале которого сотня инструментов, пользоваться одним типом скальпеля для любой операции. В плане обучения, единообразия, поставок инструментов вроде ок, но в плане результата - такое себе.
В ООП как в панацею мало кто верит
Хороший пример Rust. Язык имеющий определенный сходства с Go - отказ от ООП, наследования типов, отказ от исключений и т.д. Но сразу нашлись умники которые пошли писать библиотеки добавляющие исключения, наследование и прочее в язык в котором идиоматически этого быть не должно. Более того, нашлись гении которые пошли писать для него сборщик мусора. Так что к сожалению пока это все базируется на идеологии, а потом придет модный спикер и расскажет чего в Go не хватает.
Мой посыл наверное в том что яп не панацея, должна быть команда которая друг друга понимает, должна быть IT культура которая продвигает правильный подход.
Вы про то, что надо знать детали реализации slice, чтобы им пользоваться? В остальном вроде всё терпимо.
Ох, если бы только это.
nil
append
в nil
-слайс возвращает слайс с единственным значением, но попытка вставки в nil
-мапу паникует. Функцию cap
можно вызвать на слайсе, но не на мапе.
Для создания выделенного в куче значения можно использовать new
. С одной стороны, это избыточно, поскольку утекающий указатель на литерал значения автоматически приведёт к выделению в куче. С другой стороны, это не ортогонально, потому что де-факто ссылочные типы - слайсы, мапы и каналы - создаются через псевдо-функцию make
.
Имена переменных могут затенять встроенные идентификаторы:
package main
import "fmt"
func main() {
fmt.Println(true) // true
true := 42
fmt.Println(true) // 42
}
В языке практически отсутствует иммутабельность. Из-за этого приходится выбирать между передачей по указателю (дёшево, но функция может поменять) и передачей по значению (функция не может поменять, но потенциально дорогая операция копирования), а ещё пилить костыли. Константы могут быть только в виде чисел и строк.
У каждого типа есть нулевое значение. При создании структуры можно опустить некоторые из полей, в том числе все. Неуказанные поля получают нулевые значения. Добавил новое поле? Ищи все места самостоятельно. И не забудь, что, если структура не приватная, то внешний код тоже может такую ерунду делать.
У интерфейсов тоже есть нулевое значение, nil
, и это отличается от интерфейса, который хранит nil
-значение:
package main
import "fmt"
type I interface {
f()
}
type C struct{}
func (c *C) f() {}
func main() {
var in I
fmt.Println(in == nil) // true
var c *C = nil
in = c
fmt.Println(in == nil) // false
}
Переменные цикла переиспользуются между итерациями, что в своё время привело к отзыву трёх миллионов сертификатов Let's Encrypt. Сейчас это поправили, но более разумное поведение, ЕМНИП, всё ещё opt-in.
Каналы могут быть в двух невалидных состояниях: nil
и закрытом. Посылка и получение в nil
-канал блокируются навечно, в то время как отправка в закрытый канал паникует, а получение из закрытого канала возвращает нулевое значение. Зачем разница в поведении - неясно.
defer
вызывается в конце функции. Функции, а не лексической области видимости.
Одно ключевое слово for
имеет 11 разных значений в зависимости от семантики. У четырёх из них поведение зависит от того, по какому типу итерация, но синтаксис при этом идентичный. А, и ещё можно итерироваться с одной переменной по слайсу и получить индексы вместо значений.
Для обработки ошибок используются множественные возвраты. Обычно принято, что возвращается значение и nil
в случае нормального возврата, в случае ошибки же возвращается произвольное (обычно нулевое) значение и не-nil
ошибка. Однако этой конвенции даже в стандартной библиотеке не везде следуют.
Если в пакете есть функция под названием init
, то она будет молча вызвана при импорте этого пакета. При импорте пакета несколько раз функция init
будет вызвана несколько раз.
По моему, как-то многовато надо в голове держать для так называемого простого языка.
Хочется вступиться за юнит-тесты.
Хороший тест - это еще и документация.
Если тест тяжело писать - тестируемый код, скорее всего, не очень. Неоднократно наблюдал, как пишущего тесты страдальца спасает декомпозиция тестируемого класса. Последний реальный пример - уменьшение количества тестов с 118 до 9.
Еще частенько тесты получаются отвратными, если модель неудачная. Иногда модель нужно другую спроектировать, а иногда и специальный DSL для теста сделать, чтоб стало проще и выразительней.
От юнит-тестов не предлагаю отказываться, вот как раз такой подход выглядит хорошо:
Последний реальный пример - уменьшение количества тестов с 118 до 9.
Если подразумевалось, что тестировать стали не каждую функцию, а набор функций вместе, которые скрываются за той или иной формой интерфейса.
Еще частенько тесты получаются отвратными, если модель неудачная. Иногда модель нужно другую спроектировать, а иногда и специальный DSL для теста сделать
А вот были бы тест на контракт (рест эндпоинта, интерфейс, модуль), можно внутри сколь угодно менять модель.
Еще бывает и так, что модель хорошая была и с хорошими тестами, но требования так поменялись, что большую часть переписывать нужно. И тут много маленьких тестов опять мешать будут.
Если подразумевалось, что тестировать стали не каждую функцию, а набор функций вместе, которые скрываются за той или иной формой интерфейса.
Наоборот. Декомпозировали, протестировали каждый новый компонент, плюс исходный компонент, который превратился в композицию из новых компонентов со радикально меньшей цикломатической и комбинаторной сложностью.
А вот были бы тест на контракт (рест эндпоинта, интерфейс, модуль), можно внутри сколь угодно менять модель.
Да, но чем дальше тестируемая абстракция от компонента, давшего сбой, тем больше шанс ввалить кучу времени на поиск причины сбоя.
Конечно, если само приложение компактное, условно - пара тысяч строк кода - оно и запустится быстро, и тестов много не будет - тогда можно вообще без юнит тестов обойтись, пока приложение не распухнет выше некого порога.
Еще бывает и так, что модель хорошая была и с хорошими тестами, но требования так поменялись, что большую часть переписывать нужно. И тут много маленьких тестов опять мешать будут.
Если колбасить нужно быстро, а требования нестабильны - то это совсем другое дело. Нужен не кристальный код, а одноразовое дендрофекальное поделие, единственное назначение которого - валидация требований, проверка гипотез, получение обратной связи и т.д.
Давайте сразу разделим понятия микросервисы и сервисы (service-based architecture). Сервисы — это разбитый по доменным областям монолит (например, сервис доставки). Микросервисы — уже разбитые по функциям сервисы (например, микросервис нотификации о доставке). За подробностями можно обратиться к архитектору Марку Ричардсу Microservices vs. Service Based Architecture.
Не надо путать причину и следствие. Пусть, даже сам Марк это путает.
Микросервисы MSA это тот же SBA но с более жёсткими требованиями к независимости сервисов, главное из которых отсутствие связей по БД, то есть использование своих БД у каждого сервиса.
SBA не бывает монолитом. Для такого монолита есть своё название: modular monolith.
Внёс порядок в термины. Статья весьма интересная.
По заготовке: не разработчики, а архитекторы. И причём тут заговор? Заговор это осознанное действие.
Лучше так:
"Как архитекторы вредят корпорациям"
"Как архитекторы вредят корпорациям"
Это зависит ещё от того, что за компания и какие там процессы. Иногда архитектора просто никто не слушает.
Я же ещё в статье про принципы пишу, а они все про решения разработчиков.
Название унаследовано от предыдущей статьи, будет ещё одна про коммуникацию в компаниях. Наверное, слово "разработчик" оставлю, но в более широком контексте (все кто участвует в разработке).
И причём тут заговор? Заговор это осознанное действие
Я это понимаю. Форму подачи такую выбрал, якобы кто-то специально принимает плохие решения.
Заговор програмистов анархистов - это что-то новенькое :-). Но вообще
Миром правит не тайная ложа, а явная лажа
Програмисты как и все люди просто тупо лажают и корпорации валяться из-за их ошибок. Паттерны - это просто популярная идея, которая при черезмерном увлечении превращается в лажу.
На всякий случай уточню, что это просто форма подачи, я не считаю, что организация существует ;)
Ого себе форма подачи. Я до конца не дочитал, но впечатлении что вот типа такая организация существует О_О. Чрезмерное увлечение паттернами, как и любыми другими идеями - это зло. Хотелось бы такую же статью, но без чрезмерного анганжирования за счет темы всемирных заговоров.
И покороче. Но это на любителя уже.
Что-то весьма интересно, а что-то для кустаря-одиночки сложно. Тут изобретаю модульность для большого проекта. И микросервисы для меня не понятно что это такое. В общем-то с задачей разобрался и разберусь, но несколько костыльно. Хотелось бы понимать как лучше сделать. Сейчас такое впечатление, что микросервисы это модная идея и сильно увлекаться ей вредно. Но модульность нужна, чтобы вообще не потеряться в куче кода. Разбивка на классы, компоненты(плагины) и т.д. Можно сделать примеры разбивки кода на модули и на (микро)сервисы? Что почему и как удобнее сделать?
Всё зависит от огромного числа факторов: что за проект, какая аудитория, будет ли она расти, сколько человек будет поддерживать проект, сколько проект будет существовать, какой стек, какой бюджет, если ли необходимость пользоваться той или иной инфраструкторой.
Простых ответов нет, а примеров в интернете много. В популярном open source обычно хороший код.
ERP система для производства с интегрированным интернет-магазином. Это описание проекта. Стек MODX, php, vue и самописное API с табличным редактором на основе PrimeVue https://github.com/touol/gtsAPI. Это вкратце. Для полного описания нужно кучу статей писать. Что сейчас не реально. Но я могу что-то написать, если пойму что будет кому-то интересно. Точнее даже, через месяца 3 что-то точно напишу, но для сообщества MODX. А писать ли на Хабре не знаю...
Простых ответов нет, а примеров в интернете много. В популярном open source обычно хороший код.
Разобранных примеров: "Что? Почему и как?" я как-то не встречал. Может плохо искал??
Добавлю по языкам.
Можно в Java писать в стиле Clojure. Максимально функционально. Максимально Streams, когда они нафиг не нужны - циклы, условия, обработка null. Можно наворачивать вызовы функций друг в друга, крайне желательно с лямбдами. При этом используя стохастическое форматирование. Огромные методы без единой переменной. Создается адовая матрешка, кочан вызовов, на разбор которого можно тратить целые часы. Что вызывается, в каком порядке? Какие данные передаются в вызовы, что возвращается? Невозможно понять ни чтением, ни дебаггингом, потому что идея при включении показа возвращаемого значения начинает тупить, а по f7 внутрь лямбд она не заходит.
Все робкие протесты против такого стиля программирования убиваются простейшим "Ты просто плохо разбираешься в стримах и функциональном программирование. Тебе надо расти и много упражняться. Ты не готов стать сеньором.".
Согласен, пример старенький. Тут еще не хватает аннотаций AspectJ и Dagger, так бы получилось добавить еще 4 штуки.
Сатира, конечно, хорошо. Но использование конструкции языка 20-летней давности, имхо, слишком уж уводит в гротеск и отвлекает. На деле, конечно, аннотиции jaxb и jackson будут висеть на dto и смотреться это всё будет не столь ужасно
Буду писать препроцессор к Go, чтобы можно было нормально читать. )
Или, наоборот, вьювер кода на Go в стиле Java.
Заговор разработчиков против корпораций: архитектура и принципы