多态性和依赖注入

5

最近我经常遇到这种情况,需要一个优雅的解决方案。我有:

public abstract class TypeA 
{ 
    public abstract void AbtractMethod(IDependency dependency);
}

public class TypeB : TypeA
{
    public override void AbtractMethod(ISpecializedDependencyForB dependency) { }
}

public class TypeC : TypeA
{
    public override void AbtractMethod(ISpecializedDependencyForC dependency) { }
}

public interface IDependency { }
public interface ISpecializedDependencyForB : IDependency { }
public interface ISpecializedDependencyForC : IDependency { }

我的目标是使客户端的视角更加透明,以便像这样使用代码:

TypeA myDomainObject = database.TypeARepository.GetById(id); // The important point here is that I don't know if the object is of TypeB or TypeC when I consume it.
IDependency dependency = ? // How do I get the right dependency 
myDomainObject.AbtractMethod(dependency);

因为我不知道对象的具体类型,所以无法向其注入正确的依赖项。目前我的解决方法是创建一个抽象工厂来注入正确的属性。但这种方式会有两个问题:一是会有很多工厂;二是它使得多态性变得无用,因为客户端实际上需要关心“管理”基础类型(我需要在工厂中注入所有可能的依赖项,并在客户端代码中实例化工厂)。
因此,我考虑使用Unity进行属性注入,但我无法确定是否可以在手动实例化对象后解析其依赖项。即使采用这种方法,我认为仍然会遇到同样的问题:如果存在以下语法,则无法确定Unity是否会检查对象的实际类型并解析正确的依赖项:
 unityContainer.Resolve<TypeA>(myDomainObject) 

如果没有的话,我需要事先了解类型,并且会回到同样的问题。
2) 我找到了这篇文章提到 EF 提供了一些 DI 机制,但似乎只是用于注入框架服务(PluralizationService 等)。否则,这将是一个很好的方式来实现。
3) 在这种情况下,我也可以不使用 DI...看起来 DI 的概念与多态性不太匹配。虽然我对这个想法并不感到兴奋。
我很乐意为我正在尝试实现的属性注入找到解决方案,或者一个我可以使用的模式的想法。但是,我真的不想为此目的创建一个大型基础设施并使我的代码混乱不堪。
注意:在这种情况下,我不想使用领域事件。
谢谢

如果方法签名不同(且基本方法必须是虚拟的),则无法覆盖。同时,如果子类依赖于不同的接口,则在容器中配置这些接口应该没有问题? - StuartLC
https://dev59.com/vWw05IYBdhLWcg3wxUf_ - Ewan
我同意你不能像你试图做的那样覆盖。它必须匹配签名,因此您将不得不在每种情况下传递一个IDependency并进行强制转换。 - Tim Rutter
@Steven 谢谢你,那么你建议把这些方法放在另一个类中吗?我认为这样做只会把问题转移或者甚至制造新的问题,因为我注入的是领域服务。我别无选择,因为这些服务将调用外部API,我想你同意我不能在领域模型层实现这些调用。 - tobiak777
1
好的,依赖对象存在在哪里?一定有某个东西知道它们。我认为我基本上同意Steven的观点——我们需要更多的上下文,因为这可能不是一个通用的情况。 - Tim Rutter
显示剩余16条评论
4个回答

6

TL;DR
将多态的AbstractMethodIDependency参数替换为实现特定的构造依赖参数,该参数由IoC容器注入而不是由使用者注入。

更详细地说

原始类层次结构需要更像这样才能使继承多态工作,因为超类virtual方法和子类override方法必须匹配签名:

public abstract class TypeA // superclass
{ 
    public abstract void AbtractMethod(IDependency dependency);
}

public class TypeB : TypeA // subclass 1
{
    public override void AbtractMethod(IDependency dependency) 
    {
        Contract.Requires(dependency is ISpecializedDependencyForB);
        // ...
    } 
}

public class TypeC : TypeA // subclass 2
{
    public override void AbtractMethod(IDependency dependency)
    {
        Contract.Requires(dependency is ISpecializedDependencyForC)
        // ...
    } 
}

然而,这个设计中有一些不太合理的地方:

  • LSP 被违反了,因为尽管 AbtractMethod() 声明它接受基础的 IDependency 接口,但是两个子类实际上依赖于一个专门的子类依赖。
  • 对于这些方法的调用者来说,构建正确的依赖关系并将其传递给方法以便正确调用也是不寻常的,甚至可以说是不方便的。

