Распространенная задача, которую я вижу в объектно-ориентированном проектировании:
Волшебник — это разновидность игрока.
Воин — это разновидность игрока.
У игрока есть оружие.
Посох — это разновидность оружия.
Меч — это разновидность оружия.
Но прежде чем мы углубимся в детали, я просто хочу отметить, что на самом деле я не говорю здесь о чем-то специфичном для жанра фэнтезийных ролевых игр. Всё в этой серии одинаково хорошо применимо к корпоративным приложениями, но о волшебниках и воинах писать интереснее, так что вот.
Хорошо, отлично, у нас есть пять пунктов, так что давайте напишем несколько классов, соответствующих постановке! Что может пойти не так?
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; }
}
}
Теперь мы превратили нарушения правила из ограничений системы типов в исключения времени выполнения. Это подвержено ошибкам, вызывающий код может иметь ссылку на Волшебника
и присвоить Меч
свойству Оружие
.
Весь смысл отражения семантики предметной области в типах заключается в том, что нарушение обнаруживается во время компиляции.
(следующая статья) Какие еще способы представить эти правила в системе типов?