Комментарии 45
Здесь у вас нет DI, вы связывали жёстко логгер и базовый класс. Для DI нужен фабричный класс, который бы и занимался инжектированием. Одна из идей DI состоит именно в том, чтобы не пичкать конструктор кучей аргументов.
Покажите пример внедрения без конструктора
type
[Path('scene'), RolesAllowed(UR_USER)]
TCloudSceneRoute = class
protected
[Context, Connection('MAIN_DB')]
MAIN_DB: TMARSFireDAC;
[Context]
Token: TMARSToken;
public
[POST]
function Create([BodyParam, Required] Params: TCloudSceneJson): TCloudScene;
[GET, Path('{id}')]
function Get([PathParam('id'), Required] const Id: string): TCloudScene;
[GET, Path('{id}/status')]
function Status([PathParam('id'), Required] const Id: string): TCloudSceneStatus;
end;
Например вот. Здесь вообще нет конструктора (дефолтный, без параметров). Поля Token и MAIN_DB инжектируются автоматически. К слову, параметры вызовов функций - тоже автоматически подставляются
Это что за зверь такой? В смысле язык, раньше не встречал как будто бы.
И как в protected поля попадают данные? Через рефлексию?
Напечатал те же вопросы, что и у @MihaOo, благо прочитать успел и не отправил. Аналогичный пример можно на шарпе и без рефлексии, ещё чтобы конструктор был явный и без аргументов?
https://learn.microsoft.com/ru-ru/dotnet/core/extensions/dependency-injection
И что?
public sealed class Worker(IMessageWriter messageWriter)
: BackgroundService
Первичный конструктор принимает интерфейс, что вы этим показали?
Внимательнее посмотрите, как создаётся воркер. Никакие аргументы при этом не передаются.
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();
using IHost host = builder.Build();
Вот создается узел с воркером, которому прокидывается райтер.
Или что вы имеете ввиду?
host.Services.GetService<ExampleService>();
Логгер тут будет сам указан как зависимость автоматически. В статье примеры через выбор конструктора, но можно сделать и через рефлексию, без нужды в добавлении параметров в конструкторы
Да, потому что логгеры прокидываются в конструктор
public ExampleService(
IMessageWriter messageWriter,
IEnumerable<IMessageWriter> messageWriters)
Так смысл в том, что прокидываются они автоматически. Разработчику не нужно знать, где можно достать Logger, DI сам его укажет. В этом и смысл!
Разработчик достанет логгер там, где он прокинул эту зависимость
Т.е. в разных частях программы у тебя будет нужна в логгере и ты будешь его брать из глобальной переменной или синглтона, при этом добавляя каждый раз в using модуль с логгером и другими такими же зависимостями?
Так это буквально ваше предложение. Это же ваш воркер "Никакие аргументы не принимает". Следовательно это ваше предложение дергать службу из локатора или синглтона, когда она понадобится
Я же говорю о явном пробросе зависимости.
И причем вообще здесь юзинг?
В общем, я кажется понял что @HemulGMимеет в виду.
Мы берём, создаём класс с конструктором без параметров, приватным конструктором или чем-то подобным.
Дальше определяем свойства и аргументы для них. DI, используя рефлексию, видит что надо записать в эти свойства для создания экземпляра класса.
DI создаёт все зависимости и требуемый класс и отдаёт его нам в какой-то сервис.
К слову, аргументы можно заменить на какой-то глобальный конфиг в XML, JSON, YAML, да в чём душе угодно и аргументы не понадобятся.
Моё мнение по этому поводу: такие неявные финты ни к чему хорошему не приведут, но в целом никто нам такое делать не запрещает, хотя дополнительная работа с рефлексией не вызывает восторга.
Можно безусловно, но нужно ли? Вот тут вопрос.
Задача DI -- избавиться от ручного создания объектов. А через что это делается, через поля класса или через параметры конструктора, к самой концепции отношения не имеет.
Logger в статье создается и просто передается вручную как аргумент. Тут нет никакой автоматизации.
В DI необходимо было логер зарегистрировать и через сервис DI создавать свой класс, в который DI и внедрит зависимость сам.
Комментарий был не к статье, а к высказыванию про поля vs конструторы.
Я и не говорю, что это должно быть именно через поля класса, я говорю, что тут вообще нет автоматизации. Ты сам создаешь объект Logger и сам его вручную передаешь в другой объект. Где тут DI? Да даже если это было бы свойством или прямым полем, которое назначается после создания. DI тут всё равно нет
Потому что DI для демонстрации концепции здесь не нужен. Просто представьте, что логгер вставляете не вы сами, а DI и всё. Суть в том, что DI здесь легко подключить. А ваш пример на Delphi неудачный — класс теперь всегда требует DI для своего создания, что нехорошо. Тогда как в примере с конструктором при необходимости можно создать всё вручную.
Равно как и требует в конструкторе в статье. Вручную я тоже могу создать этот класс (и уже создаю для тестов) и делается это достаточно легко, просто вызывается DI и передается ему надо объектов для инжектирования.
просто вызывается DI
Это не вручную. Вручную — это прямо руками:
Class1 c1 = new Class1();
Class2 c2 = new Class2(c1);
// и так далее
А у вашего DI неизвестно, сколько магии под капотом закопано.
Вручную это так же. Создать можно и без DI. Если надо, можно и конструктор объявить с нужными объектами или публичные свойства для установки вручную. В этом нет никакой сложности. Однако с DI нет необходимости создавать что-то вручную и заполнять зависимостями, если речь не о юниттестах.
Холивар детектед.
Если надо, можно и конструктор объявить с нужными объектами или публичные свойства для установки вручную.
Я о том и говорю: чтобы (по-нормальному, а не выкрутасами вроде рефлексии) создать класс в обход DI, вам придётся править определение класса. Это и значит, что он жёстко к механизму DI приколочен.
DI не требует автоматизации, DI это про "создал класс и прикинул его в качестве аргумента конструктора/функции". То о чем ты говоришь - DI-контейнеры с автоматическим резолвингом зависимостей, по типу библиотеки Microsoft.DependencyInjection
Вы путаете DI (или IoC) и IoC Container. Первый о том что класс не создает зависимости для самого себя. Второй - фреймворк который резолвит эти зависимости для класса. Прокидывает он их через конструктор (хорошо) или свойства (зло! Не делай так!). В статье используется так называемый Poor man DI. Пишу с телефона, лень умные ссылки добавлять. Если очень надо, то конечно добавлю.
Возможно вечер на меня так влияет , но я не понимаю как создаётся мок объект нужный в тесте.
Автор, поправьте исходный код: у вас там =>
в примерах превратилась в =>
. В результате комментатор выше не понял, что Moq вы конфигурируете ламбда-функциями.
И, если вы пишете на Markdown, запомните - в примерах исходного кода (между обратными кавычками или их группами) использовать HTML entity вместо зарезервированных в HTML символов не нужно.
Описание паттерна ок. Только в реальном коде не надо это использовать. Чтоб не порождать тонны не анализируемого кода. Сперва один производный класс, потом другой. Потом где то через base. и.т.д. Такие подходы приводят к коду не поддающемуся рефакторингу.
Лучше заведите интерфейс, определите там что надо, реализуйте. А потом внедрите его в класс с логикой, которая будет работать с методами. (это паттерн стратегия, инверсия зависимости)
Только в реальном коде не надо это использовать.
Непонятно, чем интерфейс по сравнению с производным классом облечит анализируемость и возможность переписывания кода. Виртуальные методы по факту точно так же задают интерфейс для производных классов. Реализация интерфейса точно так же оказывается вынесена в другое место в коде, и куда именно - это точно так же понятно только уже во время выполнения. То есть, сама по себе замена набора виртуальных методов и вынесение их в интерфейс эквивалентны.
Неэквивалетность возникает, только если производные классы используют методы базового класса, но так делать совсем не обязательно (а делять так, или нет - это уже другой вопрос).
И в целом, анализируемость и пригодность к переписыванию кода ("рефакторингу") - это субъективные свойства кода: как говорится, кого чему учили. К примеру, для типично выпускника курсов-вайтишника никакой код не анализируем и не пригоден для рефакторинга - потому что его этому не учили (хотя и учиться этому самом тоже не мешали).
(это паттерн стратегия, инверсия зависимости)
Это, прежде всего - замена наследования на аггрегирование. Причем, в данном случае - по факту, на композицию: классы, реализующие такой интерфейс, нигде, кроме основного класса использовать нельзя, а потому они, скорее всего, будут иметь то же время жизни, что и основной класс.
Да, есть люди, которые не любят наслелование. И большинство из них просто не умеют его готовить.
И, к сожалению типичные, описания наследования и полиморфизма в учебниках этому не учат. У меня есть даже желание написать про это статью, про наследование в контексте coupling vs cohesion (может, теперь, когда предыдущая статья закончена, даже напишу).
Ну если писать периодически код и рефачить, то будет понятно. Виртуальные методы совсем не по факту тоже самое что и интерфейс, разве что для среды.
По факту факта это каша: определить один механизм в одном классе, и в подклассах его наращивать. Лучше не страдать такой шизой, а просто разделить ответственность между специализированными типами.
" И большинство из них просто не умеют его готовить. "
Ахах. А зачем уметь создавать химеру? Хотя учитывая ваш ответ выше, вы просто не понимаете о чем я...
Ну если писать периодически код и рефачить, то будет понятно.
Пишу, и даже периодически - переписываю. И наследованием пользуюсь вовсю. Из общедоступных образцов - вот. Ну или в репозиотрий из моей свежей статьи посмотрите: классы в пространстве имен, заканчивающиемя на StdRunner и их базовый классы - там примеров шаблонных методов много. А если не лень, загляните её в blame и предыдущие коммиты и убедитесь, что все это ещё и переписывалось в процессе.
Виртуальные методы совсем не по факту тоже самое что и интерфейс.
abstract методы, как здесь - именно интерфейс: ничего другого в этих методах просто нет. А если они ещё и protected - то это интерфейс, доступный только для классов-наследников, внешнего пространства имен не захламляющий, и при этом - доступный и из других сборок. Знаете, как подобное сделать на интерфейсах - расскажите, а то я вот сразу не соображу даже.
По факту факта это каша: определить один механизм в одном классе, и в подклассах его наращивать. Лучше не страдать такой шизой, а просто разделить ответственность между специализированными типами.
Ну, получите в таком случае кашу, но другую: из специализированных типов. Вам такая кашаа больше нравится? Что ж, на вкус, на цвет...
А зачем уметь создавать химеру?
Это не химера, просто это другая сторона программирования, сейчас объявленная немодной. А потому я как раз думаю, что надо мне написть о ней статью.
Знаете, как подобное сделать на интерфейсах - расскажите, а то я вот сразу не соображу даже.
Явная реализация интерфейса
interface IMovable
{
protected internal void Move();
}
class Person : IMovable
{
// явная реализация
void IMovable.Move()
{
Console.WriteLine($"walking");
}
}
Если вас правильно понял
Если у вас фрагменты вашего кода будут решать одну специализированную задачу, то каши не будет. То за что вы топите подталкивает создавать именно не такие фрагменты программного кода, а кашу-малашу.
Вы рано или поздно поймете о чем вам говорят, поэтому дискуссию прекращаю.
Аппеляция к skill issue - не аргумент. Есть объективно читаемый код, а есть объективно не читаемый, и не важен уровень скиллов читающего, даже если он этот код понимает.
Для объяснения паттерна пример хороший. Но такое применение в реальном приложении означало бы создание нового класса каждый раз, когда требуется поменять хотя бы один из множества шагов. Если каждый из шагов имеет по два одновременно доступных варианта, это уже 2^5 классов, которые еще и как-то придется выбирать в зависимости от выбора пользователя и других факторов. Для примера лучше подошли бы другие паттерны, позволяющие реализовать варианты каждого такого шага по отдельности, а потом объединить их в общий процесс. Например, паттерн стратегия.
Если каждый из шагов имеет по два одновременно доступных варианта, это уже 2^5 классов
Это - если реализации шагов не взаимосвязанные. Если взаимосвязанные, вариантов будет меньше. И при использовании аггрегированя нужно завести абстрактную фабрику, чтобы она выдавала согласованную реализацию допустимого набора шагов.
В таком варианте с помощью наследования все это это сделать проще - вне зависимости от того, делается ли наследование от базового класса или реализуется интерфейс, содержащий все нужные шаги.
А какой из вариантов лучше выбрать - это зависит от решаемой задачи.
Как и зачем использовать Template Method в C#