Jump to content

Задача круг-эллипс

(Перенаправлено из проблемы с кругом и эллипсом )

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

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

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

Описание

[ редактировать ]

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

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

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

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

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

[ редактировать ]

Решить проблему можно следующим образом:

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

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

Изменить модель

[ редактировать ]

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

[ редактировать ]

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

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

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

[ редактировать ]

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

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

[ редактировать ]

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

Circle::stretchX(x) { xSize = ySize = x; }
Circle::stretchY(y) { xSize = ySize = y; }

Превратите круг в эллипс

[ редактировать ]

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

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

[ редактировать ]

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

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

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

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

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

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

[ редактировать ]

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

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

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

[ редактировать ]

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

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

[ редактировать ]

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

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

[ редактировать ]

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

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

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

[ редактировать ]

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

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

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

[ редактировать ]

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

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

[ редактировать ]

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

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

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

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

(defgeneric check-constraints (shape))

;; The accessors on shape objects. Constraints on objects
;; need to be checked after either axis value is set.
(defgeneric h-axis (shape))
(defgeneric (setf h-axis) (new-value shape)
  (:method :after (new-value shape) (check-constraints shape)))
(defgeneric v-axis (shape))
(defgeneric (setf v-axis) (new-value shape)
  (:method :after (new-value shape) (check-constraints shape)))

(defclass ellipse ()
  ((h-axis :type real :accessor h-axis :initarg :h-axis)
   (v-axis :type real :accessor v-axis :initarg :v-axis)))

(defclass circle (ellipse)
  ((radius :type real :accessor radius :initarg :radius)))

;;;
;;; A circle has a radius, but also a h-axis and v-axis that
;;; it inherits from an ellipse. These must be kept in sync
;;; with the radius when the object is initialized and
;;; when those values change.
;;;
(defmethod initialize-instance :after ((c circle) &key radius)
  (setf (radius c) radius)) ;; via the setf method below

