Заводской метод с 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);
  }
}
Другие вопросы по тегам