Преимущество использования DI для меня в том, что он заставляет разработчиков использовать инверсию зависимостей и работать над уменьшением связности кода. И именно инверсия зависимостей, и слабая связность — то, что помогает писать тесты, а не DI как таковой. Использовать ли DI контейнер внутри тестов или нет — вопрос десятый. Если конкретный тест легко написать без DI контейнера — нужно писать без контейнера. Если у компонента 100500 зависимостей, для каждой из которых нужно подсунуть фикстуры/моки, бывает гораздо проще использовать DI-контейнер прямо внутри теста, и переиспользовать часть кода инициализации приложения, чем писать это все руками.
Кодогенерация — тоже отличный подход. Со своими плюсами и минусами относительно DI. И не использовать ничего из этого вообще — тоже вариант, если это устраивает в конкретном случае.
Это весьма спорный подход. Да, может быть для какого-то кейса он будет работать.
DI-контейнер используется не сам по себе, а как часть подхода инверсии зависимостей. А инверсия зависимостей позволяет минимизировать связность и упростить инициализацию, что и является основной целью.
При применении инверсии зависимостей, каждая фабричная функция будет принимать все зависимости через параметры, а значит каждая фабричная функция:
будет иметь дело с обработкой только тех ошибок, которые появляются внутри этой функции и имеют непосредственное отношение к созданию компонента.
может быть написана с предположением, что все аргументы уже проинициализированы и поставляются в готовом виде, без необходимости дополнительных проверок (опциональные зависимости — исключения из правил).
становится максимально простой и имеет минимум ветвлений, причем имеющиеся ветвления релевантны тому, что эта функция делает, а значит тесты будут осмысленными и писать их будет довольно легко.
не зависит от глобального состояния, что позволяет вызвать ее множество раз с разными параметрами для инициализации большого количества компонентов, которые разделяют имплементацию, но отличаются конфигурацией. А еще это позволяет запускать тесты параллельно.
Разбираю проблемы конкретно:
У Вас сигнатура фабричной функции подразумевает, что она не может вернуть ошибку. Но в реальности, фабричные функции вполне себе возвращают ошибки. Не удалось распарсить конфиг — ошибка, в конфиге параметры, с которыми не удается подключиться к хранилищу или другому сервису — ошибка, нет какого-то необходимого ресурса на старте — ошибка...
Если мы добавляем в сигнатуру фабричной функции возможность вернуть ошибку, то провайдер становится гораздо сложнее и простой Provider.Use(), чтобы получить значение — тоже перестает работать. Если внутри этого провайдеры фабричная функция вернет ошибку, что должно происходить? Пометить провайдер как сломанный, и вернуть ошибку из Use()? Тогда в каждой фабричной функции, будет вереница из if err != nil { return err } на каждую зависимость, что приведет к большому количеству бойлерплейта и ветвлений, которые муторно (и немного бесполезно) проверить в тестах, но которые испортят метрики покрытия. Другой вариант — провайдер может сохранить ошибку и выбросить панику из Use(), а потом можно поймать ее где-нибудь через recover и обработать ошибку полноценно. Тоже так себе решение, это подразумевает, что либо фабричные функции не производят деинициализацию при ошибках, либо делают ее исключительно внутри defer. причем Это же фабричная функция, если ошибок не было, деинициализация вызываться не должна, то есть внутри defer еще и ветвления будут. Шикарно, я не захочу писать для такого тесты, а они нужны даже больше, чем при подходе с явными ошибками.
А теперь главная проблема — при этом подходе используется много глобального состояния, и прямые зависимости. Нельзя запускать тесты параллельно — они будут конфликтовать. Каждая фабричная функция, должна точно знать, какой провайдер вызвать, чтобы получить конкретную зависимость. Это ничем не лучше прямого вызова одной фабричной функции из другой, просто добавился слой мемоизации и возможность подменить возвращаемое значение.
Если пытаться решить эти проблемы, в вашем решении, то:
придется сначала вытащить провайдеры зависимостей в аргументы функции (собственно вот на этом этапе появляется инверсия зависимостей). Без этого не появится возможность вызывать фабричную функцию многократно с разными зависимостями, или запускать тесты параллельно.
затем, чтобы избавиться от просачивания ошибок сквозь провайдер и вынести их обработку из фабричных функций, заменить в параметрах функций провайдеры на собственно объекты, которые возвращаются этими провайдерами. Это потребует вызова провайдеров в правильном порядке. Тут есть два варианта:
a. Создается одна большая функция, которая знает про все зависимости, вызывает все провайдеры в правильном порядке и обрабатывает все ошибки (вопрос, зачем в такой функции вообще провайдеры, если можно вызывать фабричные функции напрямую с тем же результатом). Такую функцию очень сложно тестировать, потому что там будет просто тьма индивидуальных ветвлений на каждый чих, и чтобы их оттестировать нужно как-то провоцировать ошибки внутри фабричных функций. С чего начинали, к тому и вернулись, разве что индивидуальные компоненты друг от друга чуть-чуть изолировали.
b. Используем рефлексию, по сигнатурам фабричных функций и каким-то дополнительным аннотациям, строим граф зависимостей, топологически его сортируем, и вызываем каждый провайдер в цикле, обрабатывая ошибки. Поздравляю, получился DI-контейнер. Накручиваем сверху совсем немножко фич, чтобы это было удобно конфигурировать, и получаем uber-fx. Оттестировать такой подход проще, потому что он не привязан к конкретному набору компонентов. Можно отдельно написать тесты на маленькие фабричные функции, отдельно на DI, и отдельно на функцию, которая создает все провайдеры для DI.
c. Используем кодогенерацию, чтобы сгенерировать код, который будет собственно все инициализировать и обрабатывать ошибки. Нормальный подход, но вопрос верификации того, что сгенерированный код работает корректно, остается открытым.
Я понимаю, что DI ощущается довольно сложной концепцией для того чтобы использовать его в маленьких сервисах, и согласен, что тащить его куда попало, просто потому что он существует — ужасный подход. Но пожалуйста, не надо никому доказывать, что DI не нужен и его можно заменить каким-то гораздо более простым кодом на сотню строк. При попытках добавить довольно простые, но реальные требования вроде обработки ошибок и переиспользования компонентов, подход начинает рассыпаться, и либо превращается в отсутствие структуры вообще, либо вырастает в полноценный DI.
Я просто декомпозицию инициализацию/деинициализацию с помощью di, у меня каждому модулю соответствует функция, которая регистрирует этот модуль в DI, включая конструкторы для всех компонентов этого модуля, все внутренние связи, а также весь код для запуска/остановки фоновых горутин. Соответственно в тестах, я беру di, пихаю в него описание одного модуля, а также моки/фикстуры вместо его зависимостей. Запускаю di, прогоняю тесты, останавливаю di. Отдельно могу запустить весь сервис со всеми компонентами, и замокать внешние зависимости, чтобы проверить, что все вместе работает как ожидается.
Главное тут то, что чтобы использовать DI нужно разделить код на отдельные слабо-связаные компоненты, а это очень важно для написания юнит-тестов, которые реально проверяют поведение, не фиксируют имплементацию.
Потому что го позиционируется как язык, который можно очень быстро изучить и сразу писать код. Это приводит к тому, что куча людей пишет код сервиса на го, по факту изучив только основы, и не вникая, ни в то, как писать идиоматично для языка, ни как нормально структурировать код. На выходе получается очень сильно связанное спагетти. Которое невозможно ни править, ни покрывать юнит-тестами. Добавляем сверху идиотское корпоративное требование 90% покрытия юнит-тестами, и получаем огромные уродливые тесты, которые ничего, на самом деле внятно не тестируют (потому что с такой связностью практически невозможно изолировать отдельные модули и тестировать их отдельно), но при этом тесты будут неочевидно ломаться от почти любых изменений в коде.
DI для нормальной работы требует какой-никакаой структуры кода и разделения на слабо-связанные модули. Что позволяет писать на эти модули нормальные тесты, сильно упрощает рефакторинг, а еще позволяет нормально декомпозировать код инициализации/деинициализации сервиса и заменить огромную функцию (1-2k строк), которая создает и связывает вместе все компоненты, попутно обрабатывая миллион ошибок, потому что надо, на внятное декларативное описание, какие модули нужно инициализировать, не вдаваясь в подробности порядка инициализации, конкретных зависимостей каждого модуля, и обработки каждой возможной ошибки. Плюс это декларативное описание получается без ветвлений, так что проверить инициализацию приложения тестами гораздо проще, чем без DI.
Возможность использовать провайдеры конкретного модуля в тестах на этот модуль — отдельное счастье, которое уменьшает количество дополнительного кода, которое нужно написать, чтобы запустить тест.
Успешно внедрил uber fx в нескольких местах, и собираюсь внедрить еще в нескольких, когда дойдут руки.
Комитет C++ очень печется об обратной совместимости. Введение любого нового ключевого слова, которого никогда не было в языке — поломка обратной совместимости, потому что до введения этого ключевого слова кто-то мог использовать его в качестве имени переменной/функции/типа. поэтому любое новое ключевое слово вводится с очень большим трудом.
Я не знаю, как в C++20 пролезли char8_t, co_await, co_yield, co_return, concept, requires, consteval, constinit, import, & module. Это просто праздник какой-то. Не думаю, что в ближайшее время мы увидем новые ключевые слова.
Однозначно. Вот только context.Done() - это канал :) Потому что только каналы пригодны для использования в select. К тому же контекст не рекомендуется сохранять в структурах, а вот канал от контекста вполне можно :).
На самом деле, не только горутины могут утекать от использования незакрытого канала.
Если мы перестаем использовать буфферизованный канал, не удостоверившись, что он пуст и закрыт, мы также допускаем утечку сообщений. Что потенциально несет гораздо большие проблемы — некорректное логическое состояние приложения, потенциально никогда не отпущенные блокировки, потерю данных, etc.
Ну и есть ситуация, когда от закрытия канала вреда больше, чем пользы. В ситуации, когда в канал может происходить запись из большого количества горутин, бывает гораздо проще, быстрее, и надежнее, не закрывать канал вообще, а просто перестать из него читать. И использовать другой канал, чтобы сигнализировать, что первый канал больше никто не читает, и писать в него больше не надо.
Правда. Unsafe позволяет разыменовывать сырой указатель. В этот момент любые гарантии памяти исчезают напрочь, потому что для сырых указателей borrow-checker не работает и не должен.
Думать о владении и времени жизни нужно всегда, если речь не идет о языке с GC. Да и там иногда приходится. Это вопрос дизайна и архитектуры. Вот только, без контроля со стороны компилятора, можно просто заложить в код инварианты, которые обеспечивают корректность, соблюдать их, и делать только те действия, которые корректны с точки зрения этих инвариантов. А с контролем, зачастую нужно отрефакторить весь код так, чтобы весьма консервативный и ограниченный компилятор согласился с тем, что с доступом к памяти в нем, на самом деле, все в порядке.
Контроль заимствований и времени жизни — это прекрасно. Но как только появляется долгоживущий стейт, на который нужны референсы из нескольких мест, не дай бог еще и циклические (а на низком уровне, на самом деле, этого весьма дохрена) все превращается в сражение с компилятором и переписывание всего вокруг, пока не получится сделать то, что без контроля заимствований и времени жизни просто работает.
На расте без использования unsafe большую часть этого кода писать не получится. А unsafe снимает все гарантии безопасного использования памяти. Какой тогда смысл кроме использования более хайпового языка?
Берем современный C++, выключаем исключения и rtti, обмазываем абстракциями использующими RAII. Бьем по рукам за использование сырых указателей без причины, запрещаем использовать UB, про любой референс задаем вопрос "кто владеет объектом и когда его удаляет?", и получается отличный безопасный для памяти код.
initrd — это Initialization RAM Disk. А не “init root directory”.
Вообще, более современный вариант будет — собрать ядро с включенным efi stub, положить его в /efi/bootx64.efi внутри fat32/gpt раздела. И тогда загрузчик не нужен вообще. Можно вкомпилировать аргументы запуска в ядро. Можно запустить ядро из uefi shell. Ну и морочиться с копированием файлов в виртуалку не нужно. Просто собираем образ диска в хост-системе и подключаем его к виртуалке в качестве usb устройства или sata диска.
Функция детектируется компилятором как корутина, если он может определить для нее promise_type. Делает он это с помощью std::coroutine_traits<Ret, Args…>::promise_type, но как правило достаточно, чтобы у возвращаемого типа был определен member type promise_type. Этот промис должен определять несколько методов, которые вызываются на разных этапах конструирования/работы корутины, и они же определяют, что происходит когда встречаются co_yield/co_await/co_return, но внутри функции этот промис не доступен. Есть только эти три оператора, каждый из которых — точка, в которой корутина прерывает свою работу (и продолжает ее позже, кроме co_return).
По сути из стек-фрейма корутины компилятор генерирует анонимную структуру, которая доступна только рантайму, и доступ к которой спрятан за coroutine_handle. Когда мы вызываем корутину, на куче создается эта структура, потом создается промис, и у промиса спрашивается возвращаемое значение. Как правило, возвращаемое значение — awaitable тип, который хранит ссылку на промис. Дальше уже отдельный вопрос, что делает твой код с этим значением. Если уничтожить его, то уничтожится и промис и корутина, возможно даже не начав выполнение. Если начать ждать этот awaitable, скорее всего это приводит к продолжению корутины. когда корутина исполняется, у нее появляется стек, который используется для вложенных вызовов функций. Но приостановить свое выполнение корутина может только на верхнем уровне, когда стек 100% пустой. Так как фрейм самой корутины живет в куче, а приостановлена она может быть только при пустом стеке, накладные расходы на переключение контекста около нулевые — по факту там нет настоящего кода переключения контекста, это просто возврат из функции, и вызов метода промиса/awaitable объекта, чтобы определить, что происходит дальше.
В C++ это невозможная ситуация. Корутина — это стейт-машина, сгенерированная компилятором. Переключение в эту корутину — вызов функции. Переключение из этой корутины — сохранение состояния и возврат из этой функции. Соответственно, только функция верхнего уровня конвертируется в стейт машину и может прервать свою корутину. Все вложенные вызовы либо должны завершиться к этому моменту, либо должны вернуть awaitable объект, который функция верхнего уровня может начать ожидать с помощью co_await. co_await/co_yield доступны только в самой корутине, но не во вложенных функциях.
Я бы сказал, все описанное относится к C, но не к C++.
В C++ есть RAII, который позволяет корректно высвобождать ресурсы без использования goto и вложенных условий.
Вариант с глобальными переменными, честно, не убедителен. Пробрасывать везде указатель, может быть и не очень удобно, но очень дешево и добавляет очень много гибкости в код. В C++ можно писать методы, которые спрячут пробрасывание этого указателя и помогут не тащить его явно через каждый вызов, но иметь его в доступе. В то же время есть большое количество вещей, которые гораздо сложнее сделать с глобальными переменными, чем без. Использовать не один глобальный объект, а несколько, которые слегка отличаются или используют разные ресурсы. Подменить имплементацию моком в тесте. Обернуть существующий объект дополнительной логикой для конкретных вызовов...
Массив на стеке — отличное решение, когда у нас есть защита от выхода за пределы массива или размер данных точно известен. Во-первых, это очень дешево, во-вторых, даже в C это гарантирует автоматическое высвобождение ресурса. Просто надо следить за размерами массива и использовать исключительно функции, которые принимают на вход размер буффера.
Доступ за пределами объявленной длины массива, это UB в C++. Да, это скорее всего будет работать как ожидается, но технически, это доступ к объекту, для которого не начат лайфтайм. Плюс потенциальные проблемы с выравниванием. Такое как правило всплывает только в коде, который работает с данными которые получены откуда-то извне, или читаются из файла (или пишутся обратно). В этом случае, можно либо использовать отдельный буффер, и копировать из него данные в нормальные структуры данных (попутно проводя валидацию), либо считывать данные частями. Да, это может быть не очень оптимально (добавляется дополнительное копирование, либо увеличивается количество вызовов чтения/записи), однако это гораздо безопаснее и с точки зрения работы с памятью, и с точки зрения валидации входящих данных.
Я очень удивляюсь тому, что где-то сейчас кто-то пишет что-то на C, вместо C++ (если это не старый проект, который уже давным давно написан на C). Отключаем исключения и RTTI и получаем практически 0 оверхеда поверх того, что можно написать на C, при этом имеем очень много удобных и гибких инструментов для упрощения написания кода, и гораздо более надежные инструменты для управления ресурсами.
В целом да, но емнип, один из основных аргументов против исключений и rtti — дополнительный оверхед, который они создают. Ну и крайняя неочевидность, в каком месте какие могут вылетать исключения, и где какие исключения нужно обрабатывать в больших кодовых базах вроде хромиума.
Угу, если несколько потоков читают из-одного файлового дескриптора (странный кейс, но в целом возможный и валидный), то без блокировки на чтении возникает рейс. А с исключением на смену потока, невозможно читать из файла корутиной, которая мигрирует между несколькими потоками (гораздо более жизненный сценарий).
Но это ладно. Меня больше задевает обработка ошибок исключительно через исключения. При том, что куча крупных проектов и почти весь embedded мир намеренно их выключают (see llvm, chromium, google c++ code convention, etc.)
Главное уметь. Для того чтобы сделать установочную флешку из macOS достаточно скачанного образа, disk utility и wimlib, которую можно установить из homebrew. Ставил винду на ноут сестры месяц назад.
По поводу early_return, можно обойтись без макросов вообще. Но тогда придется немножко сломать мозги и перейти к функциональному подходу. И использовать монады. Это несколько более многословно, и не очевидно, но приводит к желаемому результату с сохранением относительной понятности кода. Имплементацию не привожу, так как это очень длинная простыня с огромным количеством шаблонных перегрузок кучи методов, но в целом, если смотреть на пример использования, должно быть интуитивно понятно, что из себя представляет имплементация, и что она, на самом деле тривиальна, даже если не знать, что такое монады.
Другой вопрос, стоит ли оно того? Может лучше декомпозировать функцию на отдельно проверки и полезную нагрузку? Да и использовать менее развесистое форматирование для ветвлений?
std::string ApplySpell(Spell *spell) {
return ValidateSpell(spell)
.OrElse(std::bind(&Self::ApplySpellImpl, this, std::ref(*spell)));
}
private:
Maybe<std::string> ValidateSpell(Spell* spell) {
if (!spell) { return "No spell"; }
if (!spell->IsValid()) { return "Invalid spell"; }
if (IsImmuneToSpell(*spell)) { return "Immune to spell"; }
if (IsSpellApplied(*this)) { return "Spell already applied"; }
return {};
}
std::string ApplySpellImpl(Spell& spell) {
applied_spells_.Append(spell);
ApplyEffects(spell.GetEffects());
return "Spell applied";
}
Лично мне в C++ очень хочется увидеть enum с ассоциироваными значениями, как в rust/swift и паттерн-матчинг, соответственно. Но это очень большие изменения в языке, поэтому я даже не надеюсь на это. Ну и да, compile-time reflection, было бы прекрасно.
Внезапно, да. Какая разница, в каком порядке цифры, если набираешь все равно вслепую, механически зная как и что надо нажать для нужного символа? На то чтобы привыкнуть ушло какое-то время, да. Единственное неудобство, которое осталось — при переключении на любую другую раскладку цифры немного ломают мозг, конечно.
Преимущество использования DI для меня в том, что он заставляет разработчиков использовать инверсию зависимостей и работать над уменьшением связности кода. И именно инверсия зависимостей, и слабая связность — то, что помогает писать тесты, а не DI как таковой. Использовать ли DI контейнер внутри тестов или нет — вопрос десятый. Если конкретный тест легко написать без DI контейнера — нужно писать без контейнера. Если у компонента 100500 зависимостей, для каждой из которых нужно подсунуть фикстуры/моки, бывает гораздо проще использовать DI-контейнер прямо внутри теста, и переиспользовать часть кода инициализации приложения, чем писать это все руками.
Кодогенерация — тоже отличный подход. Со своими плюсами и минусами относительно DI. И не использовать ничего из этого вообще — тоже вариант, если это устраивает в конкретном случае.
Это весьма спорный подход. Да, может быть для какого-то кейса он будет работать.
DI-контейнер используется не сам по себе, а как часть подхода инверсии зависимостей. А инверсия зависимостей позволяет минимизировать связность и упростить инициализацию, что и является основной целью.
При применении инверсии зависимостей, каждая фабричная функция будет принимать все зависимости через параметры, а значит каждая фабричная функция:
будет иметь дело с обработкой только тех ошибок, которые появляются внутри этой функции и имеют непосредственное отношение к созданию компонента.
может быть написана с предположением, что все аргументы уже проинициализированы и поставляются в готовом виде, без необходимости дополнительных проверок (опциональные зависимости — исключения из правил).
становится максимально простой и имеет минимум ветвлений, причем имеющиеся ветвления релевантны тому, что эта функция делает, а значит тесты будут осмысленными и писать их будет довольно легко.
не зависит от глобального состояния, что позволяет вызвать ее множество раз с разными параметрами для инициализации большого количества компонентов, которые разделяют имплементацию, но отличаются конфигурацией. А еще это позволяет запускать тесты параллельно.
Разбираю проблемы конкретно:
У Вас сигнатура фабричной функции подразумевает, что она не может вернуть ошибку. Но в реальности, фабричные функции вполне себе возвращают ошибки. Не удалось распарсить конфиг — ошибка, в конфиге параметры, с которыми не удается подключиться к хранилищу или другому сервису — ошибка, нет какого-то необходимого ресурса на старте — ошибка...
Если мы добавляем в сигнатуру фабричной функции возможность вернуть ошибку, то провайдер становится гораздо сложнее и простой
Provider.Use()
, чтобы получить значение — тоже перестает работать. Если внутри этого провайдеры фабричная функция вернет ошибку, что должно происходить? Пометить провайдер как сломанный, и вернуть ошибку изUse()
? Тогда в каждой фабричной функции, будет вереница изif err != nil { return err }
на каждую зависимость, что приведет к большому количеству бойлерплейта и ветвлений, которые муторно (и немного бесполезно) проверить в тестах, но которые испортят метрики покрытия. Другой вариант — провайдер может сохранить ошибку и выбросить панику изUse()
, а потом можно поймать ее где-нибудь через recover и обработать ошибку полноценно. Тоже так себе решение, это подразумевает, что либо фабричные функции не производят деинициализацию при ошибках, либо делают ее исключительно внутри defer. причем Это же фабричная функция, если ошибок не было, деинициализация вызываться не должна, то есть внутри defer еще и ветвления будут. Шикарно, я не захочу писать для такого тесты, а они нужны даже больше, чем при подходе с явными ошибками.А теперь главная проблема — при этом подходе используется много глобального состояния, и прямые зависимости. Нельзя запускать тесты параллельно — они будут конфликтовать. Каждая фабричная функция, должна точно знать, какой провайдер вызвать, чтобы получить конкретную зависимость. Это ничем не лучше прямого вызова одной фабричной функции из другой, просто добавился слой мемоизации и возможность подменить возвращаемое значение.
Если пытаться решить эти проблемы, в вашем решении, то:
придется сначала вытащить провайдеры зависимостей в аргументы функции (собственно вот на этом этапе появляется инверсия зависимостей). Без этого не появится возможность вызывать фабричную функцию многократно с разными зависимостями, или запускать тесты параллельно.
затем, чтобы избавиться от просачивания ошибок сквозь провайдер и вынести их обработку из фабричных функций, заменить в параметрах функций провайдеры на собственно объекты, которые возвращаются этими провайдерами. Это потребует вызова провайдеров в правильном порядке. Тут есть два варианта:
a. Создается одна большая функция, которая знает про все зависимости, вызывает все провайдеры в правильном порядке и обрабатывает все ошибки (вопрос, зачем в такой функции вообще провайдеры, если можно вызывать фабричные функции напрямую с тем же результатом). Такую функцию очень сложно тестировать, потому что там будет просто тьма индивидуальных ветвлений на каждый чих, и чтобы их оттестировать нужно как-то провоцировать ошибки внутри фабричных функций. С чего начинали, к тому и вернулись, разве что индивидуальные компоненты друг от друга чуть-чуть изолировали.
b. Используем рефлексию, по сигнатурам фабричных функций и каким-то дополнительным аннотациям, строим граф зависимостей, топологически его сортируем, и вызываем каждый провайдер в цикле, обрабатывая ошибки. Поздравляю, получился DI-контейнер. Накручиваем сверху совсем немножко фич, чтобы это было удобно конфигурировать, и получаем uber-fx. Оттестировать такой подход проще, потому что он не привязан к конкретному набору компонентов. Можно отдельно написать тесты на маленькие фабричные функции, отдельно на DI, и отдельно на функцию, которая создает все провайдеры для DI.
c. Используем кодогенерацию, чтобы сгенерировать код, который будет собственно все инициализировать и обрабатывать ошибки. Нормальный подход, но вопрос верификации того, что сгенерированный код работает корректно, остается открытым.
Я понимаю, что DI ощущается довольно сложной концепцией для того чтобы использовать его в маленьких сервисах, и согласен, что тащить его куда попало, просто потому что он существует — ужасный подход. Но пожалуйста, не надо никому доказывать, что DI не нужен и его можно заменить каким-то гораздо более простым кодом на сотню строк. При попытках добавить довольно простые, но реальные требования вроде обработки ошибок и переиспользования компонентов, подход начинает рассыпаться, и либо превращается в отсутствие структуры вообще, либо вырастает в полноценный DI.
Я просто декомпозицию инициализацию/деинициализацию с помощью di, у меня каждому модулю соответствует функция, которая регистрирует этот модуль в DI, включая конструкторы для всех компонентов этого модуля, все внутренние связи, а также весь код для запуска/остановки фоновых горутин. Соответственно в тестах, я беру di, пихаю в него описание одного модуля, а также моки/фикстуры вместо его зависимостей. Запускаю di, прогоняю тесты, останавливаю di. Отдельно могу запустить весь сервис со всеми компонентами, и замокать внешние зависимости, чтобы проверить, что все вместе работает как ожидается.
Главное тут то, что чтобы использовать DI нужно разделить код на отдельные слабо-связаные компоненты, а это очень важно для написания юнит-тестов, которые реально проверяют поведение, не фиксируют имплементацию.
Потому что го позиционируется как язык, который можно очень быстро изучить и сразу писать код. Это приводит к тому, что куча людей пишет код сервиса на го, по факту изучив только основы, и не вникая, ни в то, как писать идиоматично для языка, ни как нормально структурировать код. На выходе получается очень сильно связанное спагетти. Которое невозможно ни править, ни покрывать юнит-тестами. Добавляем сверху идиотское корпоративное требование 90% покрытия юнит-тестами, и получаем огромные уродливые тесты, которые ничего, на самом деле внятно не тестируют (потому что с такой связностью практически невозможно изолировать отдельные модули и тестировать их отдельно), но при этом тесты будут неочевидно ломаться от почти любых изменений в коде.
DI для нормальной работы требует какой-никакаой структуры кода и разделения на слабо-связанные модули. Что позволяет писать на эти модули нормальные тесты, сильно упрощает рефакторинг, а еще позволяет нормально декомпозировать код инициализации/деинициализации сервиса и заменить огромную функцию (1-2k строк), которая создает и связывает вместе все компоненты, попутно обрабатывая миллион ошибок, потому что надо, на внятное декларативное описание, какие модули нужно инициализировать, не вдаваясь в подробности порядка инициализации, конкретных зависимостей каждого модуля, и обработки каждой возможной ошибки. Плюс это декларативное описание получается без ветвлений, так что проверить инициализацию приложения тестами гораздо проще, чем без DI.
Возможность использовать провайдеры конкретного модуля в тестах на этот модуль — отдельное счастье, которое уменьшает количество дополнительного кода, которое нужно написать, чтобы запустить тест.
Успешно внедрил uber fx в нескольких местах, и собираюсь внедрить еще в нескольких, когда дойдут руки.
Комитет C++ очень печется об обратной совместимости. Введение любого нового ключевого слова, которого никогда не было в языке — поломка обратной совместимости, потому что до введения этого ключевого слова кто-то мог использовать его в качестве имени переменной/функции/типа. поэтому любое новое ключевое слово вводится с очень большим трудом.
Я не знаю, как в C++20 пролезли char8_t, co_await, co_yield, co_return, concept, requires, consteval, constinit, import, & module. Это просто праздник какой-то. Не думаю, что в ближайшее время мы увидем новые ключевые слова.
Однозначно. Вот только
context.Done()
- это канал :) Потому что только каналы пригодны для использования вselect
. К тому же контекст не рекомендуется сохранять в структурах, а вот канал от контекста вполне можно :).На самом деле, не только горутины могут утекать от использования незакрытого канала.
Если мы перестаем использовать буфферизованный канал, не удостоверившись, что он пуст и закрыт, мы также допускаем утечку сообщений. Что потенциально несет гораздо большие проблемы — некорректное логическое состояние приложения, потенциально никогда не отпущенные блокировки, потерю данных, etc.
Ну и есть ситуация, когда от закрытия канала вреда больше, чем пользы. В ситуации, когда в канал может происходить запись из большого количества горутин, бывает гораздо проще, быстрее, и надежнее, не закрывать канал вообще, а просто перестать из него читать. И использовать другой канал, чтобы сигнализировать, что первый канал больше никто не читает, и писать в него больше не надо.
Правда. Unsafe позволяет разыменовывать сырой указатель. В этот момент любые гарантии памяти исчезают напрочь, потому что для сырых указателей borrow-checker не работает и не должен.
Думать о владении и времени жизни нужно всегда, если речь не идет о языке с GC. Да и там иногда приходится. Это вопрос дизайна и архитектуры. Вот только, без контроля со стороны компилятора, можно просто заложить в код инварианты, которые обеспечивают корректность, соблюдать их, и делать только те действия, которые корректны с точки зрения этих инвариантов. А с контролем, зачастую нужно отрефакторить весь код так, чтобы весьма консервативный и ограниченный компилятор согласился с тем, что с доступом к памяти в нем, на самом деле, все в порядке.
Контроль заимствований и времени жизни — это прекрасно. Но как только появляется долгоживущий стейт, на который нужны референсы из нескольких мест, не дай бог еще и циклические (а на низком уровне, на самом деле, этого весьма дохрена) все превращается в сражение с компилятором и переписывание всего вокруг, пока не получится сделать то, что без контроля заимствований и времени жизни просто работает.
На расте без использования unsafe большую часть этого кода писать не получится. А unsafe снимает все гарантии безопасного использования памяти. Какой тогда смысл кроме использования более хайпового языка?
Берем современный C++, выключаем исключения и rtti, обмазываем абстракциями использующими RAII. Бьем по рукам за использование сырых указателей без причины, запрещаем использовать UB, про любой референс задаем вопрос "кто владеет объектом и когда его удаляет?", и получается отличный безопасный для памяти код.
initrd — это Initialization RAM Disk. А не “init root directory”.
Вообще, более современный вариант будет — собрать ядро с включенным efi stub, положить его в /efi/bootx64.efi внутри fat32/gpt раздела. И тогда загрузчик не нужен вообще. Можно вкомпилировать аргументы запуска в ядро. Можно запустить ядро из uefi shell. Ну и морочиться с копированием файлов в виртуалку не нужно. Просто собираем образ диска в хост-системе и подключаем его к виртуалке в качестве usb устройства или sata диска.
Так корутины уже в стандарте. Это операторы.
Функция детектируется компилятором как корутина, если он может определить для нее promise_type. Делает он это с помощью
std::coroutine_traits<Ret, Args…>::promise_type
, но как правило достаточно, чтобы у возвращаемого типа был определен member typepromise_type
. Этот промис должен определять несколько методов, которые вызываются на разных этапах конструирования/работы корутины, и они же определяют, что происходит когда встречаются co_yield/co_await/co_return, но внутри функции этот промис не доступен. Есть только эти три оператора, каждый из которых — точка, в которой корутина прерывает свою работу (и продолжает ее позже, кроме co_return).По сути из стек-фрейма корутины компилятор генерирует анонимную структуру, которая доступна только рантайму, и доступ к которой спрятан за
coroutine_handle
. Когда мы вызываем корутину, на куче создается эта структура, потом создается промис, и у промиса спрашивается возвращаемое значение. Как правило, возвращаемое значение — awaitable тип, который хранит ссылку на промис. Дальше уже отдельный вопрос, что делает твой код с этим значением. Если уничтожить его, то уничтожится и промис и корутина, возможно даже не начав выполнение. Если начать ждать этот awaitable, скорее всего это приводит к продолжению корутины. когда корутина исполняется, у нее появляется стек, который используется для вложенных вызовов функций. Но приостановить свое выполнение корутина может только на верхнем уровне, когда стек 100% пустой. Так как фрейм самой корутины живет в куче, а приостановлена она может быть только при пустом стеке, накладные расходы на переключение контекста около нулевые — по факту там нет настоящего кода переключения контекста, это просто возврат из функции, и вызов метода промиса/awaitable объекта, чтобы определить, что происходит дальше.В C++ это невозможная ситуация. Корутина — это стейт-машина, сгенерированная компилятором. Переключение в эту корутину — вызов функции. Переключение из этой корутины — сохранение состояния и возврат из этой функции. Соответственно, только функция верхнего уровня конвертируется в стейт машину и может прервать свою корутину. Все вложенные вызовы либо должны завершиться к этому моменту, либо должны вернуть awaitable объект, который функция верхнего уровня может начать ожидать с помощью co_await. co_await/co_yield доступны только в самой корутине, но не во вложенных функциях.
Очень странно сейчас видеть изображения персонажа TF2 без какого-то прямого отношения к тексту, и не найти упоминания #fixtf2/#savetf2.
Я бы сказал, все описанное относится к C, но не к C++.
В C++ есть RAII, который позволяет корректно высвобождать ресурсы без использования goto и вложенных условий.
Вариант с глобальными переменными, честно, не убедителен. Пробрасывать везде указатель, может быть и не очень удобно, но очень дешево и добавляет очень много гибкости в код. В C++ можно писать методы, которые спрячут пробрасывание этого указателя и помогут не тащить его явно через каждый вызов, но иметь его в доступе. В то же время есть большое количество вещей, которые гораздо сложнее сделать с глобальными переменными, чем без. Использовать не один глобальный объект, а несколько, которые слегка отличаются или используют разные ресурсы. Подменить имплементацию моком в тесте. Обернуть существующий объект дополнительной логикой для конкретных вызовов...
Массив на стеке — отличное решение, когда у нас есть защита от выхода за пределы массива или размер данных точно известен. Во-первых, это очень дешево, во-вторых, даже в C это гарантирует автоматическое высвобождение ресурса. Просто надо следить за размерами массива и использовать исключительно функции, которые принимают на вход размер буффера.
Доступ за пределами объявленной длины массива, это UB в C++. Да, это скорее всего будет работать как ожидается, но технически, это доступ к объекту, для которого не начат лайфтайм. Плюс потенциальные проблемы с выравниванием. Такое как правило всплывает только в коде, который работает с данными которые получены откуда-то извне, или читаются из файла (или пишутся обратно). В этом случае, можно либо использовать отдельный буффер, и копировать из него данные в нормальные структуры данных (попутно проводя валидацию), либо считывать данные частями. Да, это может быть не очень оптимально (добавляется дополнительное копирование, либо увеличивается количество вызовов чтения/записи), однако это гораздо безопаснее и с точки зрения работы с памятью, и с точки зрения валидации входящих данных.
Я очень удивляюсь тому, что где-то сейчас кто-то пишет что-то на C, вместо C++ (если это не старый проект, который уже давным давно написан на C). Отключаем исключения и RTTI и получаем практически 0 оверхеда поверх того, что можно написать на C, при этом имеем очень много удобных и гибких инструментов для упрощения написания кода, и гораздо более надежные инструменты для управления ресурсами.
UUID получается разный, но дефолт для колонки будет сохранен от одного единственного вызова.
В целом да, но емнип, один из основных аргументов против исключений и rtti — дополнительный оверхед, который они создают. Ну и крайняя неочевидность, в каком месте какие могут вылетать исключения, и где какие исключения нужно обрабатывать в больших кодовых базах вроде хромиума.
Угу, если несколько потоков читают из-одного файлового дескриптора (странный кейс, но в целом возможный и валидный), то без блокировки на чтении возникает рейс. А с исключением на смену потока, невозможно читать из файла корутиной, которая мигрирует между несколькими потоками (гораздо более жизненный сценарий).
Но это ладно. Меня больше задевает обработка ошибок исключительно через исключения. При том, что куча крупных проектов и почти весь embedded мир намеренно их выключают (see llvm, chromium, google c++ code convention, etc.)
Главное уметь. Для того чтобы сделать установочную флешку из macOS достаточно скачанного образа, disk utility и wimlib, которую можно установить из homebrew. Ставил винду на ноут сестры месяц назад.
По поводу early_return, можно обойтись без макросов вообще. Но тогда придется немножко сломать мозги и перейти к функциональному подходу. И использовать монады. Это несколько более многословно, и не очевидно, но приводит к желаемому результату с сохранением относительной понятности кода. Имплементацию не привожу, так как это очень длинная простыня с огромным количеством шаблонных перегрузок кучи методов, но в целом, если смотреть на пример использования, должно быть интуитивно понятно, что из себя представляет имплементация, и что она, на самом деле тривиальна, даже если не знать, что такое монады.
Другой вопрос, стоит ли оно того? Может лучше декомпозировать функцию на отдельно проверки и полезную нагрузку? Да и использовать менее развесистое форматирование для ветвлений?
Лично мне в C++ очень хочется увидеть enum с ассоциироваными значениями, как в rust/swift и паттерн-матчинг, соответственно. Но это очень большие изменения в языке, поэтому я даже не надеюсь на это. Ну и да, compile-time reflection, было бы прекрасно.
Внезапно, да. Какая разница, в каком порядке цифры, если набираешь все равно вслепую, механически зная как и что надо нажать для нужного символа? На то чтобы привыкнуть ушло какое-то время, да. Единственное неудобство, которое осталось — при переключении на любую другую раскладку цифры немного ломают мозг, конечно.