(defmethod (setf radius) :after ((new-value real) (c circle))
  ;; We use SLOT-VALUE, rather than the accessors, to avoid changing
  ;; class unnecessarily between the two assignments; as the circle
  ;; will have different h-axis and v-axis values between the
  ;; assignments, and then the same values after assignments.
  (setf (slot-value c 'h-axis) new-value
        (slot-value c 'v-axis) new-value))

;;;
;;; After an assignment is made to the circle's
;;; h-axis or v-axis, a change of type is necessary,
;;; unless the new value is the same as the radius.
;;;

(defmethod check-constraints ((c circle))
  (unless (= (radius c) (h-axis c) (v-axis c))
    (change-class c 'ellipse)))

;;;
;;; Ellipse changes to a circle if accessors
;;; mutate it such that the axes are equal,
;;; or if an attempt is made to construct it that way.
;;;
(defmethod initialize-instance :after ((e ellipse) &key)
  (check-constraints e))

(defmethod check-constraints ((e ellipse))
  (when (= (h-axis e) (v-axis e))
    (change-class e 'circle)))
;;;
;;; Method for an ellipse becoming a circle. In this metamorphosis,
;;; the object acquires a radius, which must be initialized.
;;; There is a "sanity check" here to signal an error if an attempt
;;; is made to convert an ellipse which axes are unequal
;;; with an explicit change-class call.
;;; The handling strategy here is to base the radius off the
;;; h-axis and signal an error.
;;; This doesn't prevent the class change; the damage is already done.
;;;
(defmethod update-instance-for-different-class :after ((old-e ellipse)
                                                       (new-c circle) &key)
  (setf (radius new-c) (h-axis old-e))
  (unless (= (h-axis old-e) (v-axis old-e))
    (error "ellipse ~s can't change into a circle because it's not one!"
           old-e)))

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

$ clisp -q -i circle-ellipse.lisp 
[1]> (make-instance 'ellipse :v-axis 3 :h-axis 3)
#<CIRCLE #x218AB566>
[2]> (make-instance 'ellipse :v-axis 3 :h-axis 4)
#<ELLIPSE #x218BF56E>
[3]> (defvar obj (make-instance 'ellipse :v-axis 3 :h-axis 4))
OBJ
[4]> (class-of obj)
#<STANDARD-CLASS ELLIPSE>
[5]> (radius obj)

*** - NO-APPLICABLE-METHOD: When calling #<STANDARD-GENERIC-FUNCTION RADIUS>
      with arguments (#<ELLIPSE #x2188C5F6>), no method is applicable.
The following restarts are available:
RETRY          :R1      try calling RADIUS again
RETURN         :R2      specify return values
ABORT          :R3      Abort main loop
Break 1 [6]> :a
[7]> (setf (v-axis obj) 4)
4
[8]> (radius obj)
4
[9]> (class-of obj)
#<STANDARD-CLASS CIRCLE>
[10]> (setf (radius obj) 9)
9
[11]> (v-axis obj)
9
[12]> (h-axis obj)
9
[13]> (setf (h-axis obj) 8)
8
[14]> (class-of obj)
#<STANDARD-CLASS ELLIPSE>
[15]> (radius obj)

*** - NO-APPLICABLE-METHOD: When calling #<STANDARD-GENERIC-FUNCTION RADIUS>
      with arguments (#<ELLIPSE #x2188C5F6>), no method is applicable.
The following restarts are available:
RETRY          :R1      try calling RADIUS again
RETURN         :R2      specify return values
ABORT          :R3      Abort main loop
Break 1 [16]> :a
[17]>

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

[ редактировать ]

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

class Person
{
    void walkNorth(int meters) {...}
    void walkEast(int meters) {...}
}

Итак, заключенный, очевидно, человек. Итак, логически можно создать подкласс:

class Prisoner extends Person
{
    void walkNorth(int meters) {...}
    void walkEast(int meters) {...}
}

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

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

По аналогии, круг не является эллипсом, потому что ему не хватает тех же степеней свободы, что и эллипсу.

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

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

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

Инвариантность

[ редактировать ]

Концептуально можно рассмотреть Круг и Эллипс должен быть изменяемым типом контейнеров, псевдонимами MutableContainer<ImmutableCircle> и MutableContainer<ImmutableEllipse> соответственно. В этом случае, ImmutableCircle можно считать подтипом ИммутаблеЭллипс . Тип Т в MutableContainer<T> может быть как записан, так и прочитан, подразумевая, что он не является ни ковариантным, ни контравариантным, а вместо этого является инвариантным. Поэтому, Круг не является подтипом Эллипс , и наоборот.

  1. ^ Казимир Майоринц, Дилемма эллипса-круга и обратное наследование, ITI 98, Материалы 20-й Международной конференции по интерфейсам информационных технологий, Пула, 1998 г.
[ редактировать ]
Arc.Ask3.Ru: конец переведенного документа.
Arc.Ask3.Ru
Номер скриншота №: 8bd8396b0e3e0b7551faebd2974c00b7__1689460860
URL1:https://arc.ask3.ru/arc/aa/8b/b7/8bd8396b0e3e0b7551faebd2974c00b7.html
Заголовок, (Title) документа по адресу, URL1:
Circle–ellipse problem - Wikipedia
Данный printscreen веб страницы (снимок веб страницы, скриншот веб страницы), визуально-программная копия документа расположенного по адресу URL1 и сохраненная в файл, имеет: квалифицированную, усовершенствованную (подтверждены: метки времени, валидность сертификата), открепленную ЭЦП (приложена к данному файлу), что может быть использовано для подтверждения содержания и факта существования документа в этот момент времени. Права на данный скриншот принадлежат администрации Ask3.ru, использование в качестве доказательства только с письменного разрешения правообладателя скриншота. Администрация Ask3.ru не несет ответственности за информацию размещенную на данном скриншоте. Права на прочие зарегистрированные элементы любого права, изображенные на снимках принадлежат их владельцам. Качество перевода предоставляется как есть. Любые претензии, иски не могут быть предъявлены. Если вы не согласны с любым пунктом перечисленным выше, вы не можете использовать данный сайт и информация размещенную на нем (сайте/странице), немедленно покиньте данный сайт. В случае нарушения любого пункта перечисленного выше, штраф 55! (Пятьдесят пять факториал, Денежную единицу (имеющую самостоятельную стоимость) можете выбрать самостоятельно, выплаичвается товарами в течение 7 дней с момента нарушения.)