因此,如果可能的话,我会采用更传统的依赖关系安排方法,即将依赖项传递给子类构造函数,并在需要时提供给多态方法。这样就将提供适当的 IDependency 与方法解耦了。将其留给 IoC 容器进行适当的依赖项解析:

  • 使用构造函数注入来创建正确的依赖项到类 TypeBTypeC
  • 如果有一个次要需求将 IDependency 暴露给基类 TypeA 的消费者,则向 TypeA 添加一个额外的抽象属性,类型为 IDependency(但这似乎有点靠不住)
  • 根据 Ewan 的观察,存储库需要某种策略模式才能提供多态域实体(BC)。在这种情况下,将存储库与工厂耦合以执行此操作。具体工厂需要绑定到容器中,以便利用 Resolve()

因此,将所有这些放在一起,您可能会得到以下内容:

using System;
using System.Diagnostics;
using Microsoft.Practices.Unity;

namespace SO29233419
{
    public interface IDependency { }
    public interface ISpecializedDependencyForB : IDependency { }
    public interface ISpecializedDependencyForC : IDependency { }

    public class ConcreteDependencyForB : ISpecializedDependencyForB {};
    public class ConcreteDependencyForC : ISpecializedDependencyForC { };

    public abstract class TypeA
    {
        // Your polymorphic method
        public abstract void AbtractMethod();
        // Only exposing this for the purpose of demonstration
        public abstract IDependency Dependency { get; }
    }
    public class TypeB : TypeA
    {
        private readonly ISpecializedDependencyForB _dependency;
        public TypeB(ISpecializedDependencyForB dependency)
        {
            _dependency = dependency;
        }
        public override void AbtractMethod()
        {
           // Do stuff with ISpecializedDependencyForB without leaking the dependency to the caller
        }
        // You hopefully won't need this prop
        public override IDependency Dependency
        {
            get { return _dependency; }
        }
    }

    public class TypeC : TypeA
    {
        private readonly ISpecializedDependencyForC _dependency;
        public TypeC(ISpecializedDependencyForC dependency)
        {
            _dependency = dependency;
        }
        public override void AbtractMethod()
        {
           // Do stuff with ISpecializedDependencyForC without leaking the dependency to the caller
        }
        public override IDependency Dependency
        {
            get { return _dependency; }
        }
    }

    public interface ITypeAFactory
    {
        TypeA CreateInstance(Type typeOfA);
    }
    public class ConcreteTypeAFactory : ITypeAFactory
    {
        private readonly IUnityContainer _container;
        public ConcreteTypeAFactory(IUnityContainer container)
        {
            _container = container;
        }
        public TypeA CreateInstance(Type typeOfA)
        {
            return _container.Resolve(typeOfA) as TypeA;
        }
    }

    public class TypeARepository
    {
        private readonly ITypeAFactory _factory;
        public TypeARepository(ITypeAFactory factory)
        {
            _factory = factory;
        }
        public TypeA GetById(int id)
        {
            // As per Ewan, some kind of Strategy Pattern.
            // e.g. fetching a record from a database and use a discriminating column etc.
            return (id%2 == 0)
                ? _factory.CreateInstance(typeof (TypeB))
                : _factory.CreateInstance(typeof (TypeC));
            // Set the properties of the TypeA from the database after creation?
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Unity Bootstrapping
            var myContainer = new UnityContainer();
            myContainer.RegisterType<ISpecializedDependencyForB, ConcreteDependencyForB>();
            myContainer.RegisterType<ISpecializedDependencyForC, ConcreteDependencyForC>();
            myContainer.RegisterType(typeof(TypeB));
            myContainer.RegisterType(typeof(TypeC));
            var factory = new ConcreteTypeAFactory(myContainer);
            myContainer.RegisterInstance(factory);
            myContainer.RegisterType<TypeARepository>(new InjectionFactory(c => new TypeARepository(factory)));

            // And finally, your client code.
            // Obviously your actual client would use Dependency Injection, not Service Location
            var repository = myContainer.Resolve<TypeARepository>();

            var evenNumberIsB = repository.GetById(100);
            Debug.Assert(evenNumberIsB is TypeB);
            Debug.Assert(evenNumberIsB.Dependency is ISpecializedDependencyForB);

            var oddNumberIsC = repository.GetById(101);
            Debug.Assert(oddNumberIsC is TypeC);
            Debug.Assert(oddNumberIsC.Dependency is ISpecializedDependencyForC);

        }
    }
}

