根据子类分派协变列表元素

Dispatch co-variant list elements according to subclass

我有 classes BC,继承自 class SuperA。如果我有一个包含 SuperA 的各种实现的 SuperA 列表,我如何根据列表中每个元素的实际实现来调用采用 BC 参数的方法,而不必测试每个元素的类型(出于 open/closed 原则原因,我宁愿避免 if(item is B) 东西)。

public class Test
{
    public void TestMethod()
    {
        var list = new List<SuperA> {new B(), new C()};

        var factory = new OutputFactory();

        foreach (SuperA item in list)
        {
            DoSomething(factory.GenerateOutput(item)); // doesn't compile as there is no GenerateOutput(SuperA foo) signature in OutputFactory.
        }
    }

    private static void DoSomething(OutputB b)
    {
        Console.WriteLine(b.ToString());
    }

    private static void DoSomething(OutputC c)
    {
        Console.WriteLine(c.ToString());
    }

    public class SuperA
    {
    }

    public class B : SuperA
    {
    }

    public class C : SuperA
    {
    }


    public class OutputB
    {
        public override string ToString()
        {
            return "B";
        }
    }

    public class OutputC
    {
        public override string ToString()
        {
            return "C";
        }
    }

    public class OutputFactory
    {
        public OutputB GenerateOutput(B foo)
        {
            return new OutputB();
        }

        public OutputC GenerateOutput(C foo)
        {
            return new OutputC();
        }
    }
}

在上面的代码中,我希望打印:

B

C

编辑: 我发现一个可行的解决方案是将项目类型更改为 dynamic

foreach (dynamic item in list)
{
    DoSomething(factory.GenerateOutput(item));
}

不过,我愿意接受任何更好的想法。正如回答中指出的那样,进化后运行时错误的风险很大。

你可以这样称呼它:

DoSomething((dynamic)factory.GenerateOutput((dynamic)item));

这样,使用 dynamic 您的对象将在运行时绑定到正确的方法。

使用此实现,您将不得不考虑到发送 C 未实现任何方法的对象的风险,您的代码仍可编译,但会出现运行时错误生成。

编译器抱怨您的代码,因为正如您所指出的,OutputFactory class 中没有 GenerateOutput(SuperA) 并且方法调用解析发生在编译类型,而不是运行时,因此基于引用的类型(item 是类型为 SuperA 的引用)而不是基于运行时实例的类型。

您可以尝试不同的方法:

  1. 如果你觉得有意义,你可以尝试将多态行为(要生成的输出文本)移动到 SuperA class 层次结构中,添加一个抽象方法或 属性 到SuperA 并在 SuperA 的 subclasses
  2. 中以不同方式实现
class SuperA {
  public abstract string Content { get; }
}

class B : SuperA {
  public string Content => "B";
}

class C : SuperA {
  public string Content => "C";
}

class Test {
  public void TestMethod() {
    // ...
    foreach (SuperA item in list) {
      Console.WriteLine(item.Content);
    }
}

很简单但是在SuperA,B, andCclasses are out of your control or when the different desired behaviours you should provide forAandBclasses does not belong toBand时效果不是很好C` classes.

  1. 你可以使用我喜欢称之为责任集的方法:它类似于 GoF 的模式责任链,但没有链;-);你可以重写你的 TestMethod 如下:
public void TestMethod() {
    var list = new List<SuperA> {new B(), new C()};

    var compositeHandler = new CompositeHandler(new Handler[]  {
        new BHandler(),
        new CHandler()
    });

    foreach (SuperA item in list) {
      compositeHandler.Handle(item);
    }
}

所以你需要定义一个Handler接口及其实现如下:

interface Handler {
  bool CanHandle(SuperA item);

  void Handle(SuperA item);
}

class BHandler : Handler {
  bool CanHandle(SuperA item) => item is B;

  void Handle(SuperA item) {
   var b = (B)item; // cast here is safe due to previous check in `CanHandle()`
   DoSomethingUsingB(b);
  }
}

class CHandler : Handler {
  bool CanHandle(SuperA item) => item is C;

  void Handle(SuperA item) {
   var c = (C)item; // cast here is safe due to previous check in `CanHandle()`
   DoSomethingUsingC(c);
  }
}

class CompositeHandler {
  private readonly IEnumerable<handler> handlers;
  public CompositeHandler(IEnumerable<handler> handlers) {
    this.handlers = handlers;
  }

  public void Handle(SuperA item) {
    handlers.FirstOrDefault(h => h.CanHandle(item))?.Handle(item);
  }
}

此方法使用类型检查 (item is B),但将它们隐藏在接口后面(具体来说,接口的每个实现都应提供类型检查,以便 select 它可以处理的实例) : 如果你需要添加第三个 D extends SuperA subclass 你的层次根 class 你只需要添加第三个 DHandler : Handler 实现 Handler 接口,无需修改既没有提供实现也没有 CompositeHelper class;您应该对现有代码应用的唯一更改是在您提供给 CompositeHelper 的构造函数的列表中新 handler 实现的 注册 ,但是这可以很容易地移动到您 IoC container 配置或外部配置文件。 我喜欢这种方法,因为它可以将基于 类型检查的 算法转换为多态算法。

我最近在我的技术博客 post 中写过这个主题:https://javapeanuts.blogspot.com/2018/10/set-of-responsibility.html

  1. 您可以通过 GoF 的 visitor 模式来解决这个问题,这比我建议的方法稍微复杂一点,但正是为这种情况设计的
  2. 您可以采用基于 dynamic 的方法,如另一个回复中所建议的那样

希望对您有所帮助!