Я занимаюсь этим вопросом , но переключаюсь с кода на принцип.
Исходя из моего понимания принципа подстановки Лискова (LSP), какие бы методы ни находились в моем базовом классе, они должны быть реализованы в моем подклассе, и, согласно этой странице, если вы переопределяете метод в базовом классе и он ничего не делает или выдает исключение, вы нарушаете принцип.
Теперь моя проблема может быть подытожена так: у меня есть реферат Weapon
class
, два класса Sword
и Reloadable
. Если Reloadable
содержит конкретный method
, вызываемый Reload()
, я должен был бы понизить доступ к нему method
, и, в идеале, вы бы хотели этого избежать.
Затем я подумал об использовании Strategy Pattern
. Таким образом, каждое оружие было осведомлено только о действиях, которые оно может выполнить, так что, например, Reloadable
оружие может перезаряжаться, но Sword
не может и даже не знает о Reload class/method
. Как я уже говорил в своем сообщении о переполнении стека, мне не нужно унывать, и я могу поддерживать List<Weapon>
коллекцию.
На другом форуме первый ответ предложил дать Sword
знать Reload
, просто ничего не делать. Этот же ответ был дан на странице переполнения стека, на которую я ссылался выше.
Я не до конца понимаю почему. Зачем нарушать принцип и позволять Мечу быть в курсе Reload
и оставлять его пустым? Как я сказал в своем посте переполнения стека, SP, в значительной степени решил мои проблемы.
Почему это не жизнеспособное решение?
public final Weapon{
private final String name;
private final int damage;
private final List<AttackStrategy> validactions;
private final List<Actions> standardActions;
private Weapon(String name, int damage, List<AttackStrategy> standardActions, List<Actions> attacks)
{
this.name = name;
this.damage = damage;
standardActions = new ArrayList<Actions>(standardActions);
validAttacks = new ArrayList<AttackStrategy>(validActions);
}
public void standardAction(String action){} // -- Can call reload or aim here.
public int attack(String action){} // - Call any actions that are attacks.
public static Weapon Sword(String name, damage, List<AttackStrategy> standardActions, List<Actions> attacks){
return new Weapon(name, damage,standardActions, attacks) ;
}
}
Интерфейс атаки и реализация:
public interface AttackStrategy{
void attack(Enemy enemy);
}
public class Shoot implements AttackStrategy {
public void attack(Enemy enemy){
//code to shoot
}
}
public class Strike implements AttackStrategy {
public void attack(Enemy enemy){
//code to strike
}
}
class Weapon { bool supportsReload(); void reload(); }
. Клиенты будут проверять, если поддерживается, до перезагрузки.reload
определяется контрактом бросить тогда и только тогда!supportsReload()
. Это придерживается LSP, если классы вождения придерживаются протокола, который я только что обрисовал.reload()
пустым илиstandardActions
нет действия перезагрузки - это просто другой механизм. Там нет принципиальной разницы. Вы можете сделать оба. => Ваше решение является жизнеспособным (это был ваш вопрос) .; Sword не нужно знать о перезагрузке, если Weapon содержит пустую реализацию по умолчанию.Ответы:
LSP обеспокоен подтипом и полиморфизмом. Не весь код на самом деле использует эти функции, и в этом случае LSP не имеет значения. Два распространенных варианта использования конструкций языка наследования, которые не относятся к подтипам:
Наследование используется для наследования реализации базового класса, но не его интерфейса. Почти во всех случаях состав должен быть предпочтительным. Такие языки, как Java, не могут разделять наследование реализации и интерфейса, но, например, C ++ имеет
private
наследование.Наследование, используемое для моделирования типа / объединения суммы, например: a
Base
- либо,CaseA
либоCaseB
. Базовый тип не объявляет какой-либо соответствующий интерфейс. Чтобы использовать его экземпляры, вы должны привести их к правильному конкретному типу. Кастинг может быть выполнен безопасно, и это не проблема. К сожалению, многие языки ООП не могут ограничивать подтипы базового класса только предполагаемыми подтипами. Если внешний код может создать aCaseC
, то код, предполагающий, что aBase
может быть толькоCaseA
илиCaseB
неверен. Scala может сделать это безопасно благодаря своейcase class
концепции. В Java это можно смоделировать, когдаBase
абстрактный класс имеет закрытый конструктор, а вложенные статические классы наследуются от базы.Некоторые концепции, такие как концептуальные иерархии объектов реального мира, очень плохо отображаются в объектно-ориентированных моделях. Такие мысли, как «Оружие - это оружие, а меч - это оружие, поэтому у меня будет
Weapon
базовый класс, от которогоGun
иSword
унаследуют», вводят в заблуждение: настоящее слово «отношения» не подразумевает такие отношения в нашей модели. Одна связанная проблема заключается в том, что объекты могут принадлежать нескольким концептуальным иерархиям или могут изменять свою иерархическую принадлежность во время выполнения, что большинство языков не могут моделировать, поскольку наследование обычно для каждого класса, а не для объекта, и определяется во время разработки, а не во время выполнения.При разработке моделей ООП мы не должны думать об иерархии или о том, как один класс «расширяет» другой. Базовый класс - это не место, чтобы выделять общие части нескольких классов. Вместо этого подумайте о том, как будут использоваться ваши объекты, то есть о том, какое поведение нужно пользователям этих объектов.
Здесь пользователям может понадобиться
attack()
оружие, а может иreload()
их. Если мы хотим создать иерархию типов, то оба этих метода должны быть базового типа, хотя неперезагружаемое оружие может игнорировать этот метод и ничего не делать при вызове. Таким образом, базовый класс содержит не общие части, а объединенный интерфейс всех подклассов. Подклассы не отличаются по своему интерфейсу, а только по реализации этого интерфейса.Нет необходимости создавать иерархию. Два типа
Gun
иSword
могут быть совершенно не связаны. В то время как вGun
банкеfire()
и может только . Если вам нужно управлять этими объектами полиморфно, вы можете использовать шаблон адаптера для захвата соответствующих аспектов. В Java 8 это возможно довольно удобно с функциональными интерфейсами и ссылками на лямбды / методы. Например, у вас может быть стратегия, для которой вы поставляете или .reload()
Sword
strike()
Attack
myGun::fire
() -> mySword.strike()
Наконец, иногда разумно вообще избегать подклассов, но моделировать все объекты одним типом. Это особенно актуально в играх, потому что многие игровые объекты плохо вписываются в какую-либо иерархию и могут иметь много разных возможностей. Например, в ролевой игре может быть предмет, который является одновременно и квестовым предметом, повышает вашу статистику с силой +2 при экипировке, имеет 20% шанс игнорировать любой получаемый урон и обеспечивает рукопашную атаку. Или, может быть, перезаряжаемый меч, потому что это * магия *. Кто знает, чего требует история.
Вместо того, чтобы пытаться выяснить иерархию классов для этого беспорядка, лучше иметь класс, который предоставляет слоты для различных возможностей. Эти слоты могут быть изменены во время выполнения. Каждый слот будет стратегией / обратным вызовом типа
OnDamageReceived
илиAttack
. С вашим оружием, мы можем иметьMeleeAttack
,RangedAttack
иReload
слоты. Эти слоты могут быть пустыми, и в этом случае объект не предоставляет эту возможность. Щели затем называется условно:if (item.attack != null) item.attack.perform()
.источник
Потому что наличие стратегии
attack
не достаточно для ваших нужд. Конечно, это позволяет вам абстрагироваться от того, какие действия может выполнять предмет, но что происходит, когда вам нужно знать радиус действия оружия? Или боезапас? Или какие патроны нужны? Вы вернулись к унынию, чтобы добраться до этого. А наличие такого уровня гибкости сделает пользовательский интерфейс более сложным для реализации, поскольку для работы со всеми возможностями потребуется подобный шаблон стратегии.На все сказанное я не особо согласен с ответами на другие ваши вопросы. Имея
sword
Наследовать отweapon
являешься ужасающим, наивным OO , который неизменно приводит к методам не-оп или типа проверкам валялся кода.Но в корне проблемы ни одно из решений не является неправильным . Вы можете использовать оба решения для создания функциональной игры, в которую интересно играть. Каждый приходит со своим набором компромиссов, как и любое решение, которое вы выбираете.
источник
Weapon
класса с экземпляром меча и оружия.WeaponBuilder
который мог бы строить мечи и оружие, составляя оружие стратегий.Конечно, это жизнеспособное решение; это просто очень плохая идея.
Проблема не в том, есть ли у вас этот единственный экземпляр, в который вы помещаете reload в свой базовый класс. Проблема в том, что вам также нужно поставить «качели», «стрелять», «парировать», «стучать», «полировать», «разбирать», «точить» и «заменять гвозди заостренного конца клюшки» метод в вашем базовом классе.
Смысл LSP в том, что ваши алгоритмы верхнего уровня должны работать и иметь смысл. Так что, если у меня есть такой код:
Теперь, если это вызывает не реализованное исключение и приводит к аварийному завершению вашей программы, тогда это очень плохая идея.
Если ваш код выглядит так,
тогда ваш код может стать загроможденным очень специфическими свойствами, которые не имеют ничего общего с абстрактной идеей «оружия».
Однако, если вы применяете шутер от первого лица и все ваше оружие может стрелять / перезаряжаться, кроме одного ножа, то (в вашем конкретном контексте) имеет большой смысл, чтобы перезагрузка вашего ножа ничего не делала, так как это исключение и шансы низкая плотность вашего базового класса с определенными свойствами.
Обновление: попробуйте подумать об абстрактном падеже / терминах. Например, может быть, у каждого оружия есть действие «подготовить», которое заключается в перезарядке оружия и обнажении мечей.
источник
Очевидно, что это нормально, если вы не создаете подкласс с намерением заменить экземпляр базового класса, но если вы создаете подкласс, используя базовый класс в качестве удобного хранилища функциональности.
Теперь ли это хорошая идея или не очень спорно, но если вы никогда не заменить подкласс для BaseClass, то тот факт, что он не работает никаких проблем. У вас могут быть проблемы, но LSP не является проблемой в этом случае.
источник
LSP хорош, потому что позволяет вызывающему коду не беспокоиться о том, как работает класс.
например. Я могу вызвать Weapon.Attack () для всех видов оружия, установленных на моем BattleMech, и не беспокоиться о том, что некоторые из них могут вызвать исключение и привести к сбою в моей игре.
Теперь в вашем случае вы хотите расширить базовый тип новыми функциями. Attack () не является проблемой, потому что класс Gun может отслеживать свои боеприпасы и прекращать стрельбу, когда он заканчивается. Но Reload () - это что-то новое, а не часть оружия.
Простое решение состоит в том, чтобы унывать, я не думаю, что вам нужно чрезмерно беспокоиться о производительности, вы не собираетесь делать это каждый кадр.
В качестве альтернативы вы можете пересмотреть свою архитектуру и считать, что в абстрактном виде все Оружие перезаряжается, а некоторые виды оружия просто не нуждаются в перезарядке.
Тогда вы больше не продлеваете класс оружия и не нарушаете LSP.
Но это проблематично в долгосрочной перспективе, потому что вы обязаны придумать больше особых случаев, Gun.SafteyOn (), Sword.WipeOffBlood () и т. Д., И если вы поместите их все в Weapon, то у вас будет очень сложный обобщенный базовый класс, который вы сохраните приходится менять.
редактировать: почему шаблон стратегии является плохим (тм)
Это не так, но рассмотрим настройку, производительность и общий код.
У меня должен быть где-то конфиг, который говорит мне, что пистолет может перезарядиться. Когда я создаю оружие, я должен прочитать этот конфиг и динамически добавить все методы, проверить, нет ли повторяющихся имен и т. Д.
Когда я вызываю метод, мне приходится проходить через этот список действий и сопоставлять строки, чтобы увидеть, какой из них вызывать.
Когда я компилирую код и вызываю Weapon.Do («атака») вместо «атаки», я не получаю ошибку при компиляции.
Это может быть подходящим решением для некоторых проблем, скажем, у вас есть сотни видов оружия с различными комбинациями случайных методов, но вы теряете много преимуществ ОО и строгой типизации. Это не спасет вас от удручения
источник
SafteyOn()
иSword
будет иметьwipeOffBlood()
. Каждое оружие не знает о других методах (и они не должны быть)weapon.do("attack")
и тип, и тип-сейфweapon.attack.perform()
могут быть примерами шаблона стратегии. Поиск стратегий по имени необходим только при настройке объекта из файла конфигурации, хотя использование отражения будет в равной степени безопасным для типов.