非常感谢您的时间和详细的解释。我最终想出了另一种解决这个问题的方法。这个想法本质上与您和Erwan建议的类似,但我通过不同的方式实现了DI。我会尽快发布它! - tobiak777

1

无论是什么知道依赖关系的东西,都可以存在于一个接口IDependencyProvider后面,该接口具有一个函数

IDependency GetDependency(Type type).  

这甚至可以只返回一个对象,实现接口的类需要知道所有子类型及其关联的依赖项。

然后将AbstractMethod更改为:

void AbstractMethod(IDependencyProvider provider);

在你的子类中,你会覆盖这个并调用它。
var dependency = provider.GetDependency(this.GetType());

你的中间层不知道子类型或子依赖关系。


0

这是一个有趣的问题,我的想法是您的存储库知道并创建TypeB和TypeC类,因此您可以在那个时候添加正确的依赖关系

public class TypeARepository
{
    private ISpecializedDependencyForB depB;
    private ISpecializedDependencyForC depC;
    public TypeARepository(ISpecializedDependencyForB depB, ISpecializedDependencyForC depC)
    {
        this.depB = depB;
        this.depC = depC;
    }

    public TypeA GetById(string id)
    {
        if (id == "B")
        {
            return new TypeB(depB);
        }
        else
        {
            return new TypeC(depC);
        }

    }
}

TypeB和TypeC将使用它们的私有依赖项引用来实现其抽象方法,而不是在方法中传递它们。
我自己也时常遇到各种形式的这个问题,总觉得如果类型之间存在硬链接,仅通过注入配置等方式进行设置是错误的。因为这样可能会让安装程序设置错误的配置。
这种方法还允许您使用Unity注入依赖项。

注:我同意@Steven的评论,通常不应将注入操作插入实体类型对象中。但在这种情况下,由于类型之间存在硬链接,我无法想到一个好的方法来反转并将依赖项作为服务处理。 - Ewan

-1
非常感谢您对我的问题的关注,昨晚我想出了一个解决方案。目标是让客户端保持透明,并通过语法如 baseObjectReference.AbstractMethodCall() 充分利用多态性。
最终我意识到,我可以利用静态修饰符并将其用于 DI 目的来实现我想要的效果。所以我有了这个:
public abstract class TypeA 
{ 
    public abstract void AbtractMethod();
}

public class TypeB : TypeA
{
    private ISpecializedDependencyForB SpecializedDependencyForB 
     { 
          get 
          { 
               return GetSpecializedDependencyForB.CreateSpecializedDependencyForB(); 
          } 
    }
    public override void AbtractMethod() { // do stuff with dependency }
}


public static class GetSpecializedDependencyForB
{
    public static ISpecializedDependencyForB DependencyForB
    {
        return CreateSpecializedDependencyForB();
    }
    public delegate ISpecializedDependencyForB CreateSpecializedDependencyForBDelegate();
    public static CreateSpecializedDependencyForBDelegate CreateSpecializedDependencyForB;
}

然后,在我的Unity容器中,我添加了这段代码:

public static void RegisterTypes(IUnityContainer container)
        {
            // .... registrations are here as usual 
            GetSpecializedDependencyForB.CreateSpecializedDependencyForB = CreateMyDomainService;
        }

在同一个Unity配置类中拥有这个方法:
private ISpecializedDependencyForB CreateMyDomainService()
    {
        return container.Value.Resolve<ISpecializedDependencyForB>();
    }

最后,我可以像这样简单地使用我的对象:
TypeA myDomainObject = database.TypeARepository.GetById(id); 
myDomainObject.AbtractMethod();

就是这样!

这里有四个要点:

  • 第一个是我注入了将创建服务实例的委托。
  • 然后它是线程安全的,因为静态成员只在应用程序开始时写入一次。所有其他访问都将被读取。此外,两个线程不会共享依赖项的相同实例,因为委托始终创建新实例。
  • 还有一个有趣的事情是,我可以依赖于现有的 Unity 容器配置,无需额外的代码。这很重要,因为我的依赖项可能需要构建其他依赖项。
  • 最后,Unity 容器也是静态的,所以没有内存泄漏。

基本上,它是一个手动且易于设置的“DI框架”,与Unity并列存在。

更重要的是,它非常好用!我终于对我的设计感到满意。我只会在多态情况下使用这种方法,因为在其他情况下,在方法中注入正确的依赖项很容易。但是,完全封装领域模型可能会使用这种方法。


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