Общеизвестным недостатком традиционных иерархий классов является то, что они плохи, когда дело доходит до моделирования реального мира. В качестве примера пытаемся представить виды животных с помощью классов. При этом есть несколько проблем, но я никогда не видел решения, когда подкласс «теряет» поведение или свойство, которое было определено в суперклассе, например, когда пингвин не может летать (там Возможно, это лучшие примеры, но это первое, что приходит мне в голову).
С одной стороны, вы не хотите определять для каждого свойства и поведения какой-либо флаг, который указывает, присутствует ли он вообще, и проверять его каждый раз перед доступом к этому поведению или свойству. Вы просто хотите сказать, что птицы могут просто и ясно летать в классе птиц. Но тогда было бы хорошо, если бы потом можно было определить «исключения», не прибегая к каким-либо ужасным взломам. Это часто происходит, когда система некоторое время была продуктивной. Вы неожиданно находите «исключение», которое вообще не вписывается в оригинальный дизайн, и вы не хотите изменять большую часть своего кода, чтобы приспособить его.
Итак, существуют ли какие-то языковые или конструктивные шаблоны, которые могут чисто решить эту проблему, не требуя серьезных изменений в «суперклассе» и во всем коде, который его использует? Даже если решение обрабатывает только конкретный случай, несколько решений могут вместе сформировать полную стратегию.
Подумав больше, я понимаю, что забыл о принципе замены Лискова. Вот почему вы не можете сделать это. Предполагая, что вы определяете «черты / интерфейсы» для всех основных «групп объектов», вы можете свободно реализовывать черты в разных ветвях иерархии, например, черта «Летающий» может быть реализована птицами и некоторыми особыми видами белок и рыб.
Таким образом, мой вопрос может быть таким: «Как я могу не реализовать черту?» Если ваш суперкласс является Java Serializable, вы должны быть им тоже, даже если у вас нет возможности сериализовать ваше состояние, например, если вы содержали «Socket».
Один из способов сделать это состоит в том, чтобы всегда определять все ваши черты в парах с самого начала: Flying и NotFlying (что вызовет UnsupportedOperationException, если не будет проверено против). Not-trait не будет определять какой-либо новый интерфейс, и его можно будет просто проверить. Походит на "дешевое" решение, особенно если используется с самого начала.
источник
function save_yourself_from_crashing_airplane(Bird b) { f.fly() }
станет намного сложнее. (как сказал Питер Тёрёк, это нарушает LSP)" it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"
считаете ли вы фабричный метод управления поведением хакерским?NotSupportedException
изPenguin.fly()
.class Penguin < Bird; undef fly; end;
. Если вы должны это другой вопрос.Ответы:
Как уже упоминали другие, вы должны пойти против LSP.
Однако можно утверждать, что подкласс - это просто произвольное расширение суперкласса. Это новый объект сам по себе, и единственное отношение к суперклассу состоит в том, что он использует основу.
Это может иметь логический смысл, вместо того, чтобы говорить, что Пингвин - Птица. Вы говорите, что Пингвин унаследовал некоторое подмножество поведения от Птицы.
Как правило, динамические языки позволяют легко выразить это, например, с помощью JavaScript ниже:
В этом конкретном случае
Penguin
активно скрываетBird.fly
метод, который он наследует, записываяfly
свойство со значениемundefined
в объект.Теперь вы можете сказать, что
Penguin
больше нельзя считать нормойBird
. Но, как уже упоминалось, в реальном мире это просто невозможно. Потому что мы моделируемBird
как летающее существо.Альтернатива состоит в том, чтобы не делать широкого предположения, что птица может летать. Было бы разумно иметь
Bird
абстракцию, которая позволяет всем птицам наследовать от нее, без сбоев. Это означает только делать предположения, которые могут содержать все подклассы.Вообще идея Mixin's применима здесь. Имейте очень тонкий базовый класс и смешайте в нем все остальные виды поведения.
Пример:
Если вам интересно, у меня есть реализация
Object.make
Дополнение:
Вы не «не реализуете» черту. Вы просто исправляете свое наследие. Либо вы можете выполнить свой контракт на суперклассы, либо не должны притворяться, что вы принадлежите к этому типу.
Вот где сияет объектная композиция.
Кроме того, Serializable не означает, что все должно быть сериализовано, это только означает, что «состояние, которое вам небезразлично», должно быть сериализовано.
Вы не должны использовать черту NotX. Это просто чудовищный код. Если функция ожидает летающий объект, она должна разбиться и сгореть, когда вы дадите ей мамонта.
источник
AFAIK все языки, основанные на наследовании, построены на принципе замещения Лискова . Удаление / отключение свойства базового класса в подклассе явно нарушило бы LSP, поэтому я не думаю, что такая возможность где-либо реализована. Реальный мир действительно грязный и не может быть точно смоделирован математическими абстракциями.
Некоторые языки предоставляют черты или миксины, именно для более гибкого решения таких проблем.
источник
Class
это подкласс,Module
хотяClass
IS-NOT-AModule
. Но все же имеет смысл быть подклассом, так как он много использует кода. OTOH,StringIO
IS-AIO
, но у этих двух нет никаких отношений наследования (кроме очевидного того, что оба наследуются отObject
, конечно), потому что они не разделяют никакого кода. Классы предназначены для совместного использования кода, а типы - для описания протоколов.IO
иStringIO
имеют тот же протокол, следовательно, того же типа, но их классы не связаны.Fly()
находится в первом примере в: Head First Design Patterns для шаблона стратегии , и это хорошая ситуация для того, почему вы должны «отдавать предпочтение композиции, а не наследованию». ,Вы можете смешивать состав и наследование, имея супертипы
FlyingBird
,FlightlessBird
которые имеют правильное поведение, введенное Фабрикой, которые соответствующие подтипы, например,Penguin : FlightlessBird
получают автоматически, и все остальное, что действительно специфично, обрабатывается Фабрикой как само собой разумеющееся.источник
Разве реальная проблема, которую вы предполагаете,
Bird
заключается вFly
методе? Почему бы нет:Теперь очевидная проблема - множественное наследование (
Duck
), поэтому вам действительно нужны интерфейсы:источник
Во-первых, ДА, любой язык, который допускает легкое динамическое изменение объекта, позволит вам сделать это. Например, в Ruby вы можете легко удалить метод.
Но, как сказал Петер Тёрёк, это нарушит LSP .
В этой части я забуду о LSP и предположу, что:
Вы сказали :
Похоже, что вы хотите, чтобы Python « просил прощения, а не разрешения »
Просто заставьте ваш Penguin генерировать исключение или наследовать от класса NonFlyingBird, который генерирует исключение (псевдокод):
Кстати, что бы вы ни выбрали: создание исключения или удаление метода, в конце концов, следующий код (предположим, что ваш язык поддерживает удаление метода):
сгенерирует исключение времени выполнения.
источник
Как отмечалось выше в комментариях, пингвины - это птицы, пингвины не летают, поэтому не все птицы могут летать.
Таким образом, Bird.fly () не должен существовать или иметь возможность не работать. Я предпочитаю первое.
Конечно, наличие FlyingBird расширяет Bird, и метод .fly () будет правильным.
источник
Реальная проблема с примером fly () заключается в том, что входные и выходные данные операции определены неправильно. Что требуется для полета птицы? А что происходит после удачного полета? Типы параметров и возвращаемые типы для функции fly () должны иметь эту информацию. В противном случае ваш дизайн зависит от случайных побочных эффектов, и все может случиться. Часть any - это то, что вызывает всю проблему, интерфейс не определен должным образом и разрешены все виды реализации.
Итак, вместо этого:
У вас должно быть что-то вроде этого:
Теперь он явно определяет пределы функциональности - ваше поведение в полете может быть определено только одним поплавком - расстояние от земли, если задано положение. Теперь вся проблема автоматически решается сама собой. Птица, которая не может летать, просто возвращает 0.0 из этой функции, она никогда не покидает землю. Это правильное поведение для этого, и как только будет решено, что один float, вы знаете, что полностью реализовали интерфейс.
Реальное поведение может быть трудно закодировать для типов, но это единственный способ правильно указать ваши интерфейсы.
Изменить: я хочу уточнить один аспект. Эта версия функции fly () float-> float важна еще и потому, что она определяет путь. Эта версия означает, что одна птица не может магически дублировать себя во время полета. Вот почему параметр «одиночный поплавок» - это позиция на пути, по которому идет птица. Если вы хотите более сложные пути, то Point2d posinpath (float x); который использует тот же x, что и функция fly ().
источник
Технически вы можете сделать это практически на любом языке с динамической / утиной типизацией (JavaScript, Ruby, Lua и т. Д.), Но это почти всегда очень плохая идея. Удаление методов из класса - это кошмар обслуживания, похожий на использование глобальных переменных (т. Е. Вы не можете сказать в одном модуле, что глобальное состояние не было изменено в другом месте).
Хорошие шаблоны для проблемы, которую вы описали - это Decorator или Strategy, проектирующие архитектуру компонентов По сути, вместо удаления ненужных поведений из подклассов, вы строите объекты, добавляя необходимые поведения. Поэтому для создания большинства птиц вы бы добавили летающий компонент, но не добавляйте этот компонент к своим пингвинам.
источник
Питер упомянул принцип замещения Лискова, но я чувствую, что это нужно объяснить.
Таким образом, если птица (объект x типа T) может летать (q (x)), то пингвин (объект y типа S) может летать (q (y)) по определению. Но это явно не тот случай. Есть и другие существа, которые могут летать, но не относятся к типу птиц.
Как вы справляетесь с этим, зависит от языка. Если язык поддерживает множественное наследование, вы должны использовать абстрактный класс для существ, которые могут летать; если язык предпочитает интерфейсы, то это решение (и реализация fly должна быть инкапсулирована, а не унаследована); или, если язык поддерживает Duck Typing (без каламбура), вы можете просто реализовать метод fly для тех классов, которые могут и вызывать его, если он есть.
Но каждое свойство суперкласса должно применяться ко всем его подклассам.
[В ответ на редактирование]
Применение «черты» CanFly к Bird не лучше. В коде вызова все еще предлагается, чтобы все птицы могли летать.
Особенность в терминах, которые вы определили, это именно то, что имел в виду Лисков, когда она сказала "собственность"
источник
Позвольте мне начать с упоминания (как и всех остальных) принципа замены Лискова, который объясняет, почему вы не должны этого делать. Однако вопрос о том, что вы должны сделать, это вопрос дизайна. В некоторых случаях это может быть не важно, что Пингвин не может летать. Возможно, вы можете сделать так, чтобы Penguin выдавал при запросе «полет» «Недостаточное количество крыльев», если в документации Bird :: fly () ясно, что это может быть сделано для птиц, которые не могут летать. Есть тест, чтобы увидеть, действительно ли он может летать, хотя это раздувает интерфейс.
Альтернатива - реструктурировать ваши классы. Давайте создадим класс «FlyingCreature» (или лучше интерфейс, если вы имеете дело с языком, который это позволяет). «Птица» не наследует от FlyingCreature, но вы можете создать «FlyingBird», который делает. Ларк, Стервятник и Орел все наследуют от FlyingBird. Пингвин нет. Это просто наследуется от птицы.
Это немного сложнее, чем наивная структура, но преимущество в том, что она точна. Вы заметите, что все ожидаемые классы есть (Bird), и пользователь обычно может игнорировать «изобретенные» классы (FlyingCreature), если не важно, может ли ваше существо летать или нет.
источник
Типичный способ справиться с такой ситуацией - создать что-то вроде
UnsupportedOperationException
(Java) соотв.NotImplementedException
(С #).источник
Много хороших ответов с большим количеством комментариев, но они не все согласны, и я могу выбрать только один, поэтому я суммирую здесь все мнения, с которыми я согласен.
0) Не предполагайте «статическую типизацию» (я сделал, когда я спросил, потому что я делаю Java почти исключительно). В основном, проблема очень зависит от типа языка, который вы используете.
1) Следует отделить иерархию типов от иерархии повторного использования кода в проекте и в голове, даже если они в основном пересекаются. Как правило, используйте классы для повторного использования и интерфейсы для типов.
2) Причина, по которой обычно Bird IS-A Fly, заключается в том, что большинство птиц может летать, поэтому с точки зрения повторного использования кода это практично, но говорить, что Bird IS-A Fly на самом деле неправильно, поскольку есть хотя бы одно исключение (Пингвин).
3) В статических и динамических языках вы можете просто сгенерировать исключение. Но это следует использовать только в том случае, если это явно объявлено в «контракте» класса / интерфейса, объявляющего функциональность, в противном случае это «нарушение контракта». Это также означает, что теперь вы должны быть готовы к тому, чтобы перехватывать исключения везде, поэтому вы пишете больше кода на сайте вызовов, и это ужасный код.
4) В некоторых динамических языках фактически возможно «удалить / скрыть» функциональность суперкласса. Если проверка на наличие функциональности является способом проверки «IS-A» на этом языке, то это адекватное и разумное решение. Если, с другой стороны, операция «IS-A» - это что-то еще, что все еще говорит, что ваш объект «должен» реализовать недостающую функциональность, тогда ваш вызывающий код будет предполагать, что эта функциональность присутствует, и будет вызывать ее и падать, поэтому это разные виды броска исключения.
5) Лучшая альтернатива - отделить черту Fly от черты Bird. Таким образом, летящая птица должна явно расширять / реализовывать как Bird, так и Fly / Flying. Это, наверное, самый чистый дизайн, так как вам не нужно ничего «удалять». Единственным недостатком является то, что почти каждая птица должна реализовывать как Bird, так и Fly, поэтому вы пишете больше кода. Чтобы обойти это, нужно иметь промежуточный класс FlyingBird, который реализует как Bird, так и Fly и представляет общий случай, но этот обходной путь может иметь ограниченное использование без множественного наследования.
6) Другой альтернативой, которая не требует множественного наследования, является использование композиции вместо наследования. Каждый аспект животного моделируется независимым классом, а конкретный Bird - это композиция Bird, и, возможно, Fly или Swim ... Вы получаете полное повторное использование кода, но вам нужно сделать один или несколько дополнительных шагов, чтобы добраться до Летающий функционал, когда у вас есть ссылка на конкретную птичку. Кроме того, естественный язык «объект IS-A Fly» и «объект AS-A (приведение) Fly» больше не будут работать, поэтому вам придется придумывать собственный синтаксис (некоторые динамические языки могут обойти это). Это может сделать ваш код более громоздким.
7) Определите свою черту Мухи так, чтобы она предлагала ясный выход для чего-то, что не может летать. Fly.getNumberOfWings () может вернуть 0. Если Fly.fly (direction, currentPotinion) должен вернуть новую позицию после полета, тогда Penguin.fly () может просто вернуть currentPosition, не изменяя его. Вы можете получить код, который технически работает, но есть некоторые предостережения. Во-первых, некоторый код может не иметь очевидного поведения «ничего не делать». Кроме того, если кто-то вызовет x.fly (), он будет ожидать, что он что-то сделает , даже если в комментарии сказано, что fly () разрешено ничего не делать . Наконец, пингвин IS-A Flying все равно вернет true, что может запутать программиста.
8) Делайте как 5), но используйте композицию, чтобы обойти случаи, которые требуют множественного наследования. Это вариант, который я бы предпочел для статического языка, так как 6) кажется более громоздким (и, вероятно, требует больше памяти, потому что у нас больше объектов). Динамический язык может сделать 6) менее громоздким, но я сомневаюсь, что он станет менее громоздким, чем 5).
источник
Определите поведение по умолчанию (пометить его как виртуальное) в базовом классе и переопределите его по необходимости. Таким образом, каждая птица может «летать».
Даже пингвины летают, скользя по льду на нулевой высоте!
Поведение полета может быть изменено по мере необходимости.
Другая возможность - иметь интерфейс Fly. Не все птицы будут реализовывать этот интерфейс.
Свойства не могут быть удалены, поэтому важно знать, какие свойства являются общими для всех птиц. Я думаю, что это больше проблема дизайна, чтобы убедиться, что общие свойства реализованы на базовом уровне.
источник
Я думаю, что шаблон, который вы ищете, это старый добрый полиморфизм. Хотя вы можете удалить интерфейс из класса на некоторых языках, это, вероятно, не очень хорошая идея по причинам, указанным Петером Тороком. Однако в любом языке ОО вы можете переопределить метод, чтобы изменить его поведение, и это включает в себя ничего не делать. Чтобы заимствовать ваш пример, вы можете предоставить метод Penguin :: fly (), который выполняет любое из следующих действий:
Свойства могут быть немного легче добавлять и удалять, если вы планируете заранее. Вы можете хранить свойства в карте / словаре / ассоциативном массиве вместо использования переменных экземпляра. Вы можете использовать шаблон Factory для создания стандартных экземпляров таких структур, поэтому Bird из BirdFactory всегда будет начинаться с одним и тем же набором свойств. Кодирование значения ключа Objective-C - хороший пример такого рода вещей.
Примечание . Серьезный урок из приведенных ниже комментариев заключается в том, что хотя переопределение для удаления поведения может работать, это не всегда лучшее решение. Если вы обнаружите, что вам нужно сделать это каким-либо существенным образом, вы должны считать, что это сильный сигнал о том, что ваш график наследования неверен. Не всегда возможно реорганизовать классы, от которых вы наследуете, но когда это так, зачастую это лучшее решение.
Используя ваш пример с Пингвином, одним из способов рефакторинга было бы отделение способности к полету от класса Bird. Поскольку не все птицы могут летать, в том числе метод fly () в Bird был неуместен и приводил непосредственно к той проблеме, о которой вы спрашиваете. Итак, переместите метод fly () (и, возможно, takeoff () и land ()) в класс Aviator или интерфейс (в зависимости от языка). Это позволяет вам создать класс FlyingBird, который наследуется от Bird и Aviator (или наследуется от Bird и реализует Aviator). Пингвин может продолжать наследовать напрямую от Птицы, но не от Авиатора, что позволяет избежать проблемы. Такое расположение может также облегчить создание классов для других летающих объектов: FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect и так далее.
источник