Распространенная задача, которую я вижу в объектно-ориентированном проектировании:
Волшебник — это разновидность игрока.
Воин — это разновидность игрока.
У игрока есть оружие.
Посох — это разновидность оружия.
Меч — это разновидность оружия.
Но прежде чем мы углубимся в детали, я просто хочу отметить, что на самом деле я не говорю здесь о чем-то специфичном для жанра фэнтезийных ролевых игр. Всё в этой серии одинаково хорошо применимо к корпоративным приложениями, но о волшебниках и воинах писать интереснее, так что вот.
Хорошо, отлично, у нас есть пять пунктов, так что давайте напишем несколько классов, соответствующих постановке! Что может пойти не так?
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; }
}
}
Теперь мы превратили нарушения правила из ограничений системы типов в исключения времени выполнения. Это подвержено ошибкам, вызывающий код может иметь ссылку на Волшебника
и присвоить Меч
свойству Оружие
.
Весь смысл отражения семантики предметной области в типах заключается в том, что нарушение обнаруживается во время компиляции.
(следующая статья) Какие еще способы представить эти правила в системе типов?
Комментарии (22)
boriselec
16.01.2023 12:40-1Нет, в C# это не скомпилируется.
Скомпилируется с 9.0. Ковариантные возвращаемые значения появились.
Naf2000
17.01.2023 18:35+1Если бы это было get-only свойство, то сработало бы, всё портит что оно set
s207883
16.01.2023 13:20Зачем пихать всюду ООП, если можно сделать свойство вроде AvailableWeapons. Таким же образом составить карту возможных умений, которых мог бы изучить конкретный класс.
gandjustas Автор
16.01.2023 13:23Дождитесь следующих выпусков. Если не терпится - почитайте оригинал до конца.
JuniorNoobie
16.01.2023 13:28+2Я бы проверку на возможность носить тот или иной предмет поставил в действие "одеть оружие/применить предмет".
А ответ на то как это нужно делать далее будет где-то раскрыт? Я так понял, что тут только половина статьи или вообще ее начало...
П.С. Скажите Гендальфу, что он не может носить меч и посох одновременно)
gandjustas Автор
16.01.2023 13:31Я бы проверку на возможность носить тот или иной предмет поставил в действие "одеть оружие/применить предмет".
У меня есть ощущение, что вы читали оригинал
А ответ на то как это нужно делать далее будет где-то раскрыт?
Конечно будет.
Я так понял, что тут только половина статьи или вообще ее начало...
Верно, только начало.
П.С. Скажите Гендальфу, что он не может носить меч и посох одновременно)
Эта шутка есть во второй части.
JuniorNoobie
16.01.2023 16:48Нет, статью не читал, но фамилия автора где-то уже мелькала буквально сегодня. Буду ждать ваших переводов)
А что касается проверять при смене оружия - показалось самым логичным. Возможно, что геймерско-программистский опыт сказывается.
pygubanov
16.01.2023 14:34У Вас на лицо не верные требования из которых получается неправильная реализация.
Неверное решение, что у игрока есть оружие.
Переписав по другому всё становится намного лучше
abstract class Weapon { }
sealed class Staff : Weapon { }
sealed class Sword : Weapon { }
abstract class Player { }
sealed class Wizard : Player
{
public Staff Staff { get; set; }
}
sealed class Warrior : Player
{
public Sword Sword { get; set; }
}
Волшебник становится волшебником, так как у него есть посох.
gandjustas Автор
16.01.2023 15:37+1Так вы полностью потеряли полиморфизм и пропал смысл в базовом классе
Player
.Разбор этого варианта будет в следующих статьях серии.
Squoworode
16.01.2023 22:23+2Не посох делает волшебника, а волшебник посох.
Волшебник без посоха подобен волшебнику с посохом, но без посоха.
vxd_dev
16.01.2023 18:45-1Почему бы тогда не использовать дженерики?
abstract class Weapon { } sealed class Staff : Weapon { } sealed class Sword : Weapon { } abstract class Player<T> where T: Weapon { public abstract T Weapon { get; set; } } sealed class Wizard : Player<Staff> { public override Staff Weapon { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } } sealed class Warrior : Player<Sword> { public override Sword Weapon { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } }
gandjustas Автор
16.01.2023 18:57-1Этот вариант тоже будет в следующем выпуске.
Пока ждете выпуск напишите как поместить несколько Игроков в контейнер и получить у них всех Оружие.
vxd_dev
16.01.2023 19:27Понял в чем проблема с дженериками, тогда можно как-то так:
abstract class Player { public abstract Weapon Weapon { get; } protected abstract void SetWeapon(Weapon weapon); } sealed class Wizard : Player { public override Weapon Weapon => throw new NotImplementedException(); public void SetWeapon(Staff weapon) { SetWeapon((Weapon)weapon); } protected override void SetWeapon(Weapon weapon) { throw new NotImplementedException(); } }
gandjustas Автор
17.01.2023 00:39Какой смысл? У любого конкретного класса нельзя будет получить Weapon. Полиморфизм как бы есть, но будет выпадать ошибка.
ilya-chumakov
17.01.2023 11:46В чем ценность именно этого коротенького перевода, состоящего из 2 не самых удачных примеров кода? Получился бы более достойный хабра материал при склейке всех переводов в одну статью.
motoroller95