使用依赖注入和控制反转的工厂方法

65

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

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

1
一个快速的想法是创建一个映射类,它以 type 作为输入,并返回你所需的三个 Dep#。然后,你可以将所有依赖项映射到映射类的实例中,在引导程序中注入映射实例到工厂中。 - Anders
1
我认为当工厂的实现属于“组合根”时,你展示的“替代方式1”没有任何问题。你不应该在DI容器中注册DI容器本身。 - Arkadiusz K
它属于组合根,你是什么意思?你能提供一些代码示例吗? - MistyK
组合根是您连接应用程序 DI 容器的位置。因此,例如,这可以是一个“引导”类。 - Arkadiusz K
6个回答

104

在工厂内部使用switch 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)
    {
        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));

4
在IoC中,使用new操作符被认为是不可取的,那么工厂能否采用不同的实现方式(不使用new并且不依赖于服务定位器模式)? - Boris
4
当涉及到IoC时,抽象工厂模式的一个用途是将new操作符从代码的其余部分中抽象出来并隔离它,使其支持DI。这使得它与使用该工厂的代码松耦合。并不需要返回容器来解析实例 - 事实上,这比使用容器要快得多。尽管如此,将容器注入抽象工厂是一种常见技术,可以允许第三方使用DI集成框架 - NightOwl888
1
我正在基于枚举(enum)开发类似的解决方案。在你的方法Create(Type)中,我使用了Create(SomeEnum)。 让我困扰的是,这似乎会在调用Create时产生一个不必要的未知前提条件。 如果我向enum SomeEnum添加一个项目而没有添加相应的工厂,客户端可能会调用Create(SomeEnum.SomeNewValue)并引发异常,从而违反封装性,如此处所述:http://blog.ploeh.dk/2015/10/26/service-locator-violates-encapsulation/。 - Craig
18
你的CarStrategy实际上不是一种策略,而是一个抽象工厂。你正在通过使用FirstOrDefault方法进行切换/分支操作。在工厂中分支代码并不是代码异味。创建对象时分支语句是Factory Method / Abstract Factory模式的存在理由。 - Alexander Pope
2
我觉得你嘲笑在工厂中分支代码以找到正确实现的想法很有趣,但你却使用FirstOrDefault来查找自己的实现...这其实是完全相同的事情。这里的改进是你通过DI接收所有实现,这是好的,但也没有太多其他的改进。 - Ed S.
显示剩余11条评论

16

回复您有关组合根(Composition Root)代码示例的评论。 您可以创建以下内容,但这不是服务定位器(Service Locator)。

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

2
我之前回答过类似的问题。基本上这取决于您的选择。您必须在冗长性(可以从编译器获得更多帮助)和自动化之间做出选择,后者允许您编写更少的代码,但更容易出现错误。 这里是我的支持冗长性的答案。
这个答案也是支持自动化的好答案。
编辑 我认为您认为错误的方法实际上是最好的。说实话,通常不会有太多的依赖关系。我喜欢这种方法,因为它非常明确,很少导致运行时错误。

另一种方法 1:

这个很糟糕。实际上这是一种服务定位器,被认为是反模式

另一种方法 2:

正如您所写的那样,如果与IOC容器混合使用,则不易使用。然而,在某些情况下,类似的方法(穷人的DI)是有用的。
总之,在您的工厂中不必担心有“许多”依赖项。这是一种简单,声明性的代码。编写起来只需要几秒钟,并可以节省您处理运行时错误的时间。

你的解决方案似乎是我认为错误的问题示例(在顶部),我是对的吗? - MistyK
我大部分都喜欢它,但有一个问题。如果Dep1包含状态会发生什么?每次我创建汽车时,我得到不同的Car实例,但是相同的Dep1可能会导致一些问题。 - MistyK
@Zbigniew,传递Dep1Factory而不是Dep1怎么样? - Andrzej Gis
它将解决那个问题。我不确定我会选择哪种方法,但感谢您的帮助 :) - MistyK
@gisek:#1的问题不是因为它是服务定位器,实际上它并不是。这是“本地工厂”模式(又称依赖项解析器)的完美例子,它没有服务定位器存在的问题——你无法滥用它。#1的问题在于它向基础设施类引入了不必要和不合适的依赖关系,而这个依赖关系来自一个领域类。 - Wiktor Zychla
显示剩余9条评论

1
首先,您有一个具体的工厂,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,...是什么以及您如何使用它们。
我建议采用以下方法,将工厂提供程序与实际工厂实现分开。这种方法称为本地工厂模式:
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 );

这些依赖项是在IOC中注册的对象->服务、构建器、读取器等,而不是值对象。CarFactoryParams听起来适用于数据持有者对象,而不适用于实际执行某些操作的依赖项。我想从容器中获取它们,所以我想通过构造函数注入它们。使用在Ioc容器中声明的依赖项,Ioc将有助于为我创建这些实例。 - MistyK
@Zbigniew:根据您提供的更多细节,我已经编辑了我的答案。 - Wiktor Zychla

1
我会考虑为依赖项提供一个良好的结构,这样您就可以利用类似于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 注入所需内容。

请阅读我的评论,针对维克托的回答。Dep1、Dep2等不是数据对象,而是服务、构建器等。 - MistyK
在我理解他的回答时,他只是说要创建一个参数对象(在我的示例中为Specs)。它们是否是值对象并不重要。我认为他和我都试图组织dep1、dep2等,而不知道这些依赖项的具体内容。我方法的主要区别在于,我不仅会抽象这些参数对象,还会抽象工厂本身,这样你就可以在你和他的示例中使用DI代替switch语句。 - user4275029
我已经添加了你所说的服务、读取器等应该放置的位置。这些听起来不像是汽车依赖项,更像是汽车工厂的依赖项。 - user4275029

0
许多 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);
  }
}

我明白你的观点,但那不是我所要询问的。一般来说,在工厂中使用IContainer依赖项并不合适。如果可以这样做,问题就解决了,因为使用命名依赖项或使用Resolve语句使用switch语句没有区别。这解决了为所有已解析对象创建接口的问题,因为当我给它们命名时,我可以摆脱它们。但这里最大的问题是IContainer作为依赖项。 - MistyK
在我的观点中,工厂几乎是唯一一个我允许直接依赖容器的地方,因为它们通常与基础设施相关。 - flq
@flq:不过,你可以在工厂中设置可配置的内部提供程序,并从组合根进行配置。这样,工厂将不知道可能使用容器的配置。另一方面,在工厂构造函数中具有容器依赖关系听起来像是反模式。工厂是API的重要组成部分,它们的实现通常与基础设施相关,而API不应该是如此,我个人认为。 - Wiktor Zychla

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接