Заводской метод с DI и IoC
Я знаком с этими шаблонами, но все еще не знаю, как справиться со следующей ситуацией:
public class CarFactory
{
public CarFactory(Dep1,Dep2,Dep3,Dep4,Dep5,Dep6)
{
}
public ICar CreateCar(type)
{
switch(type)
{
case A:
return new Car1(Dep1,Dep2,Dep3);
break;
case B:
return new Car2(Dep4,Dep5,Dep6);
break;
}
}
}
В целом проблема заключается в количестве ссылок, которые необходимо ввести. Будет еще хуже, когда будет больше машин.
Первый подход, который приходит мне на ум, - это ввести Car1 и Car2 в конструктор фабрики, но это против фабричного подхода, потому что фабрика всегда будет возвращать один и тот же объект. Второй подход заключается в том, чтобы внедрить servicelocator, но это антипаттерн везде. Как это решить?
Редактировать:
Альтернативный способ 1:
public class CarFactory
{
public CarFactory(IContainer container)
{
_container = container;
}
public ICar CreateCar(type)
{
switch(type)
{
case A:
return _container.Resolve<ICar1>();
break;
case B:
return _container.Resolve<ICar2>();
break;
}
}
}
Альтернативный способ 2 (слишком сложно использовать из-за слишком большого количества зависимостей в дереве):
public class CarFactory
{
public CarFactory()
{
}
public ICar CreateCar(type)
{
switch(type)
{
case A:
return new Car1(new Dep1(),new Dep2(new Dep683(),new Dep684()),....)
break;
case B:
return new Car2(new Dep4(),new Dep5(new Dep777(),new Dep684()),....)
break;
}
}
}
6 ответов
Наличие оператора case в фабрике - это запах кода. Интересно, что вы, кажется, не сосредоточены на решении этой проблемы вообще.
Лучшим, наиболее дружественным к DI решением для этого сценария является шаблон стратегии. Это позволяет вашему DI-контейнеру внедрять зависимости в экземпляры фабрики, к которым они принадлежат, не загромождая другие классы этими зависимостями и не обращаясь к локатору службы.
Интерфейсы
public interface ICarFactory
{
ICar CreateCar();
bool AppliesTo(Type type);
}
public interface ICarStrategy
{
ICar CreateCar(Type type);
}
Фабрики
public class Car1Factory : ICarFactory
{
private readonly IDep1 dep1;
private readonly IDep2 dep2;
private readonly IDep3 dep3;
public Car1Factory(IDep1 dep1, IDep2 dep2, IDep3 dep3)
{
if (dep1 == null)
throw new ArgumentNullException("dep1");
if (dep2 == null)
throw new ArgumentNullException("dep2");
if (dep3 == null)
throw new ArgumentNullException("dep3");
this.dep1 = dep1;
this.dep2 = dep2;
this.dep3 = dep3;
}
public ICar CreateCar()
{
return new Car1(this.dep1, this.dep2, this.dep3);
}
public bool AppliesTo(Type type)
{
return typeof(Car1).Equals(type);
}
}
public class Car2Factory : ICarFactory
{
private readonly IDep4 dep4;
private readonly IDep5 dep5;
private readonly IDep6 dep6;
public Car1Factory(IDep4 dep4, IDep5 dep5, IDep6 dep6)
{
if (dep4 == null)
throw new ArgumentNullException("dep4");
if (dep5 == null)
throw new ArgumentNullException("dep5");
if (dep6 == null)
throw new ArgumentNullException("dep6");
this.dep4 = dep4;
this.dep5 = dep5;
this.dep6 = dep6;
}
public ICar CreateCar()
{
return new Car2(this.dep4, this.dep5, this.dep6);
}
public bool AppliesTo(Type type)
{
return typeof(Car2).Equals(type);
}
}
стратегия
public class CarStrategy : ICarStrategy
{
private readonly ICarFactory[] carFactories;
public CarStrategy(ICarFactory[] carFactories)
{
if (carFactories == null)
throw new ArgumentNullException("carFactories");
this.carFactories = carFactories;
}
public ICar CreateCar(Type type)
{
var carFactory = this.carFactories
.FirstOrDefault(factory => factory.AppliesTo(type));
if (carFactory == null)
{
throw new Exception("type not registered");
}
return carFactory.CreateCar();
}
}
использование
// I am showing this in code, but you would normally
// do this with your DI container in your composition
// root, and the instance would be created by injecting
// it somewhere.
var strategy = new CarStrategy(new ICarFactory[] {
new Car1Factory(dep1, dep2, dep3),
new Car2Factory(dep4, dep5, dep6)
});
// And then once it is injected, you would simply do this.
// Note that you could use a magic string or some other
// data type as the parameter if you prefer.
var car1 = strategy.CreateCar(typeof(Car1));
var car2 = strategy.CreateCar(typeof(Car2));
Обратите внимание, что, поскольку нет оператора переключения регистра, вы можете добавить дополнительные фабрики в стратегию, не изменяя дизайн, и каждая из этих фабриок может иметь свои собственные зависимости, которые вводятся контейнером DI.
var strategy = new CarStrategy(new ICarFactory[] {
new Car1Factory(dep1, dep2, dep3),
new Car2Factory(dep4, dep5, dep6),
new Car3Factory(dep7, dep8, dep9)
});
var car1 = strategy.CreateCar(typeof(Car1));
var car2 = strategy.CreateCar(typeof(Car2));
var car3 = strategy.CreateCar(typeof(Car3));
Отвечая на ваш комментарий о примере кода с Composition Root
, Вы можете создать следующее, и это не локатор службы.
public class CarFactory
{
private readonly Func<Type, ICar> carFactory;
public CarFactory(Func<Type, ICar> carFactory)
{
this.carFactory = carFactory;
}
public ICar CreateCar(Type carType)
{
return carFactory(carType);
}
и вот как выглядит твой Composition Root
используя контейнер Unity DI:
Func<Type, ICar> carFactoryFunc = type => (ICar)container.Resolve(type);
container.RegisterInstance<CarFactory>(new CarFactory(carFactoryFunc));
Я ответил на аналогичный вопрос некоторое время назад. В основном все зависит от вашего выбора. Вы должны выбирать между многословностью (которая дает вам больше помощи от компилятора) и автоматизацией, которая позволяет писать меньше кода, но более подвержена ошибкам.
Это мой ответ в поддержку многословия.
И это тоже хороший ответ, который поддерживает автоматизацию.
РЕДАКТИРОВАТЬ
Я считаю, что подход, который вы считаете неправильным, на самом деле лучший. По правде говоря, обычно там не так много зависимостей. Мне нравится этот подход, потому что он очень явный и редко приводит к ошибкам во время выполнения.
Альтернативный способ 1:
Это плохо. На самом деле это сервисный локатор, который считается анти-паттерном.
Альтернативный способ 2
Как вы написали, это нелегко использовать, если смешать с контейнером IOC. Однако в некоторых случаях подобный подход ( DI бедного человека) может быть полезным.
В общем, я бы не стал беспокоиться о том, чтобы на ваших фабриках было "много" зависимостей. Это простой декларативный код. Запись занимает несколько секунд и может сэкономить вам часы борьбы с ошибками во время выполнения.
Я хотел бы придать зависимостям хорошую структуру, чтобы вы могли использовать что-то похожее на ответ Виктора, но я бы абстрагировал сам автомобильный завод. Тогда вы не используете структуру if..then.
public interface ICar
{
string Make { get; set; }
string ModelNumber { get; set; }
IBody Body { get; set; }
//IEngine Engine { get; set; }
//More aspects...etc.
}
public interface IBody
{
//IDoor DoorA { get; set; }
//IDoor DoorB { get; set; }
//etc
}
//Group the various specs
public interface IBodySpecs
{
//int NumberOfDoors { get; set; }
//int NumberOfWindows { get; set; }
//string Color { get; set; }
}
public interface ICarSpecs
{
IBodySpecs BodySpecs { get; set; }
//IEngineSpecs EngineSpecs { get; set; }
//etc.
}
public interface ICarFactory<TCar, TCarSpecs>
where TCar : ICar
where TCarSpecs : ICarSpecs
{
//Async cause everything non-trivial should be IMHO!
Task<TCar> CreateCar(TCarSpecs carSpecs);
//Instead of having dependencies ctor-injected or method-injected
//Now, you aren't dealing with complex overloads
IService1 Service1 { get; set; }
IBuilder1 Builder1 { get; set; }
}
public class BaseCar : ICar
{
public string Make { get; set; }
public string ModelNumber { get; set; }
public IBody Body { get; set; }
//public IEngine Engine { get; set; }
}
public class Van : BaseCar
{
public string VanStyle { get; set; }
//etc.
}
public interface IVanSpecs : ICarSpecs
{
string VanStyle { get; set; }
}
public class VanFactory : ICarFactory<Van, IVanSpecs>
{
//Since you are talking of such a huge number of dependencies,
//it may behoove you to properly categorize if they are car or
//car factory dependencies
//These are injected in the factory itself
public IBuilder1 Builder1 { get; set; }
public IService1 Service1 { get; set; }
public async Task<Van> CreateCar(IVanSpecs carSpecs)
{
var van = new Van()
{
//create the actual implementation here.
};
//await something or other
return van;
}
}
Я не перечислял это, но вы можете сейчас внедрить несколько типов автомобилей и соответствующих заводов и использовать DI для впрыскивания всего, что вам нужно.
Во-первых, у вас есть конкретный завод, контейнер IoC может быть альтернативой, а не чем-то, что может вам помочь.
Затем просто проведите рефакторинг фабрики, чтобы не ожидать полного списка возможных параметров в конструкторе фабрики. Это основная проблема - почему вы передаете так много параметров, если фабричный метод не нуждается в них?
Я бы предпочел передать конкретные параметры заводскому методу
public abstract class CarFactoryParams { }
public class Car1FactoryParams : CarFactoryParams
{
public Car1FactoryParams(Dep1, Dep2, Dep3)
{
this.Dep1 = Dep1;
...
}
public class Car2FactoryParams
...
public class CarFactory
{
public ICar CreateCar( CarFactoryParams params )
{
if ( params is Car1FactoryParams )
{
var cp = (Car1FactoryParams)params;
return new Car1( cp.Dep1, cp.Dep2, ... );
}
...
if ( params is ...
Инкапсулируя список параметров в определенном классе, вы просто заставляете клиента предоставлять именно те параметры, которые необходимы для вызова конкретного фабричного метода.
Редактировать:
К сожалению, из вашего поста не было понятно, что это Dep1
... и как вы их используете.
Тогда я предлагаю следующий подход, который отделяет поставщика фабрики от фактической реализации фабрики. Этот подход известен как шаблон Local Factory:
public class CarFactory
{
private static Func<type, ICar> _provider;
public static void SetProvider( Func<type, ICar> provider )
{
_provider = provider;
}
public ICar CreateCar(type)
{
return _provider( type );
}
}
Сама фабрика не имеет никакой реализации, она здесь, чтобы установить основу для API вашего домена, где вы хотите, чтобы экземпляры вашего автомобиля создавались только с помощью этого API.
Затем в корне композиции (где-то рядом с начальной точкой приложения, где вы настраиваете свой фактический контейнер) вы настраиваете провайдера:
CarFactory.SetProvider(
type =>
{
switch ( type )
{
case A:
return _container.Resolve<ICar1>();
case B:
return _container.Resolve<ICar2>();
..
}
);
Обратите внимание, что в этом примере реализации провайдера фабрики используется делегат, но интерфейс также может использоваться в качестве спецификации для фактического провайдера.
Эта реализация в основном № 1 из вашего отредактированного вопроса, однако, у нее нет особых недостатков. Клиент все еще звонит:
var car = new CarFactory().CreareCar( type );
Многие DI-контейнеры поддерживают понятие именованных зависимостей.
Например (синтаксис Structuremap)
For<ICar>().Use<CarA>().Named("aCar");
Container.GetNamedInstance("aCar") // gives you a CarA instance
Если вы используете что-то вроде соглашения, правила, как имя происходит от конкретного типа автомобиля, у вас возникает ситуация, когда вам больше не нужно прикасаться к заводу при расширении системы.
Использовать это на фабрике просто.
class Factory(IContainer c) {
public ICar GetCar(string name) {
Return c.GetNamedInstance(name);
}
}