В этой серии мы исследуем проблему «игрок может использовать оружие, волшебник — разновидность игрока, посох — разновидность оружия, а волшебник может использовать только посох». Лучшее решение, которое мы придумали до сих пор — выбросить исключение преобразования типа во время выполнения, если разработчик допустил ошибку. Это не кажется оптимальным решением. У этого кода есть приятное свойство: если у вас есть ссылка на Волшебника, публичный интерфейс дает вам Посох когда вы запрашиваете Оружие, а система типов не позволяет вам присвоить Меч в качестве Оружия Волшебника. Но у него есть не очень приятные свойства. Мы по-прежнему нарушаем Принцип Подстановки Лисков (LSP): если у нас есть ссылка на Игрока, мы можем без ошибок присвоить Меч в качестве Оружия: В этой версии исключение происходит, когда мы пытаемся получить Посох Волшебника! Такую «бомбу замедленного действия» очень сложно отлаживать, и оно нарушает правило, согласно которому геттер никогда не должен выбрасывать исключение. Мы могли бы решить эту проблему следующим образом: Теперь, если вы хотите присвоить оружие, вам нужна ссылка на Волшебника, а не на Игрока, а сеттер обеспечивает безопасность типов. Это довольно хорошо, но все же не отлично. Если у нас есть ссылка на Волшебника и Посох в руке, то все хорошо. Но Волшебник находится в переменной типа Игрок, тогда нам нужно знать, к какому типу привести Игрока, чтобы сделать то, что является допустимым действием, но не разрешенным для типа Игрока. И, конечно же, та же проблема возникает, если Посох находится в переменной типа Оружие; теперь мы должны знать, к чему его привести, чтобы задать свойство.  На самом деле мы не многого здесь добились; у нас все время будут возникать приведения типов, некоторые из которых могут потерпеть неудачу, и в случае неудачи надо что-то делать. Интерфейсы! Да, это вариант.  Любую проблему с иерархией классов можно решить, добавив дополнительные слои абстракции, за исключением, может быть, проблемы слишком большого числа слоев абстракции. Мы потеряли то приятное свойство, что у нас есть удобный контейнер для кода, общий для всех игроков, но это поправимо, созданием базового класса. Хотя это популярное решение, на самом деле мы просто передвигаем проблему, а не исправляем ее. Полиморфизм по-прежнему полностью сломан, потому что кто-то может иметь ссылку на IPlayer и присвоить Меч в качестве Оружия, что вызовет исключение. Интерфейсы в том виде, в каком мы их использовали здесь, по существу ничем не лучше абстрактных базовых классов. Пришло время доставать большие пушки - ограничения обобщенных типов (generic constraints)! Да, детка! Это настолько ужасно, что я даже не знаю, с чего начать, но я попытаюсь. Во-первых, добавив параметрический полиморфизм, мы полностью потеряли полиморфизм подтипов. Метод больше не может принимать Игрока, он должен принимать Игрока-Который-Может-Использовать-Конкретное-Оружие, или это должен быть обобщенный метод. Мы могли бы решить эту проблему, наследуя обобщенный тип Игрока<Оружие> от необобщенного типа Игрока, у которого нет свойства Оружия, но теперь вы не можете воспользоваться тем фактом, что у Игрока есть Оружие, если у вас есть ссылка на необобщенного Игрока. Во-вторых, это полное злоупотребление нашей интуицией в отношении обобщенных типов. Мы хотим, чтобы подтипы представляли отношение «волшебник — это разновидность игрока», а дженерики — для представления отношений «коробка с фруктами и коробка с печеньем —  разновидности коробок, но не разновидности друг друга». Я знаю, что корзина с фруктами — это корзина с фруктами, но я вижу, что «волшебник — это игрок с посохом», и я не знаю, что это значит. У меня нет интуитивного понимания, что обобщения в том виде, в каком мы их здесь использовали, означают «волшебник — это игрок, который может использовать только посох». В-третьих, похоже, что это не будет масштабироваться. Если я хочу также сказать «игрок может носить доспехи, а волшебник может носить только мантию», и «игрок может читать книги, а воин может читать только немагические книги» и так далее, мы собираемся получить полдюжины аргументов типа Игрок? Объединим попытки 4 и 5. Проблема возникает, когда мы пытаемся менять оружие. Мы могли бы сделать классы игроков иммутабельными, создавать нового игрока каждый раз, когда меняется оружие, и теперь мы можем сделать интерфейс ковариантным. Хотя я большой поклонник иммутабельности, мне не очень нравится это решение.  Похоже, мы пытаемся смоделировать что-то, что действительно изменчиво в коде, и поэтому мне нравится, когда эта мутация представлена в коде. Кроме того, не совсем понятно, как это решает проблему. Если у нас ссылка на иммутабельного Игрока, которого мы клонируем, нам все равно нужен какой-то способ предотвратить создание нового Волшебника с Мечом. Ах да, еще кое-что. Увидев Арагорна с его арсеналом, я вспомнил, упоминал ли я ранее, что и волшебники, и воины могут использовать кинжалы? Возможно, ваши пользователи всегда точно сообщают вам точные ограничения предметной области, прежде чем вы начнете кодировать каждый проект, и они никогда не изменятся в будущем. Ну я не такой пользователь. Мне нужно, чтобы система учитывала тот факт, что и волшебники, и воины могут использовать кинжалы, и нет, кинжалы не являются «особым видом меча», так что даже не спрашивайте. Очевидно, что здесь нам нужно объединить все методы, которые мы видели до сих пор в этом эпизоде: Я думаю на этом закончим. Простое написание этого небольшого отрывка отняло бы у меня лишние несколько часов жизни. Оставим пока в стороне проблему посохов и мечей и рассмотрим связанную с ней проблему проектирования иерархии классов: предположим, паладин замахивается мечом на оборотня в церкви после полуночи. Это касается класса игрока, монстра, оружия или локации? Или, может быть, какая-то их комбинация? И если типы ни одного из них не известны во время компиляции, что тогда? Об этом в следующем выпуске.