Здравствуйте, Sharov, Вы писали:
S>.net core, С#. Стал с недавнего времени использовать DI, а там без интерфейсов практически никуда.
причем максимум у них 2 реализации а если не заморчиваться с тестами то ваще 1, сам пишу, но это хрень и дичь
Я изъездил эту страну вдоль и поперек, общался с умнейшими людьми и я могу вам ручаться в том, что обработка данных является лишь причудой, мода на которую продержится не более года. (с) Эксперт, авторитет и профессионал из 1957 г.
Здравствуйте, Буравчик, Вы писали: Б>Чем руководствуетесь, принимая решение выделять или не выделять интерфейс из класса, т.е. нужно ли разделить интерфейс и реализацию? Б>Когда имеется несколько реализация — понятно, интерфейс обычно выделяют. Б>Но если реализация только одна, для каких классов выделяете интерфейсы, а для каких нет?
В тех случаях, когда может/должна появиться независимая реализация. Наличие интерфейса при одной-единственной реализации это, собственно, способ сказать читателю кода: «Появится другая, возможно, я знаю, какая, а возможно — имею об этом лишь смутное представление».
Важными считаю 2 момента:
1. Иногда реализация так и остаётся единственной. Например, библиотека умирает вместе с проектом, платформой etc. Но это НЕ означает автоматически overengineering, ведь фиксировать свои мысли об архитектуре надо по мере их появления, начиная с самой ранней версии. Overengineering'ом это может быть в каждом конкретном случае. Или не быть. Поэтому когда бараны кругом начинают блеять своё «YAGNI», надо требовать доказательств аргументов применительно к данному классу и данному интерфейсу. Общий случай не катит. Хотя в частном они могут оказаться правы.
Скрытый текст
Как вы понимаете, такой подход обесценивает YAGNI в принципе.
2. В каждом конкретном случае думать приходится творчески, к механическим правилам это свести невозможно. (Невозможно заложить правила проверки в static code analyser). Такой ответ, наверно, кого-то разочарует, но кого-то, наоборот, успокоит: у всех так, посоны. Б>P.S. Возможно, ответ сильно зависит от используемого языка. По возможности, укажите используемый язык.
Зависит мало, на самом деле. 20 лет назад в плюсах дефайнили interface на class/struct и вперёд. Не для компилятора же мы это писали.
Здравствуйте, Буравчик, Вы писали:
Б>Чем руководствуетесь, принимая решение выделять или не выделять интерфейс из класса, т.е. нужно ли разделить интерфейс и реализацию?
Б>Когда имеется несколько реализация — понятно, интерфейс обычно выделяют. Б>Но если реализация только одна, для каких классов выделяете интерфейсы, а для каких нет?
Есть такое соображение, как минимизация кол-ва перестраиваемых модулей/сборок/пакетов. Руководствуясь им (в том числе им), принимаю решение о том, что нужно разделить интерфейс и реализацию даже в случае единой реализации. Интерфейсы кидаются в стабильный пакет, а реализация живет в нестабильном, который подключается динамически. Или даже статически, но уже на этапе внедрения зависимостей.
Так же можно поступать, когда разработку реализации нужно выделить вовне, либо отделить по срокам.
Б>P.S. Возможно, ответ сильно зависит от используемого языка. По возможности, укажите используемый язык.
Б>>Когда имеется несколько реализация — понятно, интерфейс обычно выделяют. Б>>Но если реализация только одна, для каких классов выделяете интерфейсы, а для каких нет?
САД>Выделяется при любом взаимодействии двух и более классов, ибо тесты.
За такое гореть в аду, фреймворки давно уже умеют мокать классы, если даже таких нет, можно дописать велосипедов самостоятельно, но при этом контракттипа не раздувая.
Б>>Но если реализация только одна, для каких классов выделяете интерфейсы, а для каких нет?
Когда обращение должно быть по интерфейсу, вместа типа. Например класс внутренняя реализация модуля (internal) создает этот тип какой-то провайдер и необходимо оставить реализацию типа внутри билиотеки, либо может внутри какого-то namespace (физически нельзя запретить, но соглашением можно)
Здравствуйте, diez_p, Вы писали:
S>>.net core, С#. Стал с недавнего времени использовать DI, а там без интерфейсов практически никуда. _>А что вам мешает подсоввать тип напрямую?
А зачем тогда DI? А если я конфигурирую реализации в конфиге, и тип узнаю только в runtime?
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, diez_p, Вы писали:
S>>>.net core, С#. Стал с недавнего времени использовать DI, а там без интерфейсов практически никуда. _>>А что вам мешает подсоввать тип напрямую?
S>А зачем тогда DI? А если я конфигурирую реализации в конфиге, и тип узнаю только в runtime?
DI решает какие объекты когда создавать, так же он заботится о том, чтобы при вызове конструктора типа, туда были переданы правильные требуемые инстансы.
Все равно непонятно почему даже в конфиге вам надо указывать именно интерфейсы, а не конкретные реализации.
Конфиг для контейнера нужен, если кто-то меняет реализацию без пересборки проекта, если этого не происходит выкинуть конфиг и зашить все в код.
Условно есть три сервиса
S1, S2, S3 — конкретные типы.
Есть клиенты этих сервисов
C12(S1, S2), C23(S2, S3), C123(S1, S2, S3)
регистрируется все это
Di.Register<S1>
Di.Register<S2>
Di.Register<S3>
Di.Register<C12>
Di.Register<C23>
Di.Register<C123>
при вызове DI.Resolve<C123> контейнер создаст S1, S2, S3, а при вызове DI.Resolve<C12>, только S1, S2
На кой черт при таком дизайне раздувать контракт интерфейсами — не вижу смысла, т.е. сам по себе DI выделения интерфейса не требует.
Здравствуйте, diez_p, Вы писали:
_>На кой черт при таком дизайне раздувать контракт интерфейсами — не вижу смысла, т.е. сам по себе DI выделения интерфейса не требует.
Две причины.
Интерфейс служит документацией к классу, не загромождая её реализацией, интерфейс позволяет подсунуть другую реализацию в юнит-тестах.
Здравствуйте, scf, Вы писали:
_>>На кой черт при таком дизайне раздувать контракт интерфейсами — не вижу смысла, т.е. сам по себе DI выделения интерфейса не требует. scf>Две причины.
Обсудили же это уже. Плохие это причины.
scf>Интерфейс служит документацией к классу, не загромождая её реализацией,
Такая документация элементарно извлекается автоматически. Писать и поддерживать её вручную — так себе развлечение.
scf>интерфейс позволяет подсунуть другую реализацию в юнит-тестах.
Это проблема конкретных мок-фреймворков. Вменяемые фреймворки умеют мокать публичные методы класса.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Здравствуйте, Sharov, Вы писали:
S>Подождите, мы DI рассматриваем в контексте IoC контейнеров, где мы в явном виде new никогда не вызываем, а используем, S>например, абстрактную фабрику. Ну так вот IoC у меня созвучен с dip, в том плане, что это dip для контроля управления. S>А dip говорит, что завязываться на детали (реализации) плохо, лучше завязываться на абстракции. Т.е. мы куда-то встраиваемся S>(во фреймворк) через интерфейсы и абстрактные классы.
DIP во многом зависит и от ЯП, и тут есть некоторые нюансы, и Мартин об этом писал, кстати, что в C++ хидер-файл включает все детали класса, включая все его приватные методы или поля, и даже инклюдит соответственно то, что ему нужно для приватных членов, и "настоящее" разделение там проще сделать через наследование от чистого абстрактного класса (считай это интерфейс в Java/C#).
В Java/C# — напротив — мы имеем автоматическое отделение интерфейса от реализации, в добавок с символическим разрешением зависимостей в рантайме. Ты можешь положить в сборку MyClass1 и будешь видеть исключительно его публичный контракт (публичный интерфейс), без каких либо дополнительных телодвижений, а его изменение лайаута — тебя вообще не волнует, рантайм позабоится об этом сам. И таким образом MyClass1 для всех потребителей — и есть уже эта самая абстракция, интерфейс.
Введение же интерфейсов или абстрактных классов в терминах C#/Java — должны уже иметь на это основания: точки расширения в фреймворках, множество реализаций и т.п.
Здравствуйте, Sharov, Вы писали:
S>Подождите, мы DI рассматриваем в контексте IoC контейнеров, где мы в явном виде new никогда не вызываем, а используем, S>например, абстрактную фабрику. Ну так вот IoC у меня созвучен с dip, в том плане, что это dip для контроля управления. S>А dip говорит, что завязываться на детали (реализации) плохо, лучше завязываться на абстракции. Т.е. мы куда-то встраиваемся S>(во фреймворк) через интерфейсы и абстрактные классы.
В целом, безудержная и бездумная трактовка каких-то принципов, правил, и т.п. приводит к совершенно чудовищным и где-то даже спорным решениям.
Раз, для тебя близок ASP.NET Core, — то далеко ходить и не будем. Можно взять его ILogger, как пример. К нему, как к инфраструктурному компоненту должны предъявляться повышенные требования, а на деле — он сам себя закрывает за ущербным интерфейсом из 3 методов, при чём поведение третьего метода — не гарантированно (scoped logging). Реально используемые методы (типа LogDebug) — выполнены вообще в виде экстеншнов, хотя здесь практически нет места свободному расширению, как в LINQ, которые в итоге форсируют использовать using namespace, даже если у тебя доступ к логгеру уже есть.
Ну и вишенка на торте, что практически у любого логгера первое, что он делает, это условие на подобии такого:
if (logEventLevel < _currentMinimumLevel) return;
// slow path
Так вот, в здоровых фреймворках, это условие всегда инлайнится, позволяя опустить сам вызов полностью. В добавок даже если вызов и произойдет — в вызове на подобии LogDebug, даже нет смысла в коде передавать уровень как параметр — уменьшая этим самым непосредственно размер исполнимого кода, при этом сохранив цепочку вызовов минимальной (у MS.Logging этого не происходит, спасибо экстеншн методам).
Ровно как и здоровые фреймворки, никогда не будут делать так:
public void LogTrace<T1>(Exception exception, string message, T1 arg1);
public void LogTrace<T1, T2>(Exception exception, string message, T1 arg1, T2 arg2);
public void LogTrace<T1, T2, T3>(Exception exception, string message, T1 arg1, T2 arg2, T3 arg3);
Да, да, всё ради того же: избавиться от холостых выделений памяти, да и generic инвокация окажется короче. Но, опять же, это невозможно, т.к. мы ограничены ущербным интерфейсом.
Это не особо важно для уровней Info и выше, т.к. они практически всегда логгируются, и механика материализации съест немало, но уровни Trace и Debug — в нормальном режиме практически всегда подавлены и достаточно чувствительны к этому. (И не надо петь про дешевость вызовов — любой, даже безусловный переход (jmp) съедает слот в предсказателе переходов.)
Ну и с практической точки зрения, для пользователя, абсолютно фиолетово, что использовать ILogger, Logger, тем более в контейнере можно ещё зарегистрировать и NLog.Logger и Serilog и всё это будет вместе жить через адаптеры, при чём бэкэндом будет любой из них. В конце концов Microsoft.Extensions.Logging — по отношению к твоему коду — просто ещё одна бибилиотека со своим интерфейсом, и фактически — со своим поведением и с единственной реализацией.
Там есть и другие нюансы у них, но по сути, я вел не к тому, какое плохое или хорошее у них логгирование (оно вполне достаточное и фреймворк достаточно универсальный... что бы его не использовать), а просто пример, того, как легко запереть себя в "абстракциях" отрезав любые эффективные решения как класс. В данном случае — абстракция (в виде интерфейса) — вообще не нужна.
Здравствуйте, Mystic Artifact, Вы писали:
S>>Подождите, мы DI рассматриваем в контексте IoC контейнеров, где мы в явном виде new никогда не вызываем, а используем, S>>например, абстрактную фабрику. Ну так вот IoC у меня созвучен с dip, в том плане, что это dip для контроля управления. S>>А dip говорит, что завязываться на детали (реализации) плохо, лучше завязываться на абстракции. Т.е. мы куда-то встраиваемся S>>(во фреймворк) через интерфейсы и абстрактные классы. MA> DIP во многом зависит и от ЯП, и тут есть некоторые нюансы, и Мартин об этом писал, кстати, что в C++ хидер-файл включает все детали класса, включая все его приватные методы или поля, и даже инклюдит соответственно то, что ему нужно для приватных членов, и "настоящее" разделение там проще сделать через наследование от чистого абстрактного класса (считай это интерфейс в Java/C#).
Согласен. Стоит отметить, что это тоже довольно условно относится к интерфейсам. В плюсах разделять можно например через CRTP или даже pImpl, без всякого наследования и интерфейсов. И уж тем более это всё ортогонально к DI.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Здравствуйте, diez_p, Вы писали:
S>>А зачем тогда DI? А если я конфигурирую реализации в конфиге, и тип узнаю только в runtime? _>DI решает какие объекты когда создавать, так же он заботится о том, чтобы при вызове конструктора типа, туда были переданы правильные требуемые инстансы. _>Все равно непонятно почему даже в конфиге вам надо указывать именно интерфейсы, а не конкретные реализации.
Вероятно я был не так понят, но в конфиге мы указываем реализацию, а не интерфейс. Интерфейс в коде, в конфиге только реализация.
_>Конфиг для контейнера нужен, если кто-то меняет реализацию без пересборки проекта, если этого не происходит выкинуть конфиг и зашить все в код.
Так то оно так, но для тестируемости и гибкости лучше все же оставить интерфейсы. Если бизнес требования поменяются?
Подправил конфиг и всех делов.
_>Условно есть три сервиса _>S1, S2, S3 — конкретные типы.
_>Есть клиенты этих сервисов _>C12(S1, S2), C23(S2, S3), C123(S1, S2, S3) _>регистрируется все это _>Di.Register<S1> _>Di.Register<S2> _>Di.Register<S3> _>Di.Register<C12> _>Di.Register<C23> _>Di.Register<C123>
_>при вызове DI.Resolve<C123> контейнер создаст S1, S2, S3, а при вызове DI.Resolve<C12>, только S1, S2 _>На кой черт при таком дизайне раздувать контракт интерфейсами — не вижу смысла, т.е. сам по себе DI выделения интерфейса не требует.
Если завязываться на DI, то уж лучше использовать интерфейсы и пользоваться всеми плюшками DI. В противном случае и DI не нужен.
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, diez_p, Вы писали:
S>>>А зачем тогда DI? А если я конфигурирую реализации в конфиге, и тип узнаю только в runtime? _>>DI решает какие объекты когда создавать, так же он заботится о том, чтобы при вызове конструктора типа, туда были переданы правильные требуемые инстансы. _>>Все равно непонятно почему даже в конфиге вам надо указывать именно интерфейсы, а не конкретные реализации.
S>Вероятно я был не так понят, но в конфиге мы указываем реализацию, а не интерфейс. Интерфейс в коде, в конфиге только реализация.
_>>Конфиг для контейнера нужен, если кто-то меняет реализацию без пересборки проекта, если этого не происходит выкинуть конфиг и зашить все в код.
S>Так то оно так, но для тестируемости и гибкости лучше все же оставить интерфейсы. Если бизнес требования поменяются?
Вот как поменяются бизнес требования, так и допишите, при этом интерфейс возможно и поменяется.
Это называется излишнее проектирование наперед. Об это много пишут в книжках.
Ну и потом такой подход это сродни инвестированию. Добавить интерфейсы в Java/C# и заменить вызовы делется быстро, нет смысла это делать наперед. Если же делать что-то наперед тяжело изменяемое, то для этого надо иметь весомые обоснования.