Полиморфизм И безопасность типов в параллельных цепочках наследования - PullRequest
1 голос
/ 11 марта 2009

У меня есть две параллельные цепочки наследования:

Chain1:
Animal <- Lion
       <- Gazelle

Chain2:
Food   <- Meat
       <- Grass

Я хочу реализовать полиморфное свойство "Eats" в Animal. Вот как это выглядит:

public abstract class Animal
{
  public abstract Food Eats { get; set;}
}


public class Lion : Animal
{
  public override Food Eats
  {
     get { return new Meat();}
     set 
     {
       if (value is Meat) DoSomething(value);
       else throw new Exception("Lions only eat meat. " + 
                                "You better learn that, dude!");
     }
  }
}

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

Может ли кто-нибудь предоставить мне пример кода, который облегчает безопасность типов, используя Generics, не жертвуя полиморфизмом?

Ответы [ 4 ]

3 голосов
/ 11 марта 2009

Использование композиции поверх наследования:

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

public interface IDigest
{
  void Eat(Meat food);
  void Eat(Plant food);
  void Eat(Offal food); //lol nethack
}

Плотоядные животные едят мясо, иногда могут есть травы и не любят дерьма:

public class Carnivorous : IDigest
{ 
  public void Eat(Meat food)
  {
    Console.Write("NOM NOM");
  }
  public void Eat(Plant food)
  {
    if(Starving)
     Console.Write("Ugh, better than nothing.");
    else
      Vomit();
  }
  public void Eat(Offal food)
  {
    Vomit();
  }
}

Травоядные привередливы и скорее умрут, чем съедят мясо (я знаю, сохраните ваши комментарии, это пример)

public class Herbivorous : IDigest
{ 
  public void Eat(Meat food)
  {
    Vomit();
  }
  public void Eat(Plant food)
  {
    Console.Write("NOM NOM");
  }
  public void Eat(Offal food)
  {
    Vomit();
  }
}

Всеядные едят что угодно. Посмотрите государственную ярмарку.

public class Omnivorous : IDigest
{ 
  public void Eat(Meat food)
  {
    Console.Write("NOM NOM");
  }
  public void Eat(Plant food)
  {
    Console.Write("NOM NOM");
  }
  public void Eat(Offal food)
  {
    Console.Write("NOM NOM");
  }
}

Все животные должны есть, поэтому они должны иметь пищеварительную систему наряду с другими системами.

public abstract class Animal
{
  /* lots of other stuff */
  public IConsume DigestiveSystem {get;set;}
  /* lots of other stuff */
}

Хиппи - это класс животных с известными вкусами; он настраивает себя на создание экземпляров. Также возможно ввести поведение и системы извне.

public class Hippie : Animal
{
  public Hippie()
  {
    /*stuff*/
    DigestiveSystem = new Herbivore();
    BodyOdorSystem = new Patchouli();
    /*more stuff*/
  }
}

И, наконец, давайте посмотрим, как хиппи съест бургер.

public static void Main()
{
  Hippie dippie = new Hippie();
  Meat burger = new Meat("Burger", 2/*lb*/);
  dippie.DigestiveSystem.Eat(burger);
}

При моделировании сложных систем, таких как животные, я бы предпочел композицию, а не наследование в ЛЮБОЙ ДЕНЬ. Сложные системы могут быстро взорвать дерево наследования. Возьмите три системы животных: всеядное животное / травоядное животное / плотоядное животное, вода / воздух / земля и ночной / суточный. Давайте даже не будем беспокоиться о том, как решить, какая классификация станет первой точкой дифференциации для животных. Распространяем ли мы Animal сначала на Carnivore, сначала на WaterLiving или на Nocturnal?

Поскольку всеядное существо может жить в воздухе и предпочитать ночь (летучая мышь *), а также быть дневным существом (людьми), вы должны иметь путь наследования, который затрагивает каждый отдельный вариант. Это наследственное дерево с 54 различными типами (его рано, будьте добры). И животные намного сложнее, чем это. Вы могли легко получить дерево наследования, которое имело миллионы типов. Композиция по наследству, определенно.

* Новозеландская короткохвостая летучая мышь, например, всеядна.

1 голос
/ 11 марта 2009

Животное может быть универсальным классом:

public abstract class Animal<T> where T : Food
{
    public abstract T Eats {get;set;}
}

тогда вы можете сделать льва мясоедом, как это

public class Lion : Animal<Meat>
{
    //etc...
}    

Но это не будет оптимальным решением. Вы больше не можете использовать animal как полиморфный интерфейс, потому что вам нужно знать детали его реализации, чтобы использовать его. Это может быть просто не место для полиморфизма.

0 голосов
/ 11 марта 2009

Я думаю, что это немного ложная дилемма. Еда, кажется, ближе к интерфейсу, чем абстрактный базовый класс, поскольку не похоже, что мясо будет совсем похоже на Grass. Вместо этого рассмотрим что-то вроде:

public interface IFood {
  public boolean IsForCarnivores();
}

public class Lion : Animal {
  ...
  public override IFood Eats
  {
    get { ... }
    set 
    {
      if (value.IsForCarnivores()) DoSomething(value);
      else throw new Exception("I can't eat this!");
    }
  }
}
0 голосов
/ 11 марта 2009

Хм, возможно, вы могли бы изменить свою первую цепочку наследования:

Animal - Плотоядное животное - лев - тигр - ... - травоядное - овцы

Тогда, возможно, вы могли бы сделать что-то вроде этого:

public class Animal<T> where T : Food
{
    public abstract T Eats { get; set; }
}

public class Carnivore : Animal<Meat>
{
   ...
}

Я не проверял, у меня просто идея ...

...