원-타원 문제
Circle–ellipse problem![]() |
소프트웨어 개발의 원-타원 문제(사각형-직각 문제라고도 함)는 객체 모델링에서 하위 유형 다형성을 사용할 때 발생할 수 있는 몇 가지 함정을 보여준다.이 문제는 객체 지향 프로그래밍(OOP)을 사용할 때 가장 일반적으로 발생합니다.정의상 이 문제는 SOLID 원칙의 하나인 Liskov 대체 원칙을 위반하는 것입니다.
이 문제는 원과 타원(또는 비슷하게 정사각형과 직사각형)을 나타내는 클래스 사이에 어떤 하위 유형 또는 상속 관계가 존재해야 하는지에 관한 것입니다.보다 일반적으로 이 문제는 파생 클래스에서 발견된 (더 강한) 불변성을 무효화하고 Liskov 대체 원칙을 위반하는 방식으로 객체를 변환하는 메서드가 기본 클래스에 포함되어 있을 때 발생할 수 있는 어려움을 보여줍니다.
원-타원 문제의 존재는 때때로 객체 지향 프로그래밍을 비판하기 위해 사용됩니다.이는 또한 계층 분류법이 보편화되기 어렵다는 것을 의미할 수 있으며, 상황 분류 시스템이 더 실용적일 수 있음을 암시할 수 있다.
묘사
대부분의 객체 지향 언어에서 상속을 통해 구현되는 서브타입 다형성은 서로의 서브셋인 객체 유형을 모델링하기 위해 사용되어야 한다는 것이 객체 지향 분석과 설계의 중심 신조입니다. 이것은 일반적으로 is-a 관계라고 불립니다.이 예에서 원의 집합은 타원 집합의 하위 집합입니다. 원은 장축과 단축의 길이가 같은 타원 집합으로 정의할 수 있습니다.따라서, 모델 형상이 만드는 객체 지향 언어로 작성된 코드는 종종 만들기 위해 선택될 것입니다.클래스 타원 클래스의 하위 클래스(즉, 클래스로부터 상속됨)에 원을 그립니다.
서브클래스는 슈퍼클래스에서 지원되는 모든 동작을 지원해야 합니다.서브클래스는 기본 클래스에 정의된 모든 뮤테이터 메서드를 구현해야 합니다.이 경우, Ellipse.stretchX 메서드는 해당 축 중 하나의 길이를 변경합니다.만약 원이 타원으로부터 상속된다면, 그것은 또한 스트레치X 메서드를 가져야 하지만, 이 메서드의 결과로 원이 더 이상 원이 아닌 것으로 바뀌게 됩니다.Circle 클래스는 자체 불변성과 Ellipse.stretchX 메서드의 동작 요구 사항을 동시에 충족할 수 없습니다.
이 상속과 관련된 문제는 구현을 검토할 때 발생합니다.타원은 주축과 부축의 길이와 회전을 지정하는 속성이 필요한 반면, 원은 반지름만 필요하기 때문에 타원은 원보다 더 많은 상태를 설명해야 합니다.언어(Eiffel 등)가 클래스의 상수 값을 만들고 인수 없이 함수를 수행하며 데이터 구성원을 교환할 수 있는 경우 이를 피할 수 있습니다.
일부 저자들은 타원이 더 많은 능력을 가진 원이라는 이유로 원과 타원의 관계를 뒤집는 것을 제안했다.안타깝게도 타원은 원의 불변성을 대부분 만족시키지 못합니다. 원의 메서드 반지름이 있으면 타원도 이를 제공해야 합니다.
생각할 수 있는 해결책
다음과 같은 방법으로 문제를 해결할 수 있습니다.
- 모델 변경
- 다른 언어 사용(또는 기존 언어의 기존 또는 사용자 정의 확장)
- 다른 패러다임을 사용하여
정확히 어떤 옵션이 적절한지는 누가 Circle을 썼고 누가 Ellipse를 썼는지에 따라 달라집니다.같은 작성자가 양쪽 모두를 처음부터 설계하고 있는 경우, 작성자는 이 상황을 처리하기 위한 인터페이스를 정의할 수 있습니다.Ellipse 객체가 이미 작성되어 있어 변경할 수 없는 경우에는 옵션이 더 제한됩니다.
모델을 변경하다
성공 또는 실패 값을 반환합니다.
개체가 각 수정자에 대해 "성공" 또는 "실패" 값을 반환하도록 허용하거나 실패 시 예외를 발생시킵니다.이 작업은 일반적으로 파일 I/O의 경우 수행되지만 여기서도 도움이 됩니다.이제 Ellipse.stretchX는 작동하고 "true"를 반환하며, Circle.stretchX는 단순히 "false"를 반환합니다.이것은 일반적인 모범 사례이지만, Ellipse의 원래 작성자가 이러한 문제를 예측하고 변이자를 값을 반환하는 것으로 정의해야 할 수도 있습니다.또한 스트레치 함수를 지원하기 위해 클라이언트 코드가 반환값을 테스트해야 합니다.이것은 사실상 참조 객체가 원인지 타원인지 테스트하는 것과 같습니다.또 다른 관점에서 보면 인터페이스를 구현하는 오브젝트에 따라 계약이 이행될 수도 있고 이행되지 않을 수도 있는 계약을 체결하는 것과 같습니다.최종적으로 포스트 조건이 유효한지 아닌지를 사전에 기술함으로써 Liskov 제약을 우회하는 것은 현명한 방법입니다.
또는 Circle.stretchX에서 예외를 발생시킬 수도 있습니다(단, 언어에 따라서는 Ellipse의 원래 작성자가 예외를 발생시킬 수 있다고 선언해야 할 수도 있습니다).
X의 새 값을 반환합니다.
이는 위와 유사한 솔루션이지만 약간 더 강력합니다.이제 Ellipse.stretchX는 X 치수의 새 값을 반환합니다.이제 Circle.stretchX는 현재 반지름을 반환하기만 하면 됩니다.모든 변경은 원의 불변성을 유지하는 Circle.stretch를 통해 수행해야 합니다.
Ellipse에서 더 약한 계약 허용
Ellipse의 인터페이스 계약에서 "StretchX가 X축을 수정한다"고만 명시하고 "다른 아무것도 변경되지 않는다"고 명시하지 않은 경우, Circle은 단순히 X와 Y 치수를 동일하게 할 수 있습니다.Circle.stretchX와 Circle.stretchY는 둘 다 X와 Y 크기를 변경합니다.
원::stretchX(x) { xSize = ySize = x;} 원::stretchY(y) { xSize = y;}
원을 타원으로 변환
Circle.stretchX가 호출되면 Circle은 타원형으로 바뀝니다.예를 들어 Common Lisp에서는 CHANGE-CLASS 메서드를 사용하여 실행할 수 있습니다.그러나 다른 기능에서 원이라고 예상하는 경우에는 위험할 수 있습니다.일부 언어에서는 이러한 유형의 변경을 금지하고 있으며, 다른 언어에서는 타원 클래스가 Circle을 대체할 수 있도록 제한하고 있습니다.C++와 같이 암묵적인 변환을 가능하게 하는 언어의 경우, 이것은 부분적인 해결책일 뿐, 콜 바이 카피로 문제를 해결하는 것은 아닙니다.
모든 인스턴스를 일정하게 설정
클래스의 인스턴스가 상수 값을 나타내도록(즉, 불변) 모델을 변경할 수 있습니다.이는 순수하게 기능적인 프로그래밍에서 사용되는 구현입니다.
이 경우 stretchX 등의 메서드는 새로운 인스턴스를 생성하기 위해 변경할 필요가 있습니다.이러한 메서드가 동작하는 인스턴스를 변경하는 것이 아닙니다.즉, Circle.stretchX를 정의하는 것은 더 이상 문제가 되지 않으며 상속은 원과 타원 사이의 수학적 관계를 반영합니다.
단점은 인스턴스 값을 변경하면 할당이 필요하기 때문에 불편하고 프로그래밍 오류가 발생하기 쉽습니다.
궤도(행성[i]):= 궤도(행성[i]).스트레치X
두 번째 단점은 그러한 할당이 개념적으로 일시적인 가치를 수반한다는 것인데, 이는 성과를 저하시켜 최적화가 어려울 수 있다.
수식자를 인수 제외하다
새로운 클래스 MutableEllipse를 정의하고 그 안에 Ellipse의 수식어를 넣을 수 있습니다.원은 타원에서만 쿼리를 상속합니다.
이로 인해 추가 클래스가 도입되는 단점이 있는데, 여기서 원하는 것은 원이 타원으로부터 수식자를 상속하지 않도록 지정하는 것입니다.
수식자에 전제 조건 적용
Ellipse.stretchX가 Ellipse.stretchable을 만족하는 인스턴스에서만 허용되도록 지정할 수 있으며 그렇지 않으면 예외가 발생합니다.이를 위해서는 타원이 정의될 때 문제가 발생할 것으로 예상해야 합니다.
공통 기능을 추상 기본 클래스로 인수 배제
EllipseOrCircle이라는 추상 기본 클래스를 만들고 이 클래스에 원과 타원 둘 다와 함께 작동하는 메서드를 배치합니다.오브젝트 유형 중 하나를 처리할 수 있는 함수는 EllipseOrCircle을 필요로 하며, Ellipse 또는 Circle 고유의 요건을 사용하는 함수는 하위 클래스를 사용합니다.그러나 원이 더 이상 타원 하위 클래스가 아니므로 위에서 설명한 "원은 타원 종류가 아닙니다" 상황이 발생합니다.
모든 상속 관계 삭제
이것으로 그 문제는 일거에 해결된다.Circle과 Ellipse 모두에 대해 필요한 모든 공통 연산은 각 클래스가 구현하는 공통 인터페이스 또는 mixin으로 추상화할 수 있습니다.
또한 원의 반지름을 사용하여 초기화된 가변 타원 객체를 반환하는 Circle.asElipse와 같은 변환 메서드를 제공할 수도 있습니다.그 시점부터, 이것은 별개의 오브젝트이며, 문제없이 원래의 서클과 별개로 변형이 가능합니다.다른 방법을 변환하는 방법은 하나의 전략에 전념할 필요가 없습니다.예를 들어 Ellipse.minimalEnclosingCircle과 Ellipse.maximalEnclosedCircle을 모두 사용할 수 있습니다.또, 그 외의 필요한 전략도 있습니다.
클래스 원을 클래스 타원에 결합
그런 다음, 이전에 원이 사용되었던 곳에 타원을 사용합니다.
원은 이미 타원으로 나타낼 수 있습니다.타원에 적용할 수 없는 서클 고유의 메서드가 필요하거나 프로그래머가 서클의 단순한 모델의 개념적 또는 성능적 이점을 얻고자 하지 않는 한 클래스 서클을 가질 이유가 없습니다.
역상속
Majorinc는 수식자, 셀렉터, 일반 방법에 대한 방법을 나누는 모델을 제안했다.선택자만 슈퍼클래스에서 자동으로 상속할 수 있으며 한정자는 서브클래스에서 슈퍼클래스로 상속해야 합니다.일반적으로 메서드는 명시적으로 상속되어야 합니다.모델은 추상 [1]클래스를 사용하여 여러 상속이 있는 언어로 에뮬레이트할 수 있습니다.
프로그래밍 언어 변경
이 문제는 충분히 강력한 OO 프로그래밍 시스템에 간단한 해결책이 있습니다.본질적으로, 원-타원 문제는 두 가지 유형의 표현을 동기화하는 것 중 하나이다. 즉, 물체의 특성에 기초한 사실상의 유형과 객체 시스템에 의해 물체와 관련된 형식적 유형이다.이 두 정보, 즉 기계 안의 비트에 불과하지만, 같은 말을 할 수 있도록 동기화된 상태를 유지한다면, 모든 것이 정상입니다.원의 기저 타원법이 매개변수의 변환을 허용하는 동안 원이 필요한 불변량을 충족할 수 없다는 것은 명백하다.그러나 원이 원의 불변량을 충족시킬 수 없는 경우, 그 유형을 갱신하여 타원이 될 수 있습니다.사실상의 타원이 된 원이 유형을 바꾸지 않는 경우, 그 유형은 현재 현실(그 후 변형이 된 것)이 아니라 오브젝트의 역사(어떻게 구성되었는지)를 반영하는 오래된 정보입니다.
일반적으로 사용되고 있는 많은 객체 시스템은 설계도에 기초하고 있으며, 이는 객체가 시공에서 최종화에 이르기까지 전 생애에 걸쳐 동일한 유형을 운반하는 것을 당연하게 여긴다.이는 OOP의 제한이 아니라 특정 구현에만 해당됩니다.
다음 예제에서는 오브젝트가 ID를 잃지 않고 클래스를 변경할 수 있는 Common Lisp Object System(CLOS)을 사용합니다.객체에 대한 참조를 유지하는 모든 변수 또는 기타 저장 위치는 클래스가 변경된 후에도 동일한 객체에 대한 참조를 계속 유지합니다.
원과 타원 모델은 원-타원 문제와 관련이 없는 산만한 세부 사항을 피하기 위해 의도적으로 단순화되었습니다.타원은 코드에서 h축과 v축이라고 불리는 두 개의 반축을 가지고 있다.타원이기 때문에 원은 이러한 특성을 계승하고 반지름 속성도 가지고 있습니다. 반지름 속성은 축의 값과 동일해야 합니다(물론 동일해야 합니다.
(디클래스 타원 () ((h축 : 타입 진짜 : 악세사리 h축 : initarg : h축) (v축 : 타입 진짜 : 악세사리 v축 : initarg : v축))) (디클래스 원형 (타원) ((반지름 : 타입 진짜 : 악세사리 반지름 : initarg : 개요))) ;;; ;;; 원에는 반지름이 있지만 다음과 같은 h축과 v축이 있습니다. ;;; 타원에서 상속됩니다.이것들은 동기화되어 있어야 합니다. ;;; 오브젝트가 초기화되었을 때의 반지름과 ;;;;; 이러한 값이 변경되었을 때. ;;; (디프로덕트 초기화 처리 ((c 원형) 키 반지름) (설정 (반지름 c) 반지름)) ;; 아래 setf 메서드에 의해 (디프로덕트 (설정 반지름) : 이후 ((새로운 가치의 진짜) (c 원형)) (설정 (슬롯 값 c 'H축') 새로운 가치의 (슬롯 값 c 'V축) 새로운 가치의)) ;;; ;;; 서클의 할당 후 ;;;;;;; h축 또는 v축, 타입의 변경이 필요하다. ;;;; 새 값이 반지름과 동일하지 않은 경우. ;;; (디프로덕트 (설정 h축) : 이후 ((새로운 가치의 진짜) (c 원형)) (~하지 않는 한 (= (반지름 c) 새로운 가치의) (체인지 클래스 c '타원'))) (디프로덕트 (설정 v축) : 이후 ((새로운 가치의 진짜) (c 원형)) (~하지 않는 한 (= (반지름 c) 새로운 가치의) (체인지 클래스 c '타원'))) ;;; ;;; 접근자일 경우 타원이 원으로 변경된다. ;;; 축이 같도록 변환한다. ;; 또는 그러한 방법으로 구축하려고 했을 경우. ;;; ;;; EQL 동일성이 사용되며, 이 값에서는 0 /= 0.0입니다. ;;; ;;; (디프로덕트 초기화 처리 : 이후 ((e 타원) 키 h축 v축) (한다면 (= h축 v축) (체인지 클래스 e '오디오'))) (디프로덕트 (설정 h축) : 이후 ((새로운 가치의 진짜) (e 타원)) (~하지 않는 한 (타이프 e '오디오') (한다면 (= (h축 e) (v축 e)) (체인지 클래스 e '오디오')))) (디프로덕트 (설정 v축) : 이후 ((새로운 가치의 진짜) (e 타원)) (~하지 않는 한 (타이프 e '오디오') (한다면 (= (h축 e) (v축 e)) (체인지 클래스 e '오디오')))) ;;; ;;; 타원이 원이 되는 방법.이 변혁에서 ;;; 객체는 반경을 취득합니다.반경은 초기화해야 합니다. ;;; 여기에서는, 에러를 나타내는 「온전성 체크」가 있습니다. ;;;; 축이 동일하지 않은 타원을 변환하기 위해 만들어짐 ;;;;; 명시적인 변경 클래스콜을 사용합니다. ;;; 여기서의 처리전략은 반경을 기준으로 하는 것입니다. ;;;;; h축 및 신호 오류. ;;; 이것은 클래스 변경을 막는 것이 아닙니다.이미 피해를 입었습니다. ;;; (디프로덕트 다른 클래스의 갱신 프로그램 : 이후 ((구식 타원) (새로운 원형) 키) (설정 (반지름 새로운) (h축 구식)) (~하지 않는 한 (= (h축 구식) (v축 구식)) (에러 "타원~s는 원이 아니기 때문에 원으로 변할 수 없습니다!" 구식)))
이 코드는 Common Lisp의 CLISP 구현을 사용하여 인터랙티브세션에서 시연할 수 있습니다.
$clisp - q - i circle - ellips [ 1 ] > (메이크 인스턴스 '타원:v-axis 3:h-axis 3) # < CIRCLE #x218AB566> [2]> (메이크 인스턴스 '타원:v축 3:h축 4) #<ELLIPSE #x218BF56E> [ 3 ] > ( defvar obj ( make - instance ' ellipse : v - axis 3 : h - axis 4 ) > OBJ [ 4 ] > ( class - of obj ) # < STANDARDARD - CLASS ELLIPSE > [ 5 obj ] > ( 반지름) ** - NO - APPRAPPRAPPLABJ - METERICLE - MATODE - Method : GENERIC : > < GENERIC :>다음 다시 시작될 때:RETRY:low일 다시 RETURN RADIUS를 호출하십시오:R2ABORT:R3를 주요 고리 1[6]>다;:[7]> 반환 값을 지정하는;(setf(v-axis obj)4cm부터 4[8]>,(반경 obj)4[9]>,(class-of obj)#<>사용할 수 있다.STANDARD-CLASS CIRCLE>,[10]>,(setf(반경 obj)9)9[11]>,(v-axis obj)9[12]>.(h-axis o.bj) 9 [13]> (h축 obj) 8 [14]> (class-of obj) # <STANDARD-CLASS ELLIPS> [15]> (반경 obj) *** - NO-APPLICABLE-METHOOD : # (#215EXC #)를 사용하여 호출하는 경우다음의 재기동을 사용할 수 있습니다.RETTY : R1 try again RADIUS : RETURN : R2 specify return values ABRT : R3 Abort main loop Break 1 [ 16 ]> : a [ 17 ] >
문제의 전제에 이의를 제기하다
얼핏 보면 원이 타원인 것처럼 보일 수 있지만, 다음과 같은 유사한 코드를 고려하십시오.
학급 사람인 { 무효 북쪽으로 걷다(인트 미터) {...} 무효 동쪽으로 걷다(인트 미터) {...} }
자, 죄수는 분명히 사람이다.따라서 논리적으로 서브클래스를 만들 수 있습니다.
학급 죄수 확장 사람인 { 무효 북쪽으로 걷다(인트 미터) {...} 무효 동쪽으로 걷다(인트 미터) {...} }
또한, 수감자가 임의의 거리를 자유롭게 이동할 수 없기 때문에, 분명히 이것은 문제를 야기합니다. 하지만 Person class의 계약서에는 사람이 할 수 있다고 명시되어 있습니다.
따라서 클래스 이름을 FreePerson으로 하는 것이 좋습니다.만약 그렇다면, 클래스 죄수들이 FreePerson을 확장한다는 생각은 명백히 잘못된 것이다.
유추하자면, 원은 타원과 같은 자유도가 없기 때문에 타원이 아닙니다.
따라서 더 나은 이름을 적용하면 원의 이름은 OneDiameterFigure로 지정되고 타원은 TwoDiameterFigure로 지정될 수 있습니다.이러한 이름을 사용하면 TwoDiameterFigure는 다른 속성을 추가하기 때문에 OneDiameterFigure를 확장해야 합니다.한편 OneDiameterFigure에는 단일 직경 속성이 있으며 TwoDiameterFigure에는 두 가지 속성(장축과 단축 길이)이 있습니다.
이는 서브클래스가 기본클래스에 암묵적인 자유를 제한할 때 상속을 사용해서는 안 되며, 'Monkey' is-an An Animal'에서와 같이 하위클래스가 기본클래스에 의해 표현되는 개념에 추가 세부사항을 추가할 때만 사용되어야 함을 강력히 시사합니다.
그러나 죄수가 임의의 거리를 이동할 수 없고 사람이 움직일 수 있다고 말하는 것은 다시 한 번 잘못된 전제이다.어느 방향으로든 움직이는 물체는 장애물에 부딪힐 수 있다.이 문제를 모델화하는 올바른 방법은 WalkAttemptResult walkToDirection(int meter, Direction) 계약을 체결하는 것입니다.이제 하위 클래스 Prisoner에 대해 walkToDirection을 구현할 때 경계를 확인하고 적절한 걷기 결과를 반환할 수 있습니다.
불변성
개념적으로 Circle과 Ellipse는 각각 Mutable Container의 에일리어스, Mutable Container의 에일리어스, Mutable Container의 에일리어스, Mutable Container의 에일리어스로 간주할 수 있습니다.이 경우 ImputableCircle은 ImputableEllipse의 서브타입으로 간주할 수 있습니다.Mutable Container의 T형 <T>는, 기입과 판독의 양쪽 모두를 할 수 있기 때문에, 공변량도 반변량도 아니고, 불변량도 아닙니다.따라서 원은 타원의 하위 유형이 아니며 그 반대도 아닙니다.
레퍼런스
- ^ Kazimir Majorinc, Ellipse-Circle Demilla and Inverse Inheritation, 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 사이트.문제를 설명하고 설명합니다.
- Alistair Cockburn이 자신의 웹사이트에 올린 서브타이핑의 건설적 해체.타이핑과 서브타이핑에 대한 기술적/수학적인 논의와 이 문제에 대한 응용 프로그램.
- Henney, Kevlin (2003-04-15). "From Mechanism to Method: Total Ellipse". Dr. Dobb's.
- http://orafaq.com/usenet/comp.databases.theory/2001/10/01/0001.htm 이 문제에 대한 Oracle FAQ의 긴 스레드 시작(Maybe reply: links 아래 참조).C.J. 데이트의 글을 참조합니다.Smalltalk에 대한 약간의 편견.
- WikiWikiWeb에서의 Liskov 대체 원칙
- 서브타이핑, 서브클래싱 및 OOP에 관한 트러블, 관련 문제를 논의하는 에세이: 세트는 가방에서 상속되어야 하는가?
- 객체 지향 데이터베이스의 제약에 의한 서브타이핑. 객체 지향 데이터베이스의 환경에서 원 타원형 문제의 확장 버전에 대해 논의하는 에세이입니다.