Преобразование типов при переборе коллекций супертипов.Альтернативы? - PullRequest
4 голосов
/ 26 октября 2010

Это довольно распространенная проблема, с которой я сталкиваюсь.Давайте послушаем ваши решения.В качестве примера я собираюсь использовать приложение для управления сотрудниками: -

У нас есть несколько классов сущностей, некоторые из которых реализуют определенный интерфейс.

public interface IEmployee { ... }
public interface IRecievesBonus { int Amount { get; } }
public class Manager : IEmployee, IRecievesBonus { ... }
public class Grunt : IEmployee /* This company sucks! */ { ... }

У нас естьполучил коллекцию сотрудников, которую мы можем перебрать.Нам нужно захватить все объекты, которые реализуют IRecievesBonus, и выплатить бонус.

Наивная реализация выглядит следующим образом: -

foreach(Employee employee in employees)
{
  IRecievesBonus bonusReciever = employee as IRecievesBonus;
  if(bonusReciever != null)
  {
    PayBonus(bonusReciever);
  }
}

или поочередно в C #: -

foreach(IRecievesBonus bonusReciever in employees.OfType<IRecievesBonus>())
{
  PayBonus(bonusReciever);
}
  • Мы не можем изменить интерфейс IEmployee, включив в него детали дочернего типа, поскольку мы не хотим загрязнять супертип деталями, о которых заботится только подтип.
  • У нас нет существующей коллекции только подтипа.
  • Мы не можем использовать Шаблон посетителя , поскольку типы элементов не являются стабильными.Кроме того, у нас может быть тип, который реализует как IRecievesBonus, так и IDrinksTea.Его метод Accept будет содержать неоднозначный вызов visitor.Visit (this).

Часто мы вынуждены идти по этому маршруту, потому что не можем изменить супертип,кроме коллекции, например, в .NET, нам может понадобиться найти все кнопки в этой форме через коллекцию дочерних элементов управления.Возможно, нам нужно что-то сделать с дочерними типами, что зависит от некоторого аспекта дочернего типа (например, сумма бонуса в приведенном выше примере).

Мне кажется странным, что не существует "принятого" способачтобы сделать это, учитывая, как часто это происходит.

1) Стоит ли избегать преобразования типов?

2) Есть ли альтернативы, о которых я не думал?

РЕДАКТИРОВАТЬ

Петер Тёрёк предлагает составить Employee и продвинуть преобразование типов вниз по дереву объектов: -

public interface IEmployee
{
  public IList<IEmployeeProperty> Properties { get; }
}

public interface IEmployeeProperty { ... }

public class DrinksTeaProperty : IEmployeeProperty
{
  int Sugars { get; set; }
  bool Milk { get; set; }
}

foreach (IEmployee employee in employees)
{
  foreach (IEmployeeProperty property in employee.Propeties)
  {
    // Handle duplicate properties if you need to.
    // Since this is just an example,  we'll just
    // let the greedy ones have two cups of tea.
    DrinksTeaProperty tea = property as DrinksTeaProperty;
    if (tea != null)
    {
      MakeTea(tea.Sugers, tea.Milk);
    }
  }
}

В этом примере определенно стоит выдвинуть эти чертытипа Employee - особенно потому, что некоторые менеджеры могут пить чай, а некоторые нет - но у нас все еще остается та же основная проблема преобразования типов.

Это тот случай, когда все в порядке, пока мыэто на правильном уровне?Или мы просто решаем проблему?

Святой Грааль был бы вариантом в схеме посетителя, где: -

  • Вы можете добавлять элементы элемента, не изменяя всех посетителей
    • Посетители должны посещать только те типы, которые им интересны при посещении
  • Посетитель может посещать участника на основе типа интерфейса
    • Элементы могут реализовывать несколько интерфейсов, которыепосещают разные посетители
  • Не включает кастинг или рефлексию

, но я понимаю, что это, вероятно, нереально.

Ответы [ 2 ]

5 голосов
/ 26 октября 2010

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

Я могу привести пример частично на Java, думаю, он достаточно близок к вашему языку (C #), чтобы быть полезным.

public enum EmployeeProperty {
  RECEIVES_BONUS,
  DRINKS_TEA,
  ...
}

public class Employee {
  Set<EmployeeProperty> properties;
  // methods to add/remove/query properties
  ... 
}

И модифицированный цикл будет выглядеть так:

foreach(Employee employee in employees) {
  if (employee.getProperties().contains(EmployeeProperty.RECEIVES_BONUS)) {
    PayBonus(employee);
  }
}

Это решение гораздо более гибкое, чем создание подклассов:

  • он может тривиально обрабатывать любую комбинацию свойств сотрудников, в то время как при создании подклассов вы будете испытывать комбинаторный взрыв подклассов по мере роста числа свойств,
  • это тривиально позволяет вам изменять время выполнения свойств Employee, в то время как при создании подклассов это потребует изменения конкретного класса вашего объекта!

В Java перечисления могут иметь свойства или (даже виртуальные) методы сами по себе - я не знаю, возможно ли это в C #, но в худшем случае, если вам нужны более сложные свойства, вы можете реализовать их с помощью класса иерархия. (Даже в этом случае вы не вернетесь к исходной точке, поскольку у вас есть дополнительный уровень косвенности, который дает вам гибкость, описанную выше.)

Обновление

Вы правы в том, что в наиболее общем случае (обсуждавшемся в последнем предложении выше) проблема преобразования типов не решена, просто на один уровень опускается на граф объектов.

В общем, я не знаю действительно удовлетворительного решения этой проблемы. Типичный способ справиться с этим - использовать полиморфизм: подтянуть общий интерфейс и манипулировать объектами через него, тем самым устраняя необходимость в откате. Но что делать в тех случаях, когда рассматриваемые объекты не имеют общего интерфейса? Это может помочь понять, что в этих случаях дизайн плохо отражает реальность: практически, мы создали интерфейс маркера исключительно для того, чтобы мы могли поместить несколько отдельных объектов в общую коллекцию, но между объектами нет семантической связи.

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

0 голосов
/ 26 октября 2010

Вы можете реализовать пользовательский итератор, который выполняет итерации только по типам IRecievesBonus.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...