ЛУЧШИЕ ПРАКТИКИ

Когда не следует использовать DI, IoC и контейнеры IoC в .NET C#

Знайте, когда DI не являются правильным решением, и лучший дизайн для использования вместо этого в .NET C#

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

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

Однако после многих лет обучения и внедрения я был удивлен, что время от времени я все еще сталкиваюсь с одной и той же проблемой. Как так получилось, что даже после 12 с лишним лет работы инженером-программистом у меня все еще одна и та же проблема?!

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

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

И что?!!



Находка

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

Внедрение зависимостей (DI), Инверсия управления (IoC) и Контейнеры IoC — наши друзья, но, как и все в жизни, если вы злоупотребляете используя их, вы получите то, чего никогда не желаете.

До DI и контейнеров IoC управлять зависимостями между разными модулями/классами было адом, и поэтому мы были более осторожными и осторожными в определении этих зависимостей. Раньше мы дважды или даже больше думали о каждой зависимости модуля/класса, прежде чем начинать реализацию.

Однако теперь, когда у вас есть DI, IoC и IoC Containers, определение зависимости стало похоже на дыхание: вы неявно делаете это, когда на самом деле не распознаете.

Что теперь?

Чтобы понять, что я на самом деле имею в виду, я бы провел вас через практический пример. Я знаю, что вы любите программировать, так почему бы не начать программировать и посмотреть, как это работает?

Однако отказ от ответственности!

  1. Код, который вы увидите, не идеален. Некоторые передовые практики были намеренно проигнорированы для демонстрации и привлечения внимания к другим передовым практикам, на которые мы нацелены в этой статье.
  2. В программном обеспечении всегда есть место для компромисса, и в 90% случаев у вас могут быть свои собственные дизайнерские ограничения. Поэтому, пожалуйста, изучите, что вы найдете в коде, проанализируйте его и посмотрите, что подходит для вашего случая.
  3. Я не эксперт по налогам и не имею практического опыта работы с ними. Поэтому, пожалуйста, извините мои фиктивные импровизированные расчеты, которые вы найдете в коде 🙂

Путешествие по кодовому переулку

Наш простой пример здесь касается программного обеспечения, которое рассчитывает налоги. У нас есть два типа определенных налогов; НДС и доход.

Теперь приступим к реализации кода.

ILogger

Что мы можем здесь заметить:

  1. Это интерфейс ILogger.
  2. Он представляет каждый регистратор, который мы могли бы иметь в решении.
  3. Он определяет только один метод с заголовком double void LogMessage(string message);.

ITaxCalculator

Что мы можем здесь заметить:

  1. Это интерфейс ITaxCalculator.
  2. Он представляет каждый налоговый калькулятор, который мы могли бы иметь в решении.
  3. Он определяет только один метод с заголовком double CalculateTaxPerMonth(double monthlyIncome);.

Консолелоггер

Что мы можем здесь заметить:

  1. Это класс ConsoleLogger, реализующий ILogger.
  2. Он оборачивает класс System.Console и использует его для записи в консоль. Это не идеальная реализация, но на данный момент этого достаточно, чтобы сосредоточиться на текущей области.
  3. Если вы хотите узнать больше о лучшем дизайне и реализации, вы можете прочитать эту статью: Как полностью покрыть консольное приложение .NET C# модульными тестами.

Подоходный налогКалькулятор

Что мы можем здесь заметить:

  1. Это класс IncomeTaxCalculator, реализующий ITaxCalculator.
  2. Это зависит от ILogger, чтобы иметь возможность регистрировать некоторые важные сообщения о вычислениях.
  3. Вот почему ILogger вводится в конструктор.
  4. В реализации метода CalculateTaxPerMonth мы просто делаем вычисления и регистрируем сообщение, используя внедренный ILogger.

НДСКалькулятор

Что мы можем здесь заметить:

  1. Это класс VatTaxCalculator, реализующий ITaxCalculator.
  2. Это зависит от ILogger, чтобы иметь возможность регистрировать некоторые важные сообщения о вычислениях.
  3. Вот почему ILogger вводится в конструктор.
  4. В реализации метода CalculateTaxPerMonth мы просто делаем вычисления и регистрируем сообщение, используя внедренный ILogger.
  5. Отличие здесь в том, что расчеты более сложные, для завершения расчетов требуется 3 шага, и для каждого шага нам нужно регистрировать важную информацию.

Программа

Что мы можем здесь заметить:

  1. Это класс Program. Это основная точка входа для всего приложения, которое, кстати, является консольным приложением C#.
  2. Мы используем контейнер AutoFac IoC, поэтому вам нужно будет установить его из диспетчера пакетов Nuget.
  3. В методе Main первое, что мы делаем, это инициализируем контейнер IoC, определяем наши пары абстракции-реализации и создаем область контейнера IoC.
  4. Внутри области мы разрешаем экземпляр ILogger и список всех доступных реализаций ITaxCalculator.
  5. Затем мы используем все налоговые калькуляторы, чтобы получить сумму всех объединенных налогов.
  6. И, наконец, регистрация сообщения.

При запуске приложения мы получим результат, показанный на изображении ниже.

Отлично, приложение работает, как и ожидалось, мы определили наши зависимости, мы используем DI, IoC и контейнеры IoC… отлично.

Хорошо, вы можете найти это идеальным и легким для чтения и понимания. Однако что, если у вас слишком много модулей, слишком много регистраторов, слишком много калькуляторов…

Что, если?

  • вы хотите контролировать всю пропускную способность процесса регистрации? Количество строк, которые будут записываться каждый час?
  • вы хотите перенести свой дизайн на микросервисы?
  • служба ведения журналов не работает, и вы хотите отслеживать пропущенные журналы
  • вы хотите использовать что-то вроде шины сообщений?

Что, если…

У нас есть много вопросов "Что, если", и не поймите меня неправильно, я понимаю, что они изначально не были частью требований. Итак, я не виню вас за то, что вы не учли это в дизайне.

Однако вот что меня беспокоит:

  1. Дизайн не готов к таким требованиям.
  2. Вам нужно будет применить слишком много изменений, чтобы приспособиться к новым потребностям.
  3. Даже если вы не проектируете свой регистратор и калькуляторы как отдельные изолированные микросервисы, это не означает, что налоговый калькулятор должен каким-то образом зависеть от регистратора.
  4. Калькулятор может использовать регистратор, но он также должен выполнять свою работу, если регистратора нет.
  5. Я слышал, как кто-то говорит, что тогда мы можем изменить реализацию и сделать регистратор необязательным, проверить, передается ли он или нет, и так далее….
  6. Даже если мы применим это, помимо плохой реализации и проверки на наличие нулей… все же модуль/класс калькулятора знает о чем-то, называемом регистратором, что нелогично.
  7. Кроме того, используя текущий дизайн, вы не можете отделить выполнение вычислений от регистрации сообщений. Кроме того, нет единой точки контроля, которая контролирует поток сообщений, поступающих от всех калькуляторов. Из-за этого вы теряете возможность выполнять агрегирование, применять пороговые значения и т. д.
  8. Кроме того, если кто-то новый присоединится к команде и начнет изучать код, он в конечном итоге заглянет в огромную паутину нелогичных зависимостей.

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

И что?

Что вы можете сделать, так это просто изменить свое отношение к зависимостям.

Да, мы знаем, что зависимость связана с реализацией, а не с абстракцией. Но все же, вы считаете, что реализация IncomeTaxCalculator должна зависеть от логгера?

Я могу понять, что реализация класса SQLDatabaseRepository, которая реализует интерфейс IRepository, по определению будет зависеть от некоторого модуля, который открывает и закрывает соединение с базой данных SQL. Это то, что вы можете легко сказать с полным доверием.

Однако вы не можете с таким же уровнем доверия сказать, что одна и та же реализация класса SQLDatabaseRepository зависит от модуля логгера, верно?

В яблочко

Как сделать это правильно?

С некоторыми простыми изменениями в дизайне мы можем это сделать. Итак, давайте углубимся в код.

ITaxCalculator

Что мы можем здесь заметить:

  1. Мы внесли изменение в интерфейс ITaxCalculator.
  2. Мы определили делегат типа TaxCalculationReportReadyEventHandler.
  3. Мы определили новый элемент в интерфейсе как событие типа TaxCalculationReportReadyEventHandler.
  4. Это событие будет использоваться для создания сообщений любому подписчику всякий раз, когда сообщение журнала готово из любого налогового калькулятора.

Налоговый КалькуляторБаза

Что мы можем здесь заметить:

  1. Мы определили новый базовый класс TaxCalculatorBase для всех реализаций ITaxCalculators.
  2. Этот класс предоставит общую реализацию защищенного метода OnTaxCalculationReportReady, который отвечает за внутренний запуск события TaxCalculationReportReady. Это одна из лучших практик, рекомендованных Microsoft.

Подоходный налогКалькулятор

Что мы можем здесь заметить:

  1. Теперь класс IncomeTaxCalculator расширяет класс TaxCalculatorBase вместо интерфейса ITaxCalculator.
  2. Он больше не зависит от интерфейса ILogger, как это было в старой реализации.
  3. Теперь всякий раз, когда ему нужно сообщить о сообщении, он запускает событие TaxCalculationReportReady вместо прямого использования экземпляра интерфейса ILogger.

НДСКалькулятор

Те же изменения, что и в IncomeTaxCalculator классе.

Программа

Что мы можем здесь заметить:

  1. Основное изменение здесь заключается в подписке на событие TaxCalculationReportReady для каждой реализации интерфейса ITaxCalculator.
  2. И обрабатывать это централизованным методом LogTaxReport.
  3. Теперь знания о необходимости налоговых расчетов и регистрации находятся в правильном месте.
  4. Теперь мы можем легко управлять различными модулями решения, собирать всю необходимую информацию, принимать коллективные решения и т. д.
  5. Просто, выруби себя, ты можешь делать все, что захочешь.

Запустив приложение, мы бы получили результат, как на изображении ниже:

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

Теперь мы можем легко адаптировать дизайн, чтобы добавить новые функции, как мы хотели в разделе «Что, если» выше, просто и понятно…

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

Наконец, надеюсь, вам было так же интересно читать эту историю, как мне было интересно ее писать.

Надеюсь, вы нашли этот контент полезным. Если вы хотите поддержать:

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

Хотите узнать больше от автора?