Отправка ковариационных элементов списка в соответствии с подклассом - PullRequest
0 голосов
/ 01 февраля 2019

У меня есть классы B и C, унаследованные от класса SuperA.Если у меня есть список SuperA, содержащий различные реализации SuperA, как я могу вызвать метод, принимающий аргументы B и C в соответствии с фактической реализацией каждого элемента в списке, без необходимости проверять типкаждый элемент (я бы предпочел избегать 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));
}

Я открыт для любой лучшей идеи, однако,Как указано в ответе, велик риск ошибки времени выполнения после эволюции.

Ответы [ 2 ]

0 голосов
/ 01 февраля 2019

Компилятор жалуется на ваш код, потому что, как вы указали, в OutputFactory нет класса GenerateOutput(SuperA), и разрешение вызовов методов происходит при типе компиляции, а не во время выполнения, и поэтому основано на типе ссылки.(item является ссылкой с типом SuperA), а не с типом экземпляра среды выполнения.

Вы можете попробовать разные подходы:

  1. , если вы обнаружите, что это делаетв смысле, вы можете попытаться переместить полиморфное поведение (генерируемый выходной текст) в SuperA иерархию классов, добавив абстрактный метод или свойство к SuperA и реализовав его по-другому в подклассах SuperA
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, and C classes are out of your control or when the different desired behaviours you should provide for A and B classes does not belong to B and C` классов.

вы можете использовать подход, который мне нравится называть набором ответственности : это что-то вроде паттерна 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), но скрывает их за интерфейсом (в частности, каждая реализация интерфейса должна обеспечивать проверку типа для выбора экземпляров, которые он может обрабатывать): если вам нужно добавить третий D extends SuperA подкласс вашей иерархииКорневой класс: вам нужно только добавить третью реализацию DHandler : Handler интерфейса Handler, без изменения ни предоставленных реализаций, ни CompositeHelper class;единственное изменение, которое вы должны применить к существующему коду, - это регистрация новой реализации handler в списке, который вы предоставляете конструктору CompositeHelper, но это можно легко перенести на вас IoC container Конфигурация или внешний файл конфигурации.Мне нравится этот подход, потому что он позволяет превратить алгоритм на основе проверки типа в полиморфный.

Я писал об этой теме в недавнем посте в моем техническом блоге: https://javapeanuts.blogspot.com/2018/10/set-of-responsibility.html.

Вы можете подойти к вопросу с помощью шаблона GoF посетитель , который немного сложнее, чем мой предложенный подход, но был задуман именно для таких случаев Вы можете принять dynamicоснованный на подходе, как предложено в другом ответе

Я надеюсь, что это может помочь вам!

0 голосов
/ 01 февраля 2019

Вы можете назвать это так:

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

Таким образом, используя dynamic, ваши объекты будут связаны во время выполнения с правильными методами.

С этой реализацией вы будетеПримите во внимание, что вы подвергаете себя риску отправки объекта C, для которого метод не был реализован, и ваш код все равно будет компилироваться, но будет сгенерирована ошибка времени выполнения.

...