Задача круг – эллипс - Circle–ellipse problem

В задача круг – эллипс в разработка программного обеспечения (иногда называют квадрат – прямоугольник) иллюстрирует несколько ошибок, которые могут возникнуть при использовании полиморфизм подтипа в объектное моделирование. Проблемы чаще всего встречаются при использовании объектно-ориентированного программирования (ООП). По определению, эта проблема является нарушением Принцип подстановки Лискова, один из ТВЕРДЫЙ принципы.

Проблема касается подтипа или наследование отношения должны существовать между классы которые представляют круги и эллипсы (или аналогично квадраты и прямоугольники ). В более общем плане проблема иллюстрирует трудности, которые могут возникнуть, когда базовый класс содержит методы которые изменяют объект способом, который может сделать недействительным (более сильный) инвариант, найденный в производном классе, что приведет к нарушению принципа подстановки Лискова.

Существование проблемы круг-эллипс иногда используется для критики объектно-ориентированного программирования. Это также может означать, что иерархические таксономии трудно сделать универсальными, а это означает, что системы ситуационной классификации могут быть более практичными.

Описание

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

Подкласс должен обеспечивать поддержку всего поведения, поддерживаемого суперклассом; подклассы должны реализовывать любые методы мутатора определен в базовом классе. В данном случае метод Ellipse.stretchX изменяет длину одной из своих осей на месте. Если Круг наследуется от Эллипс, у него также должен быть метод stretchX, но результатом этого метода будет преобразование круга во что-то, что больше не является кругом. В Круг класс не может одновременно удовлетворять собственному инварианту и поведенческим требованиям Ellipse.stretchX метод.

Связанная проблема с этим наследованием возникает при рассмотрении реализации. Эллипс требует описания большего числа состояний, чем круг, потому что для первого нужны атрибуты, определяющие длину и вращение большой и малой осей, тогда как кругу нужен только радиус. Этого можно избежать, если язык (например, Эйфель ) делает постоянные значения класса, функции без аргументов и члены данных взаимозаменяемыми.

Некоторые авторы предлагают изменить отношения между кругом и эллипсом на том основании, что эллипс - это круг с большими возможностями. К сожалению, эллипсы не удовлетворяют многим инвариантам окружностей; если Круг имеет метод радиус, Эллипс теперь тоже должен предоставить это.

Возможные решения

Решить проблему можно:

  • изменение модели
  • с использованием другого языка (или существующего или написанного на заказ расширения какого-либо существующего языка)
  • используя другую парадигму

Какой именно вариант подходит, будет зависеть от того, кто написал Круг и кто написал Эллипс. Если один и тот же автор разрабатывает их оба с нуля, то автор сможет определить интерфейс для обработки этой ситуации. Если Эллипс объект уже был написан и не может быть изменен, тогда возможности более ограничены.

Сменить модель

Возвращает успешное или неудачное значение

Разрешить объектам возвращать значение «успех» или «неудача» для каждого модификатора или поднимать исключение при неудаче. Обычно это делается в случае файлового ввода-вывода, но и здесь может быть полезно. Сейчас же, Ellipse.stretchX работает и возвращает "истину", а Circle.stretchX просто возвращает «ложь». В целом это хорошая практика, но может потребоваться, чтобы первоначальный автор Эллипс предвидел такую ​​проблему и определил мутаторы как возвращающие значение. Кроме того, он требует, чтобы клиентский код проверял возвращаемое значение для поддержки функции растяжения, что фактически похоже на проверку того, является ли объект, на который ссылается, кругом или эллипсом. Другой способ взглянуть на это - это как заключить контракт, согласно которому контракт может или не может быть выполнен в зависимости от объекта, реализующего интерфейс. В конце концов, это всего лишь умный способ обойти ограничение Лискова, заранее заявив, что условие публикации может быть или не быть действительным.

Альтернативно, Circle.stretchX может вызвать исключение (но в зависимости от языка для этого может также потребоваться, чтобы исходный автор Эллипс объявить, что он может вызвать исключение).

Вернуть новое значение X

Это решение, аналогичное описанному выше, но немного более мощное. Ellipse.stretchX теперь возвращает новое значение своего измерения X. Сейчас же, Circle.stretchX может просто вернуть свой текущий радиус. Все модификации должны производиться через Circle.stretch, сохраняющий инвариант окружности.

Разрешить более слабый контракт на Ellipse

Если контракт интерфейса для Эллипс заявляет только, что «stretchX изменяет ось X», и не заявляет «и больше ничего не изменится», тогда Круг может просто заставить размеры X и Y быть одинаковыми. Circle.stretchX и Circle.stretchY оба изменяют размер X и Y.

