Обновить
4
0

Пользователь

Отправить сообщение

Фантастика.

Как-то у меня друг работал на фрилансе Upwork с почасовой оплатой. Первое время он каждый день он отрабатывал 8 часов по трекеру. В какой-то момент работодатель снял лимит по трекеру, потому что был в восторге от "перформанса".

Мы компанией иногда собирались у него дома по вечерам. Так вот пока делался кальянчик и кто-то ходил за пивом, он просил меня "покликать". Говорил "поковыряй, пожалуйста, проект; поводи мышкой, полазь по файлам, понабирай что-нибудь на клавиатуре". И так в течении часа стабильно. А на следующий час меня заменял другой друг. Вот тебе и "перфоманс". Так он делал частенько: одной рукой набивал стату в трекере, другой переключал видео не Ютуб.

В реальности он работал часов по 5. А трекал таким образом по 10. И получал деньги за 10. Надо признать, что он талантливый малый и за 5 часов делал столько, сколько у другого разработчика того же грейда заняло бы минимум 10. Но платят за часы... И что, получается ему за 2 раза больше сделанной работы получать столько же, сколько коллеге? Работодатель не хотел этого видеть и удерживал рейт между разработчиками +- равным. Ему казалось, что если повысить одному, то все остальные начнут просить больше. Поэтому мой друг балансировал ситуацию в свою сторону таким вот не совсем честным образом.

Так что надеяться, что сотрудник честно спишет время бессмысленно. Хотите мотивировать сотрудника работать без таких трюков - дайте ему пряник в виде доли. Чтобы его доход реально эависел от результата. Он и сам начнет работать максимально эффективно и другим филонить не даст (т.е. теперь их реальный перфоманс тоже на него влияет). Или кнут - в виде жёсткого контроля и риска потерять работу. Но такое возможно только если на рынке переизбыток соискателей. Или приготовьтесь к немому согласию: работодатель догадывается, что сотрудник не дорабатывает, а сотрудник догадывается, что даже если он разорвется вклочья ради успеха проекта, ему все равно не оплатят это пропорционально его вкладу.

Поэтому он спишет ровно столько, сколько безопасно для него "сорри, сделал все что мог, но не уложились, трагедия" и сколько вы все равно не сможете проверить.

Так разве "fu" в примере @amazingname не принимает int id? Внешний фасад остаётся createVersion(id).

Но, согласен, что этот подход с делегатом на каждый io кейс это слишком. Тем более что OrganizeSomeStrangeActivity никогда не будет использоваться в каком-то другом контексте.

По сути ничего не поменялось. Оно и не удивительно, мало обработки, много оркестрации. Из действительно полезного имеет смысл вынести функцию расчета g и f во что-то, что имеет смысл в контексте бизнеса.

Далее вкусовщина:

  • Вынести получение и валидацию a & b в отдельную функцию

  • Вынести обновление counterItem в отдельную функцию

  • (С заменой флоу на событийно ориентированную) Отправка в systemXClient выглядит как сайд эффект, стоит рассмотреть возможность реализации через интеграционный ивент cComputedEvent

  • (С заменой флоу на событийно ориентированную) Обновление каунтера выглядит как сайд эффект, возможно стоит рассмотреть возможность реализации через интеграционный ивент hCompletedEvent

  • (С заменой флоу) Далее уже продолжать логику с момента counterReadyEvent.

  • В целом стоит подумать о согласованности, устойчивости и идемпотентности. Предположим при получении каунтера упала сеть. Насколько устойчива ваша система к этому рассогласованию. Одно дело, когда система находится в рассогласованном состоянии 10милисикунд, пока вы получаете каунтер, а другое часы, пока восстанавливается сеть. Какие процессы это аффектит. А можете ли вы безопасно перезапустить функцию fu, чтобы согласовать систему или придется мануально согласовывать. Если с этим всё ок, то код нормальный.

В ASP.NET Core такое количество интерфейсов обусловлено расширяемостью. Вы буквально можете вклинить свой код куда угодно. Это особенности разработки фреймворка.

Вы разрабатываете фреймворк? Или Вы используете dependency inversion? Или у вас в данный момент есть несколько реализаций которые необходимо подкидывать в рантайме? Если нет, зачем вам интерфейс - прокидывайте конкретный класс. А можете наклепать интерфейсов "на будущее" и потом распутывать граф зависимостей. Может в итоге это окупит себя, когда например вам нужно будет обмазать все приложение логами и аналитикой используя декораторы. А может и нет. Универсального решения нет, только опыт.

Тейк про юнит тестирование чужих библиотек я не понял. Т.е. создатели библиотеки обмазывают ее юнит тестами, а потом потребители библиотеки тоже обмазывают ее юнит тестами?

Когда вы тестируете свой код вперемешку с кодом какой-либо библиотеки, это уже ближе к интеграционному тесту. Просто областью действия ограниченным вашим классом и классом библиотеки.

А раз мы погружаемся в интеграционное тестирование, тогда уже лучше по возможности делать как можно ближе к границам системы.

Когда вы пишете юнит-тесты, важно понимать, что именно вы тестируете. Для этого необходима декомпозиция: нужно четко разделить вашу бизнес-логику и инфраструктуру. В юнит-тестах имеет смысл тестировать только бизнес-логику, а инфраструктуру отделяем в отдельные модули (например, доступ к службам по HTTP, gRPC и т.д.).

Таким образом, в юнит-тестах не должно быть сложных моков. Чем больше моков вы используете, тем больше сигналов о том, что вы смешиваете ответственности в коде. Все программирование сводится к трем основным этапам: получить данные (input), обработать данные (process), записать данные (output). Отделите обработку (process) в отдельную функцию, на которую следует обратить особое внимание при написании юнит-тестов. В самой "функции оркестрации" на уровне служб фиксируйте логический источник, из которого получаете данные (сервис/метод) и какие данные (контракт), и логический источник, куда записываете данные (сервис/метод). Для этого можно использовать инверсию зависимостей. Также можно зафиксировать корректную реакцию функции оркестрации в случае, если не удалось получить данные (если у приложения есть какая-то логика действий в этом случае).

утрированный пример инверсии зависимотей на c#
public interface IMyUseCaseInputService { MyUseCaseData GetData(params); }
public interface IMyUseCaseOutputService { void SaveData(data); }

Если ваша бизнес-логика предполагает оркестрацию нескольких служб, но не включает трансформацию данных, то фактически все тесты будут состоять из моков, которые фиксируют последовательность вызовов и передачу необходимых аргументов, при условии, что замоканная функция возвращает заранее заданный результат и т.д. В реальности передаваемые аргументы могут оказаться невалидными, а службы могут иметь поведение, не соответствующее вашим мокам. В результате все тесты будут проходить успешно, но в действительности система работать не будет. Такие тесты имеют очень ограниченную ценность.

И здесь на помощь приходят интеграционные тесты. В интеграционных тестах мы проверяем, как все компоненты взаимодействуют с реальной инфраструктурой (или частично реальной, в зависимости от области тестирования).

Так что по сути рецепт такой: юнит тестами проверяем только ключевую бизнес логику связанную с обработкой данных (process). В гексагональной архитектуре это и есть центр гексагона. А все остальное (адаптеры) интеграционными. А еще лучше - все вместе интеграционными области e-2-e, но это уже если ресурс есть.

На самом деле, всю работу с базой данных можно организовать по принципу: создать, прочитать, удалить сущность.

Предположим, необходимо разработать систему редактирования, например, кабинет управления магазином на маркетплейсе. Для демонстрации возьмем сущность "продукт". В репозитории будет всего 4 метода: прочитать, добавить, удалить и сохранить. Процесс внесения изменений будет выглядеть следующим образом: получить продукт (из репозитория в сервис) -> внести изменения (на уровне сервиса) -> сохранить продукт (из сервиса в репозиторий). Для чтения создается отдельный интерфейс: получить один "продукт", получить фильтрованный список "продуктов" (с постраничной навигацией, поиском по атрибутам и т.д.). Подключаем индексацию. Этого достаточно для формирования необходимого интерфейса для юзкейсов менеджера магазина, связанных с управлением сущностью продукт. По такой же схеме внедряем агрегаты "опция продукта", "картинка опции продукта", "инвентарь продукта" и тд.

Далее необходимо разработать систему потребления, например, каталог маркетплейса с точки зрения пользователя. Для демонстрации возьмем ту же сущность "продукт" в контексте каталога. Допустим, пользователь переходит к продукту по прямой ссылке. Здесь нужно загрузить сразу данные о производителе, изображения, информацию о наличии, комментарии к продукту и т.д. Вместо того чтобы выполнять десятки джойнов по ключам, можно предагрегировать всю эту информацию по ключу продукта в асинхронном процессе. Предагрегация подразумевает формирование нового ... агрегата / агрегатов. Поэтому методы останутся теми же: получить, сохранить изменения, добавить, удалить. Для чтения создается отдельный интерфейс: получить один "продукт каталога", получить фильтрованный список "продуктов каталога" (с постраничной навигацией, поиском по атрибутам и т.д.). Подключаем индексацию. Настраиваем агрегаты под юзкейсы, и в итоге получаем единый интерфейс для работы с базой.

Теперь, если Вам потребуется перейти на другую базу данных полностью или частично (например, перенести только продукты каталога), достаточно будет внести изменения только на инфраструктурном уровне, что никак не отразится на коде сервисов.

Хотя этот принцип соблюдается даже в более простых конфигурациях. Грубо говоря, можно отделять мух от котлет, а можно и нет.

Вы когда-нибудь задавались вопросом, зачем Вы используете репозиторий? И почему получается так, что сервисы просто проксируют запросы в репозиторий?

Если у вас простой CRUD, в котором самое сложное это валидация и сериализация, а приложению характерна жёсткая завязка на конкретную инфраструктуру - пишите все хоть в контроллере, это наиболее эффективный вариант. Внедрение CQS, аггрегатов, сервисов, репозиториев будет оверинжинирингом.

Для поиска ответа на эти вопросы, предлагаю Вам ознакомиться с гексагональной архитектурой и DDD - так будет куда понятнее, какую роль играют контроллеры, сервисы и причем тут репозиторий.

Я говорил в целом про любые типы сообщений, не только события. Команды это тоже сообщения. Команду так же можно принять и обработать позже.

Приведенная схема напоминает схему репликации баз данных. Только вместо классической модели polling представлена модель pushing. Выбор зависит от задачи: если нужно получить весь лог, то polling , елси нужны только cамые последние записи, хватит pushing.

В данном случае Event Log реализует паттерн transactional outbox. Записывать данные в специальную таблицу Outbox не обязательно; главное — обеспечить гарантию транзакционности. Event Log даже является более предпочтительным вариантом, поскольку это нативный инструмент базы данных.

То есть сервер может получить команду PlaceOrderCommand и сохранить ее в таблице Commands для последующей обработки. После обработки команды можно сгенерировать событие OrderPlacedEvent и записать его в таблицу Events. Если требуется представление заказа, то можно либо консистентно материализовать состояние заказа в таблице Orders в одной транзакции с записью события OrderPlacedEvent, либо обновить таблицу Orders позже, обеспечивая консистентность "в конечном итоге".

На самом деле, ваша идея о хранении данных на клиенте с последующей идемпотентной синхронизацией действительно рабочая и может рассматриваться как опция в некоторых сценариях. Заметки функционируют именно так. Однако в случае с онлайн-заказами я бы лично не стал использовать такой подход.

