Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
Во вторых используя развитые инструменты (например VS+Resharper) вы точно также получите в один клик все зависимости списком и можете их скопом изменить.
Я чего то не понял, зачем Вам получать все зависимости класса, использующего сервис локатор.
Я что то неправильно понял?
Вы знаете, что все его зависимости выражены в виде вызовов сервис локаторов.
Начинаете поиск по инстансу сервис локатора
дальше в результате поиска сморите вызовы для своего класса
В чем проблема?
Я всего лишь хотел сказать что технические проблемы конкретного инструментария — это технические проблемы конкретного инструментария.
А концепция не становится хуже в принципе при наличии и отсутствии подобных проблем.
Calling SERVICE LOCATOR an anti-pattern is controversial. Some people consider it a proper design pattern, whereas others (me included) consider it an anti-pattern. In this book, I’ve decided to describe it as an antipattern
because I think its disadvantages are greater than its advantages; but don’t be surprised if you see it endorsed in other places. The important thing is to understand the benefits and shortcomings enough to be able to make an informed decision for yourself.
«Control Freak». Руководитель-наркоман.
Зависимости просто могут быть исходя из логики.
Но теперь любой код, создающий экземпляр, должен знать почему-то, что внутри этого класса используется ProductRepository repository. И передавать его в конструктор. Просто взять и подумать, сколько раз будут повторения у создающего кода?..
class ProductServiceFactory
{
class ProductService: IProductService
.......
public static IProductService Create()
{
IProductRepository repository = ProductRepositoryFactory.Create();
return new ProductService(repository);
}
}
public void ProcessMessage(Message message)
{
if (handledMessageFuncs.Contain(message.Type) && IsValid(message))
{
hadledMessageFuncs[Message.Type](message.Parameters);
}
}
Вот это плохо: public ProductService(ProductRepository repository) Потому что класс ProductService получает ссылку на конкретный класс, т.е. на конкретную реализацию.ProductRepository — это конкретная реализация.Но тогда любой внешний код при создании ProductService, должен знать то, что должно быть скрыто, а именно: что класс ProductService использует IProductRepository. [...] И банально, если этот сервис часто создается, кругом будет дублирование кода.
IProductRepository должен быть ровно в одном месте. Никто из пользователей ProductService не должен создавать его напрямую, потому что это тоже нарушение IoC.С чего вдруг? Это кишки сервиса.
IProductRepository он пользуется.нужно приблизительно так:
class ProductServiceFactory { class ProductService: IProductService ....... public static IProductService Create() { IProductRepository repository = ProductRepositoryFactory.Create(); return new ProductService(repository); } }
Оно вроде и красиво, но накладно каждый раз писать фабрики.
А если вы разбрасываете логику, пытаясь разделить зависимости просто ради разделения — то это может быть и плохо.
Кроме последней идеи — «не убрал конкретных зависимостей». К счастью не убрал. Кой какие зависимости быть обязаны, потому что кода не бывает ради кода, как фильмов ради режиссеров. Код должен описывать смысл. И зависимости — не что иное, как статическая типизация. Иначе получится холодец, а не код. Очень гибкий, настраиваемый, но любая настройка делает его ненадежным.
Никто не мешает фабрику использовать одну на все сервисы (в смысле — в проекте).
А то, что фабрика своя на каждый проект — так это нормально, это composition root так выражен.
public interface IAClass
{
string Foo();
}
public class A:IAClass
{
public string Foo()
{
return "In instance A";
}
}
public interface IBClass
{
string Foo();
}
public class B : IBClass
{
public string Foo()
{
return "In instance B";
}
}
public class GenericFactory<T>
{
sealed class ConstructorCreator
{
static ConstructorCreator()
{
ConstructorCreator<IAClass>.BindType<A>(); // Здесь по одной строке мы объявляем, какой тип соответствует интерфейсу
ConstructorCreator<IBClass>.BindType<B>();
}
public static Func<Ti> GetDefaultConstructor<Ti>()
{
return ConstructorCreator<Ti>.defaultConstructor;
}
}
sealed class ConstructorCreator<Tc>
{
public static Func<Tc> defaultConstructor;
public static Func<Tc> DefaultConstructor
{
get { return ConstructorCreator.GetDefaultConstructor<Tc>(); }
}
public static void BindType<TInheritor>() where TInheritor : Tc
{
var costructor = typeof(TInheritor).GetConstructor(new Type[] { });
defaultConstructor = Expression.Lambda<Func<Tc>>(LambdaExpression.New(costructor)).Compile();
}
}
public static T CreateInstance()
{
if (ConstructorCreator<T>.DefaultConstructor == null)
{
throw new NullReferenceException(string.Format("GenericFactory is not initialized for type {0}", typeof(T).FullName));
}
return ConstructorCreator<T>.DefaultConstructor();
}
}
Console.WriteLine(GenericFactory<IAClass>.CreateInstance().Foo());
Console.WriteLine(GenericFactory<IBClass>.CreateInstance().Foo());
Давайте так, прямо из вики:
Service service = (Service)DependencyManager.get(«CarBuilderService»)
1. По всей видимости эта штука возвращает object. Это значит, что бага проявится только в рантайме, если вызовут этот код. А это ухудшает надежность. Этот код может и через год завалить систему. И компилятор тут не причем.
2. Имя сервиса в стринге — то же самое — не статическая типизация.
var service = ServiceLocator.Get<IService>();
И где много кода?
var container = new UnityContainer();
container.RegisterType<IAClass, A>();
container.RegisterType<IBClass, B>();
var locator = new UnityServiceLocator(container);
ServiceLocator.SetLocatorProvider(() => locator);
Console.WriteLine(ServiceLocator.Current.GetInstance<IAClass>().Foo());
GetInstance вместо CreateInstance?) и вообще управление временем жизниА если не конфиги, то этот паттерн ничем или очень мало чем отличается от фабрик. Примеры выше. Т.е. даже называть его dependency injection как-то язык не поворачивается.
ServiceLocator.Current.GetInstance<IA>, может быть только параметр в конструкторе с типом IA (вбрасывание в конструктор), или свойство с типом IA (вбрасывание в свойство), или параметр в методе с типом IA (вбрасывание в метод). И никак иначе. Вот это — Dependency Injection, со всеми его достоинствами и недостатками.Незачем. Но смотря какой код. Какие размеры и какая сложность.
Этот код сложный? Нет.
В частности, неокрепшие умы могут увидеть чрезвычайную гибкость (конфиги) и поломать весь шарп.
Так что фабрики рулят.
Оно конечно, не делает гибкой архитектуру (а зачем???).
Ну, очевидно же, что этот паттерн использовать хорошо разве что для внешних вещей.
Это утверждение никак не доказано.
Т.е. плюс — статическая типизация.
Была бы фабрика, я бы быстро нашел в коде, что за тип используется (ну бывает, надо ходить по коду и смотреть, как он устроен),
А заодно и фабрику.
Я успешно в приватные свойства и поля хожу.
Вам уже явно показали, что DI тоже работает со статической типизацией.
В нашем шарпе нельзя четко сказать, статическая или нестатическая.
Если есть фабрика, то ест и метод, который возвращает экземпляр.
Все эти проверки компилятора в случае паттерна из статьи — уходят в рантайм.
Можно. Тип каждой переменной известен в момент компиляции.
void Foo(object someData)
{
if (someData is TextBox)
{
((TextBox)someData).Text = "This is TextBox";
}
else if (someData is ProductService)
{
((ProductService)someData).SendAnyText("Hello, ProductService");
}
}
А такой код, на шарпе:
Так что не надо относится к коду шарпа так легко
Компилятор в данном случае не знает реального типа данных во время компиляции.
Пока вы будете думать о том, что имя имеет зарегистрированный тип — ваши знания о статической типизации бесполезны.
И вот эту проверку можно столькими способами в шарпе куда подальше послать.
Ну то есть вы считаете, что все ошибаются, один вы знаете правильный термин.
В шарпе и имена имеют тип и данные тоже. Т.е. экземпляр в памяти имеет тип и с ним нельзя работать иначе.
Поэтому там так обтекаемо и написали «The C# type system performs static-like compile-time type checking».
А вообще, непонятно, почему мы спорим.
Какого паттерна? О каком именно паттерне вы говорите, конкретное его название?
class Dependent
{
public Dependent(IDependency dependency)
{
....
}
}
dependency == null (как и в случае с вашей фабрикой). От таких вещей ставят guard condition и забывают про них, как про страшный сон.Dependency Injection скрывает реализации от нас, а также от компилятора, что явный минус.
И еще скрывает от нас в плане читаемости кода. Была бы фабрика, я бы быстро нашел в коде, что за тип используется (ну бывает, надо ходить по коду и смотреть, как он устроен)
Если бы шарп позволял на этапе компиляции проверить, все ли реализации прикреплены (сразу, пусть даже потом меняются, но нал не допускать), то я бы намного проще соглашался.
Я в принципе против «преждевременной гибкости». Это не столько и не только к IoC относится.
Эксперимент с такими сплошными интерфейсами — это попытки с него сделать нечто другое — всё что крякает — утка. Возможно вы не тот язык выбрали?
IoC — это не «преждевременная гибкость», это «гибкость по месту».
«Все, что крякает — утка» — это duck typing, к интерфейсам отношения не имеет.
На счет, против ли я вообще, то я разве где-то отказывался от фабрик? Нет.
И отказываюсь пользоваться движками, сокращающими написание кода, потому что там неявно прикрепляется реализация, что ошибки уносит в рантайм
(«Code Contracts со включенным static checking» — не знаю, что это такое, явно, не шарп)
Если вам кажется такая гибкость сразу — нормально, то мне «слишком»
Определить что есть кряканье, а потом в рантайме цеплять тех, кто крякает, ну очень мало по смыслу отличается от «просто в рантайме определить кто имеет подходящее поведение».
В общем, экспериментируйте, а я на такие настраиваемые движки перейду не скоро.
и, значит, как минимум на нуллрефы вы готовы.
гарантированно создавать фабрики, которые не умеют налы возвращать. Несбыточная мечта.
я когда и фабрики пишу, нулрефы не использую. Благо есть return из любого места
null вместо зависимости гарантируется только доброй волей программиста.Не понимаю, почему вдруг решили, что надо держать промежуточную ссылку, пока не создадут класс.
Вы можете это проверить на этапе компиляции? Нет, не можете. Но используете тем не менее.
Собственно, это призвано продемонстрировать тот факт, что код с фабриками ничем не более верифицируем на этапе компиляции, нежели код, написанный по правилам IoC
Фабрика, когда пишется для интерфейса — она одна. На нее выводит студия, когда ищешь интерфейс или класс. На конфиг ничего не выводит. Там скорее всего списком пишутся типы. Пропустить что-то проще. Точно так же в женерик-фабрике. Создали новый интерфейс, создали класс, но не зарегистрировали. Каким образом это обнаружить?
Теперь вызывающий класс с т.з. классического ООП, должен знать о реализации данного класса
interface IDbProvider {}
class DbProvider: IDbProvider {}
interface IRepository {}
class Repository: IRepository {
public Repository(IDbProvider provider) {}
}
class Controller {}
public ActionResult SomeAction()
{
// ....
(new Repository(new Provider)).GetSomeData()
// ....
}
public Controller(IRepository repository)
{
_repository = repository;
}
public ActionResult SomeAction()
{
// ....
_repository.GetSomeData()
// ....
}
То используя фокус с неинициализированным объектом, вы получаете для тестов объект с null (конструктор не вызывался). Далее, делаете в тесте прямо нужную вам инъекцию — мок репозитория. И профит
Далее, т.к. вызов конструктора может невероятно распухать, то куда деваться — находим удобный фреймворк, разрешающий только типы связывать
Эти вещи в моем воображении скрывает конструктор. Т.е. конструктор берет на вход то, что логично для создающего его класса, чтобы не знать о внутренней реализации класса. А что он может создать сам и клиента класса это не должно волновать, то он сам это и делает.
Скажем, вам надо использовать IDataReader к какой-то вымышленной IDataBase. При этом прикрепили реализацию провайдера к SQL-Server, а датаридер к MySql.
Опыта нет, не видел кода, где кругом через интерфейсы создается всё невидимыми фабриками.
Код надо создавать так, чтобы реализации были согласованными.
Передача моков через конструктор не избавляет от проблемы — как тестировать закрытые методы.
Я сторонник тестирования и закрытых методов.
Если надо протестировать какой-то метод, то мне не обязательно инициализировать громоздкий конструктор (у вас с моками в конструктор может быть даже хуже в плане экономии усилий).
Я просто создаю объект и выставляю поля и свойства, нужные только для его окружения и тестирую.
public void TransferData()
{
using(var cn = new SqlConnection(someConnectionString))
{
// здесь получаем из коннекшна команду, из нее ридер, из него - строки
// для наглядности пропущено нафиг
using(var c = new SomeWebServiceClient(someUri))
{
foreach(var row in rows)
{
c.PutData(row)
}
}
}
}
Я сторонник того, что код в первую очередь должен служить требованиям, а не тестированию
Во-первых, как вы можете проверить соответствие кода требованиям без тестирования?
А во-вторых, если вы сторонник того, что код должен служить требованиям — зачем вы тестируете закрытые методы, которые требованиями не покрываются?
Отвечу пока только на часть, потому как с вами не поработаешь ))
Т.е. на требования самого программиста.
Тесты по хорошему должны тестировать каждую ветку процесса в коде.
Требования самого программиста тоже могут предъявляться только к публичному контракту класса. Смотрите Design by Contract.
Пытаться протестировать каждую ветку — это тестирование прозрачного ящика, вы ставите тест в зависимость от (возможно некорректной) реализации.
Если будут контрактники пытаться лучше покрыть тестами, возможно, количество контрактов начнет расти необоснованно.
Программист пишет юнит-тест на требования, исходящие от него самого.
Или вы думаете, если программист пишет «if», он не ставит никаких требований к этому ифу, просто потому, что в контракте это не описано?
if.Ваши контракты, это где-то между
Накладно через контракты тестить каждую ветку.
if в коде — а я считаю, что надо сначала покрыть тестами все возможные сочетания входных данных (и не надо мне говорить, что мы о них не знаем — если вы писали if, значит, вы думали о каких-то входных данных), а потом уже писать под это код. Если в процессе написания кода нам пришло в голову что-то еще — надо выразить это через входные данные, а потом уже писать. Если эту ситуацию нельзя выразить через входные данные, то ее не существует.Вы считаете, что надо тестировать каждый if в коде — а я считаю, что надо сначала покрыть тестами все возможные сочетания входных данных (и не надо мне говорить, что мы о них не знаем — если вы писали if, значит, вы думали о каких-то входных данных), а потом уже писать под это код.
Т.е. вы пытаетесь «дать сочетание данных» в контракт так, чтобы покрыть все случае
Он мыслит, скорее всего так перед кодированием: «сейчас мне нужно, чтобы система меняла свое поведение в зависимости от такого условия». Это и есть требование к «иф».
if, это требование к системе. Но это затрудняет создавать предусловия.
Вообще, вы сейчас входите в противоречие, пытаясь навязать, что правильно писать через контракты и т.д.
И даже в любых языках, в которых понятие контракт не имеет смысла.
Юнит-тест — это то, что я написал — закрепляет на уровне утверждений поведение кода.
Что значит «дать данные в контракт»? Что такое по-вашему «контракт»?
Нет, это не требование к if, это требование к системе.
Почему затрудняет, если это предусловие по определению внешнее по отношению к SUT?
Писать через контракты — правильно. Но юнит-тесты тут не при чем (их просто проще писать в условиях явно оформленных контрактов).
Это какие языки, интересно?
Что такое «утверждение» в вашем понимании?
Прекрасно. Вот типичный для direct control метод:
хотя я сразу во втором комментарии описал, что я имею ввиду и что совершенно не собирался что-то критиковать. Просто неточность в статье.
Если цепляетесь к словам, можно водить хороводы по кругу до скончания времен.
А именно: «точные и верифицируемые спецификации интерфейсов для компонентов системы»
Тем более, выше несколько раз написал
Кстати, и здесь, почему-то вы воспринимаете на свой счет изначально
интуитивно человек считает одну часть очень важной — интерфейсы, а другую — плохой и не стоящей внимания
Есть разница, когда человек фор использует, а когда форич. Второй конструкцией человек явно указывает читателю кода, что ему порядок не имеет значения. Есть разница, вызвать у Enumerable Sum() или форич +=, первым способом человек указывает, что ему не важен алгоритм суммы (они бывают разные). Есть разница, создает интерфейс IList<>, или List<>. В первом случае говорит, что ему важен порядок элементов, но не важна реализация.
Первое правило — код должен быть ясным локально.
Всякие умные техники скрывания за интерфейсами, за конфигами, прятание реализации, не являются обязательно кореллирующими с понятностью кода.
Я ничего лишнего никогда в конструктор не передам. Только если реально класс что-то не может сам создать, потому что это что-то зависит от внешнего к классу окружения.
Декларативные. Т.е. если весь код состоит только из деклараций, то странно описывать параллельно к этому коду декларации в виде контрактов. В коде прямо они и описываются.
Главная ценность юнит-тестов — отлавливание багов до комита. И они отлавливаются, когда внимательно ветки протестировать.
На уровне аналогий и интуиции много раз объяснил.
Вам я тоже попытаюсь, а может уже не вам, еще раз аналогией, где-то интуицией объяснить, что код с интерфейсами, это почти что код с фекалиями.
Теперь, сервис-локатор. Первый кандидат на сайд эффект. Если вы тестируете метод, а в методе создаете некий интерфейс через сервис-локатор, а не передавая через параметры или не создавая явно класс с помощью new, то работа этого интерфейса уже создает сайд-эффект.
вы можете протестировать код метода, но при этом в рантайме сервис-локатор породит другой класс и ваш код будет вести себя иначе, чем в тестах
Моки нужны только там, где они имеют смысл для теста. В других местах лучше, чтобы система вела себя одинаково и определенно.
Значит — любой метод в классе — содержит ваши требования к методу.
Мало того, непокрытая тестов ветка — сразу подсказка, что там может спрятаться потенциальный баг. Непокрытая тестов ветка — это непокрытое тестом поведение при определенных условиях — потенциальный баг.
class FastMath
{
public double Sqrt(double val)
{
if (val < 0)
return 0;
return SqrtImpl(val);
}
public double StupidMath(double val)
{
if (val < 0)
return 0;
var sqrt = SqrtImpl(val);
if (sqrt < 0)
return 0;
return SqrtImpl(sqrt);
}
private double SqrtImpl(double val)
{
if (val < 0)
return 0;
// адская математика
}
}
SqrtImpl не покрыта. Равно как и не покрыта строчка if (sqrt < 0) return 0;. И обе их невозможно покрыть, пользуясь публичным интерфейсом класса.Отнюдь. Вы регулярно критикуете некий подход (более того, группу подходов), но каждый раз, когда я пытаюсь понять основу для этой критики — вы уходите от темы.
Ох. Почитайте МакКоннела. Эти «умные техники» направлены на то, чтобы уменьшить сложность каждого локального куска кода (и сделать его более понятным). Они направлены на то, чтобы вам не было нужно читать код зависимостей.
IList<int> bindingList = new List<int>();IEnumerable<int> bindingList = new List<int>();</source">
Т.е. читать мне код там где-то не надо. Я декларирую это в типах переменных. Выражаю, так сказать минимализм - суть того, зачем они мне нужны. Зачем в этом случае ходить в код конструктора и смотреть на реализацию? Но при этом везде сохранилась связь для того, чтобы код вел себя определенным неизменяемым способом. Сохранилась жесткость. Я уменьшаю по возможности неконтролируемые этим участком кода сайд-эффекты. Сайд-эффектом в данном случае я называю то, что при одинаковых параметрах метод возвратит разные результаты. Что не так? Потому что это другой метод другого объекта? А кого это волнует, это ж должно быть скрыто.
Ловко вы себя обманываете, закрепляя в тесте одни и те же моки и в границах теста не получая сайд-эффектов. Только интересно, вы за баги болеете? )
Но как только мне вдруг завтра понадобится гибкое поведение, легко заменяю хоть сервис-локатором, хоть через конструктор готовый давать.
<blockquote>Я надеюсь, вы понимаете, что это затрудняет тестирование.</blockquote>
В шарпе нисколько не затрудняет, писал об этом фокусе:
<a href="http://habrahabr.ru/post/166287/#comment_5753359">m36</a>
<blockquote>Это все — читабельность кода внутри метода. Это все прекрасно, но это все не имеет никакого (да, ровным счетом никакого) значения для потребителя этого кода. Для потребителя достаточно того, что код делает обещанное.</blockquote>
Вот, и я об этом. С чего ли в шарпах и джавах люди так ненавидят язык программирования и пользуются любой возможностью, чтобы код хуже (да, эмоции ;). Например, гибким. Страх перед изменениями?
Для потребителя важно то, что код делает обещанное. Только самые дорогие баги, по тому же Макконелу - это неучтенные требования. Код часто о них говорит. Потом, потребитель юнит-ттестов - вы. Для вас, как разработчика класса - все методы открыты.
<blockquote>Я уже объяснял, в чем проблема «тестирования веток» (в противовес «тестированию требований»). Если вкратце — это приводит к ситуации, когда все ветки протестированы, а требования не реализованы тем не менее.</blockquote>
Да, объясняли. И я тоже объяснял. Так может это вы меня не поняли, а не я вас. Попытаюсь послать читать Кента Бека (TDD) . Код - это требования. И если вы пишете сначала тест, ветки еще нет, но порождая требования, далее порождаете ветку в коде, то по сути, такой подход и говорит, что вы тестируете требования. <b>Нет кода. Нет кода никогда.</b> Если у вас такой говнокод, что к нему как к неизвестности относиться можно и по нему нельзя понять, что от него хотели - то тесты, даже тестирующие требования - это уже попытки воскресить мертвого.
Поэтому тестирование всех требований при хорошем коде и при своевременном написании тестов - равносильно тестированию веток. Противоречие вы видите, скорее потому, что представляете тестирование в разрыве от кодирования и когда код уже написан. И еще, скорее, другим человеком. Да, конечно, тогда зачем же закреплять в тестах написанные баги. Тестируют как черный ящик, на всякие неожиданности - тестировщики. А юнит-тесты как раз - прозрачный ящик. Вы пишете тест (свои требования, к новому куску кода, который сейчас родится), потом пишете тест. Получилось много веток - шаг и тест были слишком общими и требование тоже. Понаписывали тесты, порефакторили, посмотрели для обратной связи - сколько веток тесты не проходят? Это все минусы в 100-процентное покрытие. Хотя, конечно, оно не важно. Но обратная связь такая важна, чтобы хотя бы потенциальные проблемы определить. Действительно, на самые мелочи тесты не нужны (хотя скорее полезны, чем вредны). Когда вы пишете тест на ветку, вы не закрепляете ветку. Вы этого сделать не можете. Вы тестируете некоторый участок в области определения функции. Это требование. Не просил пользователь - значит упустили требование. Не просил и это ему не нужно - значит функция делает лишние вещи - потенциальный баг.
Ладно, я думаю, большей ясности мы в общении уже не добьемся. А вырастили уже длинную "ветку"Если мне нужен список, то я пишу: IList<int> bindingList = new List<int>();var и intention-revealing variable names бесполезно?я пишу: IEnumerable<int> bindingList = new List<int>()Выражаю, так сказать минимализм — суть того, зачем они мне нужны.
Сайд-эффектом в данном случае я называю то, что при одинаковых параметрах метод возвратит разные результаты.
Попытаюсь послать читать Кента Бека (TDD)
Код — это требования.
И если вы пишете сначала тест, ветки еще нет, но порождая требования, далее порождаете ветку в коде, то по сути, такой подход и говорит, что вы тестируете требования.
Противоречие вы видите, скорее потому, что представляете тестирование в разрыве от кодирования и когда код уже написан. И еще, скорее, другим человеком.
Я так подозреваю, что давать вам ссылку на Липперта, и его рассуждения про var и intention-revealing variable names бесполезно?
Простите, вы мои фразы про том, что тесты надо писать до кода, целенаправленно игнорируете, или случайно?
Если следовать вашей логике, то для покрытия первой из них надо поднять рефлекшн и впихнуть-таки туда отрицательное значение; что же касается второй — то тут логика пасует.
… ознакомьтесь, все же, с терминологией. Это не сайд-эффект, это non-deterministic behaviour.
Поскольку пишется лишь тот код, что необходим для прохождения теста, автоматизированные тесты покрывают все пути исполнения. Например, перед добавлением нового условного оператора, разработчик должен написать тест, мотивирующий добавление этого условного оператора.
Рассуждения про вар Липерта мне тоже бесполезны. В мире до хрена авторитетов, на любой вкус. Верить всем одновременно — это обманывать себя.
Так если класс обязан иметь конструктор по умолчанию, который будет вызываться к примеру каким-то фреймворком или просто по какому-то паттерну, то как здесь использовать constructor injection?
Между тем, границы принципов внедрения зависимости достаточно размыты. Невозможно провести действительно четкую границу между этим и другими принципами написания качественного объектно-ориентированного кода. Например, принцип Dependency Inversion из SOLID, который часто путают с Dependency Injection, как бы подразумевает внедрение зависимостей, но им не ограничивается.
Dependency Injection: анти-паттерны