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

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

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

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

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

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

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

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

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)


  1. motoroller95
    16.01.2023 12:28
    +4


  1. boriselec
    16.01.2023 12:40
    -1

    Нет, в C# это не скомпилируется.

    Скомпилируется с 9.0. Ковариантные возвращаемые значения появились.

    https://devblogs.microsoft.com/dotnet/c-9-0-on-the-record/


    1. gandjustas Автор
      16.01.2023 12:48

      Но вы их не сможете использовать в свойствах


      1. Naf2000
        17.01.2023 18:35

        В get-only вполне


    1. Naf2000
      17.01.2023 18:35
      +1

      Если бы это было get-only свойство, то сработало бы, всё портит что оно set


  1. s207883
    16.01.2023 13:20

    Зачем пихать всюду ООП, если можно сделать свойство вроде AvailableWeapons. Таким же образом составить карту возможных умений, которых мог бы изучить конкретный класс.


    1. gandjustas Автор
      16.01.2023 13:23

      Дождитесь следующих выпусков. Если не терпится - почитайте оригинал до конца.


  1. JuniorNoobie
    16.01.2023 13:28
    +2

    Я бы проверку на возможность носить тот или иной предмет поставил в действие "одеть оружие/применить предмет".

    А ответ на то как это нужно делать далее будет где-то раскрыт? Я так понял, что тут только половина статьи или вообще ее начало...

    П.С. Скажите Гендальфу, что он не может носить меч и посох одновременно)


    1. gandjustas Автор
      16.01.2023 13:31

      Я бы проверку на возможность носить тот или иной предмет поставил в действие "одеть оружие/применить предмет".

      У меня есть ощущение, что вы читали оригинал

      А ответ на то как это нужно делать далее будет где-то раскрыт?

      Конечно будет.

      Я так понял, что тут только половина статьи или вообще ее начало...

      Верно, только начало.

      П.С. Скажите Гендальфу, что он не может носить меч и посох одновременно)

      Эта шутка есть во второй части.


      1. JuniorNoobie
        16.01.2023 16:48

        Нет, статью не читал, но фамилия автора где-то уже мелькала буквально сегодня. Буду ждать ваших переводов)

        А что касается проверять при смене оружия - показалось самым логичным. Возможно, что геймерско-программистский опыт сказывается.


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

     }

     

    Волшебник становится волшебником, так как у него есть посох.


    1. gandjustas Автор
      16.01.2023 15:37
      +1

      Так вы полностью потеряли полиморфизм и пропал смысл в базовом классе Player.

      Разбор этого варианта будет в следующих статьях серии.


    1. Squoworode
      16.01.2023 22:23
      +2

      Не посох делает волшебника, а волшебник посох.

      Волшебник без посоха подобен волшебнику с посохом, но без посоха.


      1. donRumatta
        17.01.2023 17:01

        Мое уважение


      1. lexxpavlov
        17.01.2023 21:39

        Самурай одобряет!


  1. 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(); }
        }


    1. gandjustas Автор
      16.01.2023 18:57
      -1

      Этот вариант тоже будет в следующем выпуске.

      Пока ждете выпуск напишите как поместить несколько Игроков в контейнер и получить у них всех Оружие.


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


        1. gandjustas Автор
          17.01.2023 00:39

          Какой смысл? У любого конкретного класса нельзя будет получить Weapon. Полиморфизм как бы есть, но будет выпадать ошибка.


          1. vxd_dev
            17.01.2023 01:36

            Удалено


  1. ilya-chumakov
    17.01.2023 11:46

    В чем ценность именно этого коротенького перевода, состоящего из 2 не самых удачных примеров кода? Получился бы более достойный хабра материал при склейке всех переводов в одну статью.


    1. gandjustas Автор
      17.01.2023 13:17

      Исходные статьи разбиты так.