На самом деле, иногда один толковый программист не связанный по рукам и ногам необходимостью согласовывать каждое свое техническое решение с лидами, архитекторами и менеджерами может крайне эффективно разрабатывать приложение. В некоторых случаях даже сопостовимо с целой командой разрработчиков. Особенно если это веб сервис плана Фриланс, в котором не нужны глубокие технические знания или знания домена: достаточно сделать 30-40% ключевого функциональности хорошо, а "остальное" сделать просто нормально.

На протяжении своей карьеры я не раз сталкивался с ситуацией, когда один человек в команде выполняет до 80% работы. Распределение задач в команде выглядит примерно так: ключевой разработчик самостоятельно выполняет 60% работы, а в последующих 20% он активно участвует, подробно объясняя коллегам, что именно нужно сделать и курируя их работу. Последние 20% работы выполняют остальные члены команды относительно самостоятельно. И как раз такой ключевой разработчик сфокусирован на том, чтобы 30-40% ключевого функционала сделать хорошо. А вот то "остальное" что раньше путем совместных усилий всей команды делалось хорошо, он самостоятельно может сделать нормально.

Если взять двух таких человек, вы получаете: быстро развивающийся проект; высокий кпд за вложенные деньги (зп разработчика по верху рынка и иногда немного выше рынка); гарантию, что ключевой функционал будет сделан как минимум хорошо; и дизастер рекавери если с одим из разработчиков что-то случается; а для "остального" функционала можно позже и при необходимости выделить менее талантливый ресурс.

Таким образом, данный подход может быть либо примером эффективного менеджмента от Хабра, либо просто везение, либо, как я считаю, нечто промежуточное между этими двумя вариантами. :)

Разумеется. Просто я хотел подчеркнуть что проверка крайних случаев это только одна из задач юнит тестирования. И что даже если ChatGPT не очень силен в этом (в том числе потому что среднестатистический разработчик не очень силен в этом), то другие аспекты он покрывает достаточно хорошо, а потому полезен. Он снимает с меня исполнительскую нагрузку, оставляя только аналитическую.

Проверка крайних случаев это не единственная задача юнит тестов.

Представьте что вы будете добавлять тест на обработку строки длиной в гигабайт для каждой функции, которая принимает строку. Это возможно, а имеет ли смысл?

Основная функция юнит теста это фиксация поведения. Это позволяет расширять или рефакторить код более уверено. Вы упоминали snapshot testing, но он хорошо работает только для чистой функции. А если нужно проверить, что определенная функция была вызвана в определенный момент с определенными аргументами?

ChatGPT позволяет мне быстро зафиксировать поведение функции / класса. После первого раунда, я глазами пробегаюсь и прошу догенерировать конкретные кейсы. Чаще всего получается быстрее, чем мануально. При этом моя голова тоже работает.

Могу ошибаться, но возникает такое чувство, что Вы считаете, что если сообщение не было обработано сервисом (проблемы с сетью или железом), то оно не будет обработано им после восстановления к штатному режиму работы.

Гарантию доставки в шину обмена сообщений обеспечивает transactional outbox. Сами шины обмена сообщений гарантируют at least once delivery. Когда сервис вернётся в рабочий режим он продолжит работу с той точки, где он закончил, даже если он упал где-то в середине обработки сообщения. Для этого и нужна идемпотентность, чтобы убедиться, что повторная обработка сообщения на приведет к сбоям в логике работы приложения.

Так что все будет в порядке, и результат будет такой же, каким Вы себе его представляете в Вашей интерпретации Реактивной Архитектуре.

Насколько мне известно из школьной программы, они взаимопритягиваются. Сила притяжения зависит от массы объектов и от расстояния. Чем выше масса и меньше расстояние, тем выше сила притяжения, поэтому я позволил себе обобщить, что земля притягивает яблоко сильнее, чем яблоко землю.

Наверное есть более научное объяснение этому, но это не мой профиль, я не провожу в работе над законами физики 12 часов в день на протяжении 10 лет :)