Круг :: stretchX (x) {xSize = ySize = x; } Circle :: stretchY (y) {xSize = ySize = y; }

Преобразуйте круг в эллипс

Если Circle.stretchX называется, то Круг превращается в Эллипс. Например, в Common Lisp, это можно сделать с помощью СМЕНА-КЛАСС метод. Однако это может быть опасно, если какая-то другая функция ожидает, что это будет Круг. Некоторые языки исключают этот тип изменений, а другие налагают ограничения на Эллипс класс быть приемлемой заменой Круг. Для языков, допускающих неявное преобразование, например C ++, это может быть только частичное решение проблемы при вызове по копии, но не при вызове по ссылке.

Сделайте все экземпляры постоянными

Можно изменить модель так, чтобы экземпляры классов представляли постоянные значения (т.е. неизменный ). Это реализация, которая используется в чисто функциональном программировании.

В этом случае такие методы, как stretchX должны быть изменены для получения нового экземпляра, а не для изменения экземпляра, на который они действуют. Это означает, что больше не проблема определить Circle.stretchX, а наследование отражает математические отношения между кругами и эллипсами.

Недостатком является то, что для изменения значения экземпляра требуется назначение, что неудобно и подвержено ошибкам программирования, например,

Орбита (планета [i]): = Орбита (планета [i]). StretchX

Второй недостаток заключается в том, что такое назначение концептуально включает временное значение, которое может снизить производительность и его будет сложно оптимизировать.

Вынесение модификаторов

Можно определить новый класс MutableEllipse, и поместите модификаторы из Эллипс в этом. В Круг наследует только запросы от Эллипс.

Недостатком этого является введение дополнительного класса, в котором все, что требуется, - это указать, что Круг не наследует модификаторы от Эллипс.

Наложение предварительных условий на модификаторы

Можно указать, что Ellipse.stretchX допускается только в случаях, удовлетворяющих Эллипс. Растягиваемый, а в противном случае вызовет исключение. Это требует предвидения проблемы при определении Ellipse.

Вынесите общие функциональные возможности в абстрактный базовый класс

Создайте абстрактный базовый класс с именем ЭллипсОрКруг и поместите методы, которые работают с обоими Кругпесок Эллипсs в этом классе. Функции, которые могут работать с любым типом объекта, будут ожидать ЭллипсОрКруг, и функции, использующие Эллипс- или же Круг-специфические требования будут использовать классы-потомки. Тем не мение, Круг тогда больше не Эллипс подкласс, ведущий к "a Круг это не что-то вроде Эллипс"ситуация, описанная выше.

Отбросить все отношения наследования

Это мгновенно решает проблему. Любые общие операции, необходимые как для Circle, так и для Ellipse, можно абстрагировать в общий интерфейс, который реализует каждый класс, или в миксины.

Также можно предоставить такие методы преобразования, как Circle.asEllipse, который возвращает изменяемый объект Ellipse, инициализированный с использованием радиуса круга. С этого момента это отдельный объект, и его можно без проблем изменять отдельно от исходного круга. Методы, преобразующие другой путь, не обязательно связаны с одной стратегией. Например, могут быть оба Ellipse.minimalEnclosingCircle и Ellipse.maximalEnclosedCircle, и любая другая желаемая стратегия.

Объедините класс Circle в класс Ellipse

Затем, везде, где раньше использовался круг, используйте эллипс.

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

Обратное наследование

Majorinc предложил модель, которая разделяет методы на модификаторы, селекторы и общие методы. Только селекторы могут быть автоматически унаследованы от суперкласса, а модификаторы должны быть унаследованы от подкласса к суперклассу. В общем случае методы должны быть явно унаследованы. Модель может быть эмулирована на языках с множественное наследование, с помощью абстрактные классы.[1]

Сменить язык программирования

Эта проблема имеет простые решения в достаточно мощной системе объектно-ориентированного программирования. По сути, задача круг-эллипс - это синхронизация двух представлений типа: де-факто тип, основанный на свойствах объекта, и формальный тип, связанный с объектом системой объектов. Если эти две части информации, которые в конечном итоге являются всего лишь битами в машине, синхронизированы так, что они говорят одно и то же, все в порядке. Ясно, что круг не может удовлетворять требуемым инвариантам, в то время как его методы базового эллипса допускают изменение параметров. Однако существует вероятность того, что, когда круг не может соответствовать инвариантам круга, его тип можно обновить, чтобы он стал эллипсом. Если круг, ставший де-факто эллипс не меняет тип, тогда его тип - это часть информации, которая теперь устарела, отражая историю объекта (как он когда-то был построен), а не его нынешнюю реальность (во что он с тех пор мутировал).

