Pull to refresh

Воины и волшебники, часть первая

Reading time2 min
Views5K
Original author: Eric Lippert

Распространенная задача, которую я вижу в объектно-ориентированном проектировании:

  • Волшебник — это разновидность игрока.

  • Воин — это разновидность игрока.

  • У игрока есть оружие.

  • Посох — это разновидность оружия.

  • Меч — это разновидность оружия.

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

Хорошо, отлично, у нас есть пять пунктов, так что давайте напишем несколько классов, соответствующих постановке! Что может пойти не так?

abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }
abstract class Player 
{ 
  public Weapon Weapon { get; set; }
}
sealed class Wizard : Player { }
sealed class Warrior : Player { }

Разработка хорошей иерархии классов заключается в отражении семантики предметной области в системе типов, верно? И здесь мы проделали большую работу. Если есть поведение, общее для всех игроков, оно относится к абстрактному базовому классу. Если есть поведение, уникальное для волшебников или воинов, оно может быть передано в производные классы. Ясно, что мы на пути к успеху.

Пока не появились новые требования…

  • Воин может использовать только меч.

  • Волшебник может использовать только посох.

Какое неожиданное развитие событий!

Что теперь делать? Читатели, знакомые с теорией типов, знают, что проблема заключается в нарушении Принципа Подстановки Лисков (LSP). Но нам не нужно понимать лежащую в основе теорию типов, чтобы увидеть, что происходит ужасно неправильно. Все, что нам нужно сделать, это попытаться изменить код для поддержки новых требований.

Попытка №1

abstract class Player 
{ 
  public abstract Weapon Weapon { get; set; }
}
sealed class Wizard : Player
{
  public override Staff Weapon { get; set; }
}

Нет, в C# это не скомпилируется. Переопределяющий член класса должен соответствовать сигнатуре (и типу возвращаемого значения) переопределяемого члена.

Попытка №2

abstract class Player 
{ 
  public abstract Weapon { get; set; }
}
sealed class Wizard : Player
{
  private Staff weapon;
  public override Weapon Weapon 
  {
     get { return weapon; }
     set { weapon = (Staff) value; }
  } 
}

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

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

(следующая статья) Какие еще способы представить эти правила в системе типов?

Tags:
Hubs:
Total votes 8: ↑5 and ↓3+2
Comments24

Articles