Кстати как раз этот важный нюанс про взаимное притяжение этом анекдоте опущен, так что пример не самый удачный :)

Предположим приложение состоит из двух классов:

  • Program - инициализация приложения;

  • MyAppColorSchema - реализует интерфейс IColorSchema из библиотеки @house2008.Библиотека позволяет окрашивать вывод в консоль используя разные цвета.

Отношение будет такое:

Program (МВУ) использует библиотеку (в данном контексте МНУ, но без инверсии). В Program происхожит инициализаия MyAppColorSchema (в данном контексте МНУ для Program) и передача ее в библиотеку (предположим через статический класс, не принципиально). Когда происходит вывод в консоль, библиотека (в этом контексте уже МВУ) использует MyAppColorSchema (МНУ).

Этим примером я пытался проиллюстрировать, что один и тот же модуль может быть МВУ и МНУ одновременно в зависимости от контекста выполнения.

Поэтому с такой точки зрения ковыряться в этом нет смысла. В чем есть смысл, та это в каждом конкретном случае идентифицировать, в каком контексте будет использоваться код библиотеки вашим кодом, и в каком контексте будет использоваться тот код, который мы передаем в библиотеку используя DIP, чтобы получить необходимый вам результат.

Пожалуйста!

Если ваша библиотека предполагает возможность расширения пользователем, то без интерфейсов не обойтись. Без них пользователь не сможет адаптировать поведение библиотеки под свои нужды. Например, интерфейс AutoMapper.IValueConverter: https://github.com/AutoMapper/AutoMapper/blob/master/src/AutoMapper/Configuration/IMemberConfigurationExpression.cs#L306. А вот пример его использования из документации: https://docs.automapper.org/en/latest/Value-converters.html.

Но если у вас библиотека типа Math, то здесь вряд ли потребуются расширения, разве что математика каким-то образом меняется от клиента к клиенту. :)

Я не могу представить, как можно обсуждать какой-либо принцип без наглядного примера. Например, закон всемирного тяготения часто иллюстрируется анекдотом о Ньютоне, которому на голову упало яблоко. Здесь все ясно: яблоко упало, потому что Земля, как более массивный объект, притягивает его. Это понятно даже ребенку, и в этом анекдоте есть многослойная мораль о том, что на голову могут падать предметы, это больно, опасно; и объяснение, почему это происходит.

При описании принципов, таких как DIP, всегда нужно приводить примеры, чтобы показать, зачем они нужны.

Например, у меня есть задача создать простейшую систему расширений для браузера. Если бы я не использовал принцип DIP, мне пришлось бы следовать такому подходу:

Разработчик расширения пишет код -> передает его мне -> я интегрирую его в код своего браузера. Все жестко зафиксировано. Грубо говоря интерфейс здесь это мой email на который мне может написать разработчик.

  • Преимущества: полный контроль и возможность добавлять новые функции в браузер для улучшения работы конкретного расширения.

  • Недостатки: жесткая зависимость, значительная операционная нагрузка на команду разработки браузера; при большом количестве расширений потребуется создание целой интеграционной команды; сложно вносить изменения и обновлять версии этих расширений;

Тогда я придумал, что было бы полезно, если бы разработчики создавали файл под названием index.js, в котором обязательно должен быть метод init, и загружали его на сервер в определенную папку. Я встроил в свой браузер движок, который обходит эту папку, читает все файлы index.js и вызывает в них метод init. Здесь интерфейсом является необходимость загрузить файл в определенную папку, название файла index.js и название функции в этом файле.

  • Преимущества: чтобы разработчик мог добавить новое расширение в мой браузер (МВУ), мне не нужно ничего делать, всю работу выполняет разработчик и его расширение (МНУ).

  • Недостатки: Нужно потратить некоторое время на проектирование такой системы, четко определить границы того, к чему у расширений есть доступ, а к чему нет.

