Зависимость Композиция корня и декоратора
Я получаю StackoverflowException
в моей реализации шаблона декоратора при использовании инъекции зависимостей. Я думаю, что это потому, что я "пропускаю" что-то из своего понимания DI/IoC.
Например, в настоящее время у меня есть CustomerService
и CustomerServiceLoggingDecorator
. Оба класса реализуют ICustomerService
, и весь класс декоратора делает это с помощью вложенного ICustomerService
, но добавляет несколько простых журналов NLog, поэтому я могу использовать ведение журнала, не затрагивая код в CustomerService
, а также не нарушая принцип единой ответственности.
Однако проблема заключается в том, что, поскольку CustomerServiceLoggingDecorator
реализует ICustomerService
, и ему также нужна реализация ICustomerService
, введенная в нее для работы, Unity будет пытаться разрешить ее обратно к себе, что вызывает бесконечный цикл до тех пор, пока он переполняет стек.
Это мои услуги:
public interface ICustomerService
{
IEnumerable<Customer> GetAllCustomers();
}
public class CustomerService : ICustomerService
{
private readonly IGenericRepository<Customer> _customerRepository;
public CustomerService(IGenericRepository<Customer> customerRepository)
{
if (customerRepository == null)
{
throw new ArgumentNullException(nameof(customerRepository));
}
_customerRepository = customerRepository;
}
public IEnumerable<Customer> GetAllCustomers()
{
return _customerRepository.SelectAll();
}
}
public class CustomerServiceLoggingDecorator : ICustomerService
{
private readonly ICustomerService _customerService;
private readonly ILogger _log = LogManager.GetCurrentClassLogger();
public CustomerServiceLoggingDecorator(ICustomerService customerService)
{
_customerService = customerService;
}
public IEnumerable<Customer> GetAllCustomers()
{
var stopwatch = Stopwatch.StartNew();
var result = _customerService.GetAllCustomers();
stopwatch.Stop();
_log.Trace("Querying for all customers took: {0}ms", stopwatch.Elapsed.TotalMilliseconds);
return result;
}
}
В настоящее время у меня установлены настройки регистрации (этот метод заглушки был создан Unity.Mvc
):
public static void RegisterTypes(IUnityContainer container)
{
// NOTE: To load from web.config uncomment the line below. Make sure to add a Microsoft.Practices.Unity.Configuration to the using statements.
// container.LoadConfiguration();
// TODO: Register your types here
// container.RegisterType<IProductRepository, ProductRepository>();
// Register the database context
container.RegisterType<DbContext, CustomerDbContext>();
// Register the repositories
container.RegisterType<IGenericRepository<Customer>, GenericRepository<Customer>>();
// Register the services
// Register logging decorators
// This way "works"*
container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>(
new InjectionConstructor(
new CustomerService(
new GenericRepository<Customer>(
new CustomerDbContext()))));
// This way seems more natural for DI but overflows the stack
container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>();
}
Итак, теперь я не уверен в правильном способе создания декоратора с инъекцией зависимости. Я основал свой декоратор на Mark Seemann, отвечая здесь. В его примере он создает несколько объектов, которые передаются в класс. Вот как мой "работает" * работает фрагмент. Однако, я думаю, что я пропустил фундаментальный шаг.
Почему вручную создавать новые объекты, подобные этому? Разве это не отрицает того, что контейнер для меня разрешает? Или я должен вместо этого contain.Resolve()
(локатор сервисов) в рамках этого одного метода, чтобы все вложенные зависимости все еще были?
Я немного знаком с концепцией "составной корень", в которой вы должны подключать эти зависимости в одном и только одном месте, которое затем каскадируется до нижних уровней приложения. Итак, Unity.Mvc
сгенерированный RegisterTypes()
корень композиции приложения ASP.NET MVC? Если это так, правильно ли здесь быть непосредственно новыми объектами?
У меня создалось впечатление, что, как правило, с Unity вам нужно создать собственный корневой состав, однако Unity.Mvc
является исключением из этого в том, что он создает свой собственный корневой состав, поскольку он, похоже, способен вводить зависимости в контроллеры которые имеют интерфейс, такой как ICustomerService
в конструкторе, без того, что я пишу код, чтобы сделать это.
Вопрос. Я считаю, что мне не хватает ключевой информации, которая приводит меня к StackoverflowExceptions
из-за круговых зависимостей. Как я правильно реализую свой класс декоратора, все еще следуя за зависимостями впрыска/инверсии принципов и соглашений управления?
Второй вопрос: А если я решил, что я только хотел применить декоратор журнала в определенных обстоятельствах? Поэтому, если у меня было MyController1
, что я хотел иметь зависимость CustomerServiceLoggingDecorator
, но MyController2
нужен только обычный CustomerService
, как мне создать две отдельные регистрации? Потому что, если я это сделаю:
container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>();
container.RegisterType<ICustomerService, CustomerService>();
Затем будет перезаписано значение, означающее, что оба контроллера будут либо снабжены инжектором декоратора, либо обычной инъекцией. Как мне разрешить оба?
Изменить: это не дублирующий вопрос, потому что у меня возникают проблемы с круговыми зависимостями и недостаточным пониманием правильного подхода DI для этого. Мой вопрос относится ко всей концепции, а не только шаблон декоратора, как связанный вопрос.
Ответы
Ответ 1
Преамбула
Всякий раз, когда у вас возникают проблемы с контейнером DI (Unity или иным способом), спросите себя: использует контейнер DI, который стоит усилий?
В большинстве случаев ответ должен быть не. Вместо этого используйте Pure DI. Все ваши ответы тривиальны, чтобы ответить Pure DI.
Unity
Если вы должны использовать Unity, возможно, следующее поможет. Я не использовал Unity с 2011 года, поэтому с тех пор ситуация изменилась, но в разделе 14.3.3 рассмотрена проблема в в моей книге, что-то вроде этого может сделать трюк:
container.RegisterType<ICustomerService, CustomerService>("custSvc");
container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>(
new InjectionContructor(
new ResolvedParameter<ICustomerService>("custSvc")));
Кроме того, вы также можете это сделать:
container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>(
new InjectionConstructor(
new ResolvedParameter<CustomerService>()));
Этот вариант проще поддерживать, поскольку он не полагается на именованные службы, но имеет (потенциальный) недостаток, который невозможно разрешить CustomerService
через интерфейс ICustomerService
. Вы, вероятно, не должны этого делать, так что это не должно быть проблемой, поэтому, вероятно, это лучшая альтернатива.
Ответ 2
Вопрос: Я считаю, что мне не хватает ключевого фрагмента информации, что приводит меня к выводам StackoverflowException из-за круговых зависимостей. Как я правильно реализую свой класс декоратора, все еще следуя за зависимостями впрыска/инверсии принципов и соглашений управления?
Как уже указывалось, лучший способ сделать это - со следующей конструкцией.
container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>(
new InjectionConstructor(new ResolvedParameter<CustomerService>()));
Это позволяет указать, как параметры разрешаются по типу. Вы также можете сделать это по имени, но по типу - это более чистая реализация и позволяет лучше проверять во время компиляции, так как изменение или ошибка в строке не будут обнаружены. Обратите внимание, что единственная минута разницы между этой частью кода и кодом, предложенным Марком Семаном, - это исправление в написании InjectionConstructor
. Я больше не буду останавливаться на этой части, так как больше нечего добавить, что Марк Семанн еще не объяснил.
Второй вопрос: а как же, если я решил, что я только хотел применить декоратор регистрации в определенных обстоятельствах? Поэтому, если у меня был MyController1, я хотел бы иметь зависимость CustomerServiceLoggingDecorator, но MyController2 нужен только обычный CustomerService, как мне создать две отдельные регистрации?
Вы можете сделать это, используя способ, указанный выше, с использованием нотации Fluent или с использованием именованной зависимости с переопределением зависимостей.
свободный
Это регистрирует контроллер с контейнером и задает перегрузку для этого типа в конструкторе. Я предпочитаю этот подход во втором, но это зависит от того, где вы хотите указать тип.
container.RegisterType<MyController2>(
new InjectionConstructor(new ResolvedParameter<CustomerService>()));
Именованная зависимость
Вы делаете это точно так же, вы регистрируете их оба так.
container.RegisterType<ICustomerService, CustomerService>("plainService");
container.RegisterType<ICustomerService, CustomerServiceLoggingDecorator>(
new InjectionConstructor(new ResolvedParameter<CustomerService>()));
Разница здесь в том, что вы используете именованную зависимость вместо других типов, которые могут быть разрешены с использованием одного и того же интерфейса. Это связано с тем, что интерфейс должен быть разрешен только к одному конкретному типу каждый раз, когда решение разрешено Unity, поэтому вы не можете иметь несколько неназванных зарегистрированных типов, зарегистрированных на одном и том же интерфейсе. Теперь вы можете указать переопределение в своем конструкторе контроллера с использованием атрибута. Мой пример - для контроллера с именем MyController2
, и я добавил атрибут Dependency
с именем, указанным выше в регистрации. Таким образом, для этого конструктора вместо CustomerServiceLoggingDecorator
type будет введен тип CustomerService
. MyController1
по-прежнему будет использовать невынужденную регистрацию по умолчанию для ICustomerService
, которая является типом CustomerServiceLoggingDecorator
.
public MyController2([Dependency("plainService")]ICustomerService service)
public MyController1(ICustomerService service)
Есть также способы сделать это, когда вы вручную разрешаете тип в самом контейнере, см. "Разрешение объектов с помощью переопределений" . Проблема здесь в том, что вам нужен доступ к самому контейнеру, чтобы сделать это, что не рекомендуется. В качестве альтернативы вы можете создать обертку вокруг контейнера, которую затем вводите в контроллер (или другой тип), а затем извлекаете тип таким образом с помощью переопределений. Опять же, это становится немного грязным, и я бы избегал его, если это было возможно.
Ответ 3
На основе второго ответа Mark я бы хотел зарегистрировать CustomerService
с помощью InjectionFactory
и зарегистрировать его только с типом службы без интерфейса:
containter.RegisterType<CustomerService>(new InjectionFactory(
container => new CustomerService(containter.Resolve<IGenericRepository<Customer>>())));
Тогда это позволит, как и в ответе Марка, зарегистрировать объект ведения журнала, например:
containter.RegisterType<ICutomerService, CutomerServiceLoggingDecorator>(new InjectionConstructor(
new ResolvedParameter<CustomerService>()));
Это в основном тот же метод, который я использую всякий раз, когда требуется, чтобы что-то было лениво загружено, так как я не хочу, чтобы мои объекты зависели от Lazy<IService>
и, обернув их в прокси-сервер, позволяет мне вводить только IService
, но он лениво разрезал через прокси.
Это также позволит вам выбрать, куда вводится объект регистрации или обычный объект, вместо того чтобы требовать строки magic
, просто разрешив CustomerService
для вашего объекта вместо ICustomerService
.
Для журнала CustomerService
:
container.Resolve<ICustomerService>()
Или для не-протоколирования CustomerService
:
container.Resolve<CustomerService>()