Многие широко используемые объектные системы основаны на дизайне, который считает само собой разумеющимся, что объект имеет один и тот же тип на протяжении всего своего жизненного цикла, от создания до завершения. Это ограничение не для ООП, а только для конкретных реализаций.

В следующем примере используется Общая объектная система Lisp (CLOS), в котором объекты могут изменять класс без потери своей идентичности. Все переменные или другие места хранения, которые содержат ссылку на объект, продолжают содержать ссылку на тот же объект после того, как он изменит класс.

Модели круга и эллипса намеренно упрощены, чтобы избежать отвлекающих деталей, которые не имеют отношения к задаче круг-эллипс. Эллипс имеет две полуоси, называемые ось h и ось v в коде. Круг, будучи эллипсом, наследует их, а также имеет радиус свойство, значение которого равно значению осей (которое, конечно, должно быть равным).

(defclass эллипс ()  ((ось h :тип настоящий : аксессуар ось h : initarg : ось h)   (ось v :тип настоящий : аксессуар ось v : initarg : ось v)))(defclass круг (эллипс)  ((радиус :тип настоящий : аксессуар радиус : initarg :радиус)));;;;;; У круга есть радиус, но также есть оси h и v, которые;;; он наследуется от эллипса. Их нужно синхронизировать;;; с радиусом при инициализации объекта и;;; когда эти значения меняются.;;;(defmethod инициализировать экземпляр ((c круг) &ключ радиус)  (setf (радиус c) радиус)) ;; с помощью метода setf ниже(defmethod (setf радиус) :после ((новое значение настоящий) (c круг))  (setf (значение слота c 'ось h) новое значение        (значение слота c 'ось v) новое значение));;;;;; После выполнения задания кружку;;; ось h или ось v, изменение типа необходимо,;;; если новое значение не совпадает с радиусом.;;;(defmethod (setf ось h) :после ((новое значение настоящий) (c круг))  (пока не (= (радиус c) новое значение)    (класс изменения c 'эллипс)))(defmethod (setf ось v) :после ((новое значение настоящий) (c круг))  (пока не (= (радиус c) новое значение)    (класс изменения c 'эллипс)));;;;;; Эллипс превращается в круг, если аксессоры;;; измените его так, чтобы оси были равны,;;; или если сделана попытка построить его таким образом.;;;;;; Используется равенство EQL, при котором 0 / = 0,0.;;;;;;(defmethod инициализировать экземпляр :после ((е эллипс) &ключ ось h ось v)  (если (= ось h ось v)    (класс изменения е 'круг)))(defmethod (setf ось h) :после ((новое значение настоящий) (е эллипс))  (пока не (typep е 'круг)    (если (= (ось h е) (ось v е))      (класс изменения е 'круг))))(defmethod (setf ось v) :после ((новое значение настоящий) (е эллипс))  (пока не (typep е 'круг)    (если (= (ось h е) (ось v е))      (класс изменения е 'круг))));;;;;; Метод превращения эллипса в круг. В этой метаморфозе;;; объект приобретает радиус, который необходимо инициализировать.;;; Здесь есть «проверка работоспособности», чтобы сигнализировать об ошибке, если попытка;;; сделан для преобразования эллипса, оси которого не равны;;; с явным вызовом класса изменения.;;; Стратегия обработки здесь - основывать радиус на;;; ось h и сигнализирует об ошибке.;;; Это не предотвращает смену класса; ущерб уже нанесен.;;;(defmethod обновление-экземпляр-для-другого-класса :после ((старый-е эллипс)                                                       (new-c круг) &ключ)  (setf (радиус new-c) (ось h старый-е))  (пока не (= (ось h старый-е) (ось v старый-е))    (ошибка "эллипсы не могут превратиться в круг, потому что это не один!"           старый-е)))

Этот код можно продемонстрировать в интерактивном сеансе с использованием CLISP-реализации Common Lisp.

$ clisp -q -i круг-эллипс.lisp [1]> (make-instance 'эллипс: ось v 3: ось h 3)#<КРУГ # x218AB566>[2]> (make-instance 'эллипс: ось v 3: ось h 4)#<ЭЛЛИПС # x218BF56E>[3]> (defvar obj (make-instance 'ellipse: v-axis 3: h-axis 4))OBJ[4]> (класс объекта)#<STANDARD-CLASS ELLIPSE>[5]> (радиус obj)*** - НЕПРИМЕНИМЫЙ-МЕТОД: при вызове # <СТАНДАРТНЫЙ-ОБЩИЙ-ФУНКЦИОНАЛЬНЫЙ РАДИУС>      с аргументами (# ) никакой метод не применим.Доступны следующие перезапуски:ПОВТОР: R1 попробуйте снова позвонить в RADIUSВОЗВРАТ: R2 указывает возвращаемые значенияABORT: R3 Прервать основной циклПерерыв 1 [6]>: a[7]> (setf (v-axis obj) 4)4[8]> (радиус obj)4[9]> (класс объекта)#<STANDARD-CLASS CIRCLE>[10]> (setf (радиус obj) 9)9[11]> (ось vj)9[12]> (ось h obj)9[13]> (setf (ось h obj) 8)8[14]> (класс объекта)#<STANDARD-CLASS ELLIPSE>[15]> (радиус obj)*** - НЕПРИМЕНИМО-МЕТОД: при вызове # <СТАНДАРТНЫЙ-ОБЩИЙ-ФУНКЦИОНАЛЬНЫЙ РАДИУС>      с аргументами (# ) никакой метод не применим.Доступны следующие перезапуски:ПОВТОР: R1 попробуйте снова позвонить в RADIUSВОЗВРАТ: R2 указывает возвращаемые значенияABORT: R3 Прервать основной циклПерерыв 1 [16]>: a[17]>

Бросьте вызов посылке проблемы

Хотя на первый взгляд может показаться очевидным, что Круг является Эллипс, рассмотрим следующий аналогичный код.

учебный класс Человек{    пустота прогулка на север(int метры) {...}    пустота прогулка(int метры) {...}}

Очевидно, что заключенный - это человек. Таким образом, можно создать подкласс:

учебный класс Заключенный расширяет Человек{    пустота прогулка на север(int метры) {...}    пустота прогулка(int метры) {...}}

Также очевидно, что это приводит к неприятностям, поскольку заключенный нет свободно перемещаться на произвольное расстояние в любом направлении, но контракт Человек класс заявляет, что человек может.

Таким образом, класс Человек можно было бы лучше назвать FreePerson. Если бы это было так, то идея, что class Prisoner расширяет FreePerson явно не так.

По аналогии, тогда Круг - это нет эллипс, потому что у него отсутствуют те же степени свободы, что и у эллипса.

Таким образом, применяя лучшее именование, вместо этого можно было бы назвать Circle ОдинДиаметрФигура и эллипс можно назвать ДваДиаметрФигура. С такими именами теперь более очевидно, что ДваДиаметрФигура следует продлить ОдинДиаметрФигура, поскольку добавляет к нему еще одно свойство; в то время как ОдинДиаметрФигура имеет свойство единственного диаметра, ДваДиаметрФигура имеет два таких свойства (т.е. длину большой и малой оси).

Это настоятельно предполагает, что наследование никогда не должно использоваться, когда подкласс ограничивает свободу, неявную в базовом классе, но должно использоваться только тогда, когда подкласс добавляет дополнительные детали к концепции, представленной базовым классом, как в 'Monkey'. -животное'.

Однако утверждение, что заключенный не может перемещаться на произвольное расстояние в любом направлении, а человек может - это еще раз неверная посылка. Любой объект, движущийся в любом направлении, может столкнуться с препятствиями. Правильный способ смоделировать эту проблему - иметь WalkAttemptResult walkToDirection (целые метры, направление направления) договор. Теперь при реализации walkToDirection для подкласса Prisoner вы можете проверить границы и вернуть правильные результаты ходьбы.

Рекомендации

  1. ^ Казимир Майоринк, Дилемма эллипса-круга и обратное наследование, ITI 98, Материалы 20-й Международной конференции интерфейсов информационных технологий, Пула, 1998 г.

внешняя ссылка

  • https://web.archive.org/web/20150409211739/http://www.parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.6 Популярный C ++ Сайт FAQ по Маршалл Клайн. Заявляет и объясняет проблему.
  • Конструктивная деконструкция подтипов к Алистер Кокберн на собственном сайте. Техническое / математическое обсуждение набора текста и выделения подтипов с приложениями к этой проблеме.
  • Хенни, Кевлин (2003-04-15). «От механизма к методу: полный эллипс». Доктора Добба.
  • http://orafaq.com/usenet/comp.databases.theory/2001/10/01/0001.htm Начало длинной нити (следуйте Может ответить: ссылки) в FAQ по Oracle, где обсуждается проблема. Относится к произведениям Си Джей Дэйта. Некоторая предвзятость к Болтовня.
  • ЛисковЗаменаПринцип в WikiWikiWeb
  • Подтипирование, создание подклассов и проблемы с ООП, эссе, в котором обсуждается родственная проблема: должны ли наборы наследоваться от пакетов?
  • Подтипирование по ограничениям в объектно-ориентированных базах данных, эссе, в котором обсуждается расширенная версия проблемы круга-эллипса в среде объектно-ориентированных баз данных.