Обеспечение зависимостей в IoC через конструктор?

Я пытаюсь смириться с использованием IoC / Dependency Injection, в то же время программируя на контракты, а не на определенные классы. Моя дилемма - это напряжение между:

  1. Программируйте интерфейсы для IoC: я начал с IoC, в значительной степени полагаясь на интерфейсы. Судя по примерам проектов Spring, интерфейсы - это лучший способ программирования в соответствии с контрактом с IoC.

  2. (... хотя обычно предпочтительны абстрактные классы: основным недостатком интерфейсов является то, что они гораздо менее гибкие, чем классы, когда дело доходит до эволюции API)

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

  4. ... за исключением того, что вы не можете принудительно применять подпись конструктора в интерфейсах / абстрактных классах: ни интерфейсы, ни абстрактные классы не позволяют определять подпись конструктора (легко / элегантно). См. Также раздел 4.4 Рекомендаций по разработке фреймворка: НЕ < / strong> определить общедоступные или защищенные внутренние конструкторы в абстрактных типах. ... Конструкторы должны быть общедоступными, только если пользователям нужно будет создавать экземпляры типа.

Этот вопрос связан с предыдущим вопросом о переполнении стека: Интерфейс, определяющий подпись конструктора?

Но у меня вопрос:

Поскольку вы не можете определить конструктор в интерфейсе / абстрактном классе C #, как задается в вопросе выше, на практическом уровне:

Как согласовать это с разумной практикой передачи зависимостей через конструктор?

Изменить: Спасибо за ответы. Я надеюсь получить некоторое представление о том, что мне следует делать в этом случае. Просто не использовать аргументы contructor? Использовать какой-то метод Init (), который принимает зависимости? Edit2: Спасибо за отличные ответы, очень полезно.


person Jeffrey Knight    schedule 16.04.2009    source источник


Ответы (3)


Я всегда думаю, что это легче объяснить на (выдуманном) примере ...

Представьте, что у вас есть интерфейс ICustomerRepository, интерфейс IShoppingCartRepository и интерфейс ICheckout. У вас есть конкретные реализации этих интерфейсов - CustomerRepository, ShoppingCartRepository и CheckoutService.

У вашего конкретного класса CheckoutService есть конструктор, который принимает ICustomerRepository и IShoppingCartRepository, например

public CheckoutService(ICustomerRepository customerRepository, IShoppingCartRepository shoppingCartRepository)
{
  // Set fields for use in some methods later...
  _customerRepository = customerRepository;
  _shoppingCartRepository = shoppingCartRepository;
}

Затем, когда вы хотите, чтобы реализация ICheckoutService выполняла некоторую работу, вы сообщаете своему контейнеру IoC, какой конкретный класс он должен использовать для каждого типа интерфейса, и просите его создать вам ICheckoutService. Контейнер IoC пойдет и создаст для вас ваши классы, внедряя правильные конкретные классы в конструктор вашего CheckoutService. Он также будет строить зависимости на всем протяжении иерархии классов здесь, поэтому, если, например, ваш ShoppingCartRepository принимает интерфейс IDatabaseSession в конструкторе, ваш контейнер IoC также внедрит эту зависимость, если вы указали ему, какой конкретный класс использовать для вашего IDatabaseService.

Вот код, который вы можете использовать при настройке (например) StructureMap в качестве контейнера IoC (этот код будет обычно вызывается при запуске приложения):

public class AppRegistry : Registry
{
  public AppRegistry()
  {
    ForRequestedType<ICheckoutService>().TheDefaultIsConcreteType<CheckoutService>();
    ForRequestedType<ICustomerRepository>().TheDefaultIsConcreteType<CustomerRepository>();
    // etc...
  }
}

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

var checkoutService = ObjectFactory.GetInstance<ICheckoutService>();

Надеюсь это имеет смысл!

person Steve Willcock    schedule 16.04.2009
comment
Ага! Поэтому я могу продолжать использовать аргументы конструктора, чтобы явно указывать зависимости, в то же время программируя контракт. Но позвольте контейнеру IoC позаботиться о правильном внедрении этих зависимостей. Спасибо, это полностью проясняет. - person Jeffrey Knight; 17.04.2009

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

Конструктор - это деталь реализации, поэтому вам не нужно отделять его определение от конкретного класса.

person Michael Meadows    schedule 16.04.2009

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

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

Принцип IoC гласит, что вместо того, чтобы класс A знал и создавал экземпляр класса B, вы должны вместо этого передать ссылку на IB конструктору A. Тогда A не нужно будет знать о классе B, и, таким образом, вы можете легко заменить класс B с какой-то другой реализацией IB.

Поскольку вы передаете уже созданный объект класса B, интерфейс IB не нуждается в подписи конструктора.

person Peter Lillevold    schedule 16.04.2009