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

от автора

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

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

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

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

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

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

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

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

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; }   }  }

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

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

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


ссылка на оригинал статьи https://habr.com/ru/post/710748/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *