【问题标题】:Factory method with DI and IoC使用 DI 和 IoC 的工厂方法
【发布时间】:2015-08-11 19:31:11
【问题描述】:

我熟悉这些模式,但仍然不知道如何处理以下情况:

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,但这与工厂方法相反,因为工厂总是返回相同的对象。第二种方法是注入服务定位器,但它到处都是反模式。如何解决?

编辑:

替代方式一:

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;
            }
     }
}

【问题讨论】:

  • 一个快速的想法是创建一个映射类,它将type 作为输入,并返回您需要的三个Dep#。然后你可以将所有依赖映射到引导程序中映射类的一个实例中,然后将映射实例注入到工厂中。
  • 我认为 Alternative way 1 在该工厂的实现属于 Composition Root 时显示没有任何问题。你不应该在你的 DI 容器中注册 DI 容器本身
  • 你是什么意思它属于Composition Root?能否提供一些代码示例?
  • Composition Root 是您连接应用程序 DI 容器的地方。因此,例如,这可以是 Bootstrap 类。

标签: c# dependency-injection inversion-of-control factory-pattern


【解决方案1】:

在工厂内部使用 switch case 语句是一种代码味道。有趣的是,您似乎根本没有专注于解决这个问题。

对于这种情况,最好的、对 DI 最友好的解决方案是 strategy pattern。它允许您的 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)
    {
        this.dep1 = dep1 ?? throw new ArgumentNullException(nameof(dep1));
        this.dep2 = dep2 ?? throw new ArgumentNullException(nameof(dep2));
        this.dep3 = dep3 ?? throw new ArgumentNullException(nameof(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 Car2Factory(IDep4 dep4, IDep5 dep5, IDep6 dep6)
    {
        this.dep4 = dep4 ?? throw new ArgumentNullException(nameof(dep4));
        this.dep5 = dep5 ?? throw new ArgumentNullException(nameof(dep5));
        this.dep6 = dep6 ?? throw new ArgumentNullException(nameof(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)
    {
        this.carFactories = carFactories ?? throw new ArgumentNullException(nameof(carFactories));
    }
    
    public ICar CreateCar(Type type)
    {
        var carFactory = this.carFactories
            .FirstOrDefault(factory => factory.AppliesTo(type));
            
        if (carFactory == null)
        {
            throw new InvalidOperationException($"{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));

请注意,由于没有 switch case 语句,您可以在不更改设计的情况下向策略中添加额外的工厂,并且每个工厂都可以有自己的依赖项,这些依赖项由 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));

【讨论】:

  • 但是当涉及到 IoC 时,使用新的操作符不是被认为是不可行的吗?工厂能否以不同的方式实现(不使用 new 且不诉诸服务定位器模式)?
  • 对于 IoC,抽象工厂模式的一种用途是从其余代码中抽象出 new 运算符以隔离它,使其对 DI 友好。这使得它与使用工厂的代码松散耦合。没有要求您必须返回容器来解析实例 - 事实上,这将比使用容器执行得快得多。也就是说,将容器注入抽象工厂是allow 3rd parties to integrate with a framework 使用 DI 的常用技术。
  • @Boris - 另请参阅 Is there a pattern for initializing objects created via a DI containerWhy do we need Abstract factory design pattern?,了解有关将抽象工厂与 DI 结合使用的更多信息。
  • 我正在研究基于枚举的类似解决方案。你有一个方法Create(Type) 我有Create(SomeEnum)。困扰我的是,这似乎在调用 Create 时提出了一个不需要的未知先决条件。如果我将一个项目添加到enum SomeEnum 而不添加相应的工厂,客户可能会调用Create(SomeEnum.SomeNewValue) 并导致异常违反封装,如此处所述blog.ploeh.dk/2015/10/26/service-locator-violates-encapsulation
  • 您的 CarStrategy 并不是真正的策略,而是一个 AbstractFactory。您正在通过 FirstOrDefault 方法进行切换/分支。工厂中的分支代码不是代码味道。创建对象时的分支语句是工厂方法/抽象工厂模式存在的理由。
【解决方案2】:

使用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);
 }

这就是使用 Unity DI 容器的 Composition Root 的外观:

Func<Type, ICar> carFactoryFunc = type => (ICar)container.Resolve(type);
container.RegisterInstance<CarFactory>(new CarFactory(carFactoryFunc));

【讨论】:

    【解决方案3】:

    前段时间我回答了一个类似的问题。基本上,这完全取决于您的选择。您必须在冗长(编译器为您提供更多帮助)和自动化(允许您编写更少的代码但更容易出现错误)之间做出选择。

    This 是我支持冗长的答案。

    this也是支持自动化的好答案。

    编辑

    我相信你认为错误的方法实际上是最好的。说实话,通常那里不会有这么很多依赖项。我喜欢这种方法,因为它非常明确并且很少导致运行时错误。

    替代方式一:

    这个不好。它实际上是一个服务定位器,被认为是anti-pattern

    替代方式2

    就像你写的那样,如果与 IOC 容器混合使用它并不容易。但是在某些情况下,类似的方法 (poor man's DI) 可能会很有用。

    总而言之,我不会费心在您的工厂中拥有“许多”依赖项。这是一个简单的声明性代码。编写只需几秒钟,可以为您节省数小时处理运行时错误的时间。

    【讨论】:

    • 您的解决方案似乎是我认为错误的问题的示例(在顶部),对吗?
    • 我最喜欢它,但它有一个问题。如果 Dep1 包含状态会发生什么?每次我创建汽车时,我都会得到不同的 Car 实例,但相同的 Dep1 可能会导致一些问题。
    • @Zbigniew 传递 Dep1Factory 而不是 Dep1 怎么样?
    • 它会解决这个问题。我不确定我会选择哪种方法,但感谢您的帮助:)
    • @gisek:#1 不好的原因不是因为它是 SL,因为它不是。这是 Local Factory 模式(又名 Dependency Resolver)的完美示例,它没有 SL 的问题 - 你不能滥用它。 #1 不好的原因是因为它从域类中引入了对基础设施类的不必要和不正确的依赖。
    【解决方案4】:

    我会考虑为依赖项提供一个良好的结构,以便您可以使用类似于 Wiktor 的答案的东西,但我会抽象 Car 工厂本身。然后,您不使用 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 注入你需要的任何东西。

    【讨论】:

    • 请阅读我对 Viktor 回答的评论。 Dep1、Dep2.. 不是数据对象,而是服务、构建器等。
    • 在我对他的回答的解释中,他只是说创建一个参数对象(在我的示例中为 Specs)。它们是否是价值对象并不重要。我认为他和我都在尝试组织 dep1、dep2 等,但不知道这些依赖项的具体内容。我的方法的主要区别在于,我不仅会抽象这些 params 对象,还会抽象工厂本身,因此您可以对它们使用 DI,而不是在您和他的示例中使用 switch 语句。
    • 我已经添加了您所说的服务、阅读器等的去向。这些听起来不太像汽车依赖,更像是汽车工厂依赖。
    【解决方案5】:

    首先,你有一个具体的工厂,一个 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 创建。

    然后,在 Composition Root(在您配置实际容器的应用程序起点附近)中配置提供程序:

    CarFactory.SetProvider(
        type =>
        {
            switch ( type )
            {
               case A:
                 return _container.Resolve<ICar1>();
               case B:
                 return _container.Resolve<ICar2>();
               ..
        }
    );
    

    请注意,工厂提供者的这个示例实现使用委托,但接口也可以用作实际提供者的规范。

    这个实现基本上是您编辑的问题中的第一名,但是,它没有任何特别的缺点。客户端仍然调用:

    var car = new CarFactory().CreareCar( type );
    

    【讨论】:

    • 这些依赖项是在 IOC 中注册的对象 -> 服务、构建器、读取器等,而不是值对象。 CarFactoryParams 对于数据持有者对象来说听起来不错,而不是对实际做某事的依赖项。我想从容器中获取它们,这就是为什么我想用构造函数注入它们。 Ioc 将有助于使用 Ioc 容器中声明的依赖项为我创建这些实例。
    • @Zbigniew:在您提供更多详细信息时编辑了我的答案。
    【解决方案6】:

    许多 DI 容器支持命名依赖的概念。

    例如(结构图语法)

    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);
      }
    }
    

    【讨论】:

    • 我明白你的意思,但这并不是我所要求的。一般来说,我不觉得在工厂中使用 IContainer 依赖是合适的。如果是这样,那么问题就解决了,因为如果我使用命名依赖或使用 Resolve 使用 switch 语句,这并没有什么区别。它解决了为所有已解析对象创建接口的问题,因为当我命名它们时,我可以摆脱它们。但是这里最大的问题是 IContainer 作为依赖。
    • 在我的书中,工厂几乎是我允许直接依赖容器的唯一地方,因为它们通常与基础设施相关。
    • @flq:仍然,您可以在工厂中有可配置的内部提供程序,可以从组合根配置。这样,工厂将不知道使用容器的可能配置。另一方面,在工厂构造函数中包含容器依赖项听起来像是一种反模式。工厂是 API 的重要组成部分,它们的实现通常与基础设施相关,API 不应该是,恕我直言。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-11-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多