Если мой браузер предполагает 3-4 расширения, которые к тому же будут редко обновляться, я не буду тратить время реализуя сложный движок, а просто зашью эти "расширения" в свою систему.

Мне вообще не нравится слово "принцип". Здесь больше подойдет термин "подход". А выбор подхода всегда нужно делать исходя из конкретной ситуации взвесив все за и против.

Что касается расположения интерфейса, это не имеет принципиального значения. Он может находиться на том же уровне, что и модуль, который его использует, или в каком-либо другом реестре или библиотеке классов. Однако он никогда не будет на одном уровне с внешними модулями. Важно понять, зачем вам нужен собственный интерфейс. В последнее время разработчики создают интерфейсы для каждого класса, что не всегда оправдано. Интерфейс должен быть там, где ваша система, модуль или класс делегирует выполнение операции другому модулю, и при этом не принципиально, какому именно (runtime dependency).

Из моего опыта, в рамках веб-служб, в вашем собственном коде, который вы контролируете в рамках одного репозитория, интерфейсы, вероятно, будут следующими:

  1. Доступ к данным: На уровне пользовательского сценария создаются собственные интерфейсы для доступа к данным, например, через репозиторий. Данные интерфейсы реализуются на инфраструктурном уровне. Можно использовать готовый интерфейс, например, EntityFramework.IDbContext, который обеспечивает гибкость в работе с хранилищем, а также огромное количество МНУ (EntiryFramework.MSSQL, EntityFramework.SQLLite & etc).

  1. Бизнес-стратегия: Возможно, в вашем бизнесе есть стратегии, которые должны определяться в рантайме, например, в зависимости от типа клиента или других параметров бизнес-логики. В этом случае интерфейсы будут находиться на уровне бизнес-логики, и их реализации будут там же, если они контролируются вами. Если же реализации подключаются как плагины (что бывает редко и четко прописано в требованиях), они будут подключаться в рантайме как внешние модули.

  1. Бизнес-службы:

  • Виртуальные: Обобщенные службы, необходимые для выполнения бизнес-сценария. Например, вы можете создать IPaymentGateway на уровне пользовательских сценариев и реализовать адаптеры StripePaymentGateway и AuthorizeNetPaymentGateway на инфраструктурном уровне. Или в отдельной библиотеке, подключая их через DI-контейнер. Это нечто среднее между доступом к данным и бизнес-стратегией.

  • Конкретные: Например, IStripeService, если в вашем бизнес-сценарии важен именно Stripe. Здесь многое зависит от того, как написан StipeSDK для вашего языка программирования. Возможно, вы сможете использовать IStripeClient напрямую из библиотеки. Если это невозможно, вы создадите более удобный интерфейс для ваших пользовательских сценариев и делегируете конфигурацию StripeService - адаптеру, расположенному на уровне инфраструктуры. Так же возможен сценарий, когда у вас используется сервисно ориентированная архитектура и есть библиотека классов, которая предоставляет общий контракт служб. Тогда там будут находится интерфейсы вроде IMyProductServiceA и IMyProductServiceB и так далее. В даннном случае вы так же абстрагируетесь только от деталей транспорта (протокол, авторизация и аутентификация), но используете конкретную службу.

Другие вариации сходу не приходят на ум, но я специализируюсь на разработке веб-приложений и говорю конкретно об этой области.

С интерфейсами следует обращаться с большой осторожностью: граф зависимостей может так же легко превратиться в запутанный клубок, как и проводные наушники, оставленные в кармане. Чем глубже ваш граф зависимостей, тем экспоненциально выше вероятность, что это уже произошло.

Такой интерфейс будет абстрагировать только транспорт. В некоторых случаях это имеет смысл, например, когда транспорт у одной службы может быть разный (json-rpc / grpc). Сложно представить случай, когда появляется необходимость менять транспорт в рантайме.

Логика самой службы все ещё остаётся не абстрагированной. Об этом я говорил в комментариях выше.

Информация

В рейтинге
Не участвует
Зарегистрирован
Активность