迭代超类型集合时键入转换。备择方案?

时间:2010-10-26 10:44:50

标签: oop language-agnostic

这是我遇到的一个常见问题。让我们听听您的解决方案。我将使用Employee-managing应用程序作为示例: -

我们有一些实体类,其中一些实现特定的接口。

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)有没有我没有想过的替代方案?

修改

PéterTörök建议编写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类型中推出 - 特别是因为有些经理可能会喝茶,有些可能不会 - 但我们仍然有类型转换的相同根本问题。

只要我们在合适的级别进行,它是否“ok”?或者我们只是解决问题?

圣杯将成为访客模式的变体,其中: -

  • 您可以添加元素成员而无需修改所有访问者
    • 访客只能访问他们有兴趣访问的类型
  • 访问者可以根据界面类型访问该成员
    • 元素可能会实现不同访问者访问的多个界面
  • 不涉及施法或反思

但我很欣赏这可能是不切实际的。

2 个答案:

答案 0 :(得分:5)

我肯定会通过将所需的属性/特征与Employee相关联而不是将其子类化来尝试通过组合而不是继承来解决这个问题。

我可以在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#中使用,但在最坏的情况下,如果需要更复杂的属性,可以用类实现它们层次结构。 (即使在这种情况下,你也不会回到原点,因为你有一个额外的间接水平,这为你提供了上述的灵活性。)

更新

你是对的,在最一般的情况下(在上面的最后一句中讨论),类型转换问题没有解决,只是在对象图上向下推了一层。

总的来说,我不知道这个问题真正令人满意的解决方案。处理它的典型方法是使用多态:拉出公共接口并通过它操纵对象,从而消除了向下转换的需要。但是,如果有问题的对象没有通用接口,该怎么办?可能有助于认识到,在这些情况下,设计并不能很好地反映现实:实际上,我们创建了一个标记界面,只是为了使我们能够将一堆不同的对象放入一个公共集合中,但这些对象之间没有语义关系。

所以我相信在这些情况下,向下倾斜的尴尬是一个信号,表明我们的设计可能存在更深层次的问题。

答案 1 :(得分:0)

您可以实现一个只迭代IRecievesBonus类型的自定义迭代器。