SetWidth 및 SetHeight 메서드를 재정의하면 Rectangle에서 Square 상속이 문제가되는 이유는 무엇입니까?


105

사각형이 사각형 유형 인 경우 사각형에서 사각형을 상속 할 수없는 이유는 무엇입니까? 아니면 왜 나쁜 디자인입니까?

사람들이 말하는 것을 들었습니다.

사각형을 사각형에서 파생시킨 경우 사각형을 기대하는 모든 위치에서 사각형을 사용할 수 있어야합니다

여기서 무슨 문제가 있습니까? 사각형이 필요한 곳이라면 어디에서 Square를 사용할 수 있습니까? Square 객체를 생성하고 Square에 대한 SetWidth 및 SetHeight 메소드를 재정의하는 경우에만 문제가 발생하는 것보다 사용할 수 있습니다.

Rectangle 기본 클래스에 SetWidth 및 SetHeight 메서드가 있고 Rectangle 참조가 Square를 가리키는 경우 SetWidth 및 SetHeight는 하나를 설정하면 다른 것과 일치하도록 변경되므로 의미가 없습니다. 이 경우 Square는 Rectangle을 사용한 Liskov 대체 테스트에 실패하고 Square를 Rectangle에서 상속받는 추상화는 좋지 않습니다.

누군가 위의 주장을 설명 할 수 있습니까? 다시 Square에서 SetWidth 및 SetHeight 메서드를 재정의하면이 문제가 해결되지 않습니까?

나는 또한 들었다 / 읽었다 :

실제 문제는 사각형을 모델링하는 것이 아니라 "결과 사각형"즉, 너비 나 높이를 만든 후에 수정할 수있는 사각형 (그리고 여전히 같은 개체라고 생각하는 사각형)이라는 것입니다. 이런 식으로 사각형 클래스를 보면 사각형은 "모두 가능한 사각형"이 아닙니다. 사각형은 모양을 바꿀 수없고 여전히 사각형입니다 (일반적으로). 수학적으로 우리는 문제가 보이지 않습니다. 왜냐하면 가변성은 수학적 맥락에서 의미가 없기 때문입니다

여기서는 "크기 조정 가능"이 올바른 용어라고 생각합니다. 사각형은 크기를 조정할 수 있으며 사각형도 마찬가지입니다. 위의 주장에서 뭔가 빠졌습니까? 사각형은 사각형처럼 크기를 조정할 수 있습니다.


15
이 질문은 굉장히 추상적 인 것 같습니다. 어떤 클래스에서 어떤 클래스를 상속받는 것이 좋은지에 대한 여부와 상관없이 클래스와 상속을 사용하는 방법은 일반적으로 좋은 방법입니다. 실제 사례가 없으면이 질문이 어떻게 관련 답변을 얻을 수 있는지 알 수 없습니다.
aaaaaaaaaaaa

2
어떤 상식을 사용하면 square 사각형 이라는 것이 기억 되므로 사각형이 필요한 곳에서 square 클래스의 객체를 사용할 수 없다면 어쨌든 일부 응용 프로그램 디자인 결함 일 수 있습니다.
크 툴후

7
더 좋은 질문은 Why do we even need Square? 펜이 두 개있는 것과 같습니다. 하나의 파란색 펜과 하나의 빨간색 파란색, 노란색 또는 녹색 펜. 파란색 펜은 여분입니다-비용 이점이 없으므로 정사각형의 경우 훨씬 더 좋습니다.
Gusdor

2
@eBusiness 그것의 추상적 성은 좋은 학습 문제를 만드는 것입니다. 특정 사용 사례와 독립적으로 어떤 하위 유형 사용이 나쁜지를 인식 할 수 있어야합니다.
Doval

5
@Cthulhu 실제로는 아닙니다. 서브 타이핑은 동작 에 관한 것이며 변경 가능한 사각형은 변경 가능한 사각형처럼 동작하지 않습니다. 이것이 "..."은유가 나쁜 이유입니다.
Doval

답변:


189

기본적으로 우리는 현명하게 행동하기를 원합니다.

다음 문제를 고려하십시오.

직사각형 그룹이 주어졌으며 면적을 10 % 증가시키고 싶습니다. 그래서 내가하는 일은 사각형의 길이를 이전보다 1.1 배로 설정하는 것입니다.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

이제이 경우 모든 직사각형의 길이가 10 % 증가하여 면적이 10 % 증가합니다. 불행히도 누군가가 실제로 사각형과 사각형의 혼합물을 전달했으며 사각형의 길이가 변경되면 너비도 바뀌 었습니다.

모든 단위 테스트를 작성하여 사각형 모음을 사용했기 때문에 단위 테스트가 통과되었습니다. 이제 몇 달 동안 눈에 띄지 않을 수있는 미묘한 버그를 응용 프로그램에 도입했습니다.

더 나쁜 것은 회계에서 Jim이 내 방법을보고 내 방법에 사각형을 전달하면 크기가 21 % 증가한다는 사실을 사용하는 다른 코드를 작성하는 것입니다. 짐은 행복하고 더 현명한 사람은 없습니다.

Jim은 다른 부서의 우수한 업무를 위해 승진했습니다. Alfred는 주니어로 회사에 합류했습니다. 그의 첫 번째 버그 보고서에서 Jill from Advertising은이 방법으로 제곱을 전달하면 21 % 증가하고 버그 수정을 원한다고보고했습니다. Alfred는 Squares와 Rectangles가 코드의 어느 곳에서나 사용되며 상속 체인을 깨는 것은 불가능하다는 것을 알고 있습니다. 또한 회계 소스 코드에 액세스 할 수 없습니다. 따라서 Alfred는 다음과 같이 버그를 수정합니다.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

Alfred는 자신의 동네 짱 해킹 기술에 만족하고 질은 버그가 수정되었음을 서명합니다.

다음 달 회계는 IncreaseRectangleSizeByTenPercent방법에 제곱을 전달 하고 면적이 21 % 증가하는 데 의존했기 때문에 아무도 돈을받지 못했습니다 . 회사 전체가 "우선 순위 1 버그 수정"모드로 들어가 문제의 원인을 추적합니다. 그들은 Alfred의 수정에 대한 문제를 추적합니다. 그들은 회계와 광고를 모두 행복하게 유지해야한다는 것을 알고 있습니다. 따라서 메소드 호출로 사용자를 식별하여 문제를 해결합니다.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

그리고 등등.

이 일화는 프로그래머가 매일 직면하는 실제 상황을 기반으로합니다. Liskov 교체 원칙의 위반은 위반 사항을 수정하는 일의 무리를 끊고되는 시간은 작성하고 이후 만 년을 발탁 매우 미묘한 버그 도입 할 수 고정되지 는 가장 큰 클라이언트를 화나게됩니다.

이 문제를 해결하는 두 가지 현실적인 방법이 있습니다.

첫 번째 방법은 Rectangle을 변경할 수 없게 만드는 것입니다. Rectangle 사용자가 Length 및 Width 속성을 변경할 수 없으면이 문제는 사라집니다. 길이와 너비가 다른 사각형을 원하면 새 사각형을 만듭니다. 사각형은 사각형에서 행복하게 상속받을 수 있습니다.

두 번째 방법은 사각형과 사각형 사이의 상속 체인을 끊는 것입니다. 사각형이 단일 SideLength속성 을 갖는 것으로 정의 되고 사각형에 Lengthand Width속성이 있고 상속이 없으면 사각형을 기대하고 사각형을 가져 와서 실수로 물건을 깰 수 없습니다. C # 용어로 seal사각형 클래스 를 사용할 수 있습니다. 이렇게하면 모든 사각형이 실제로 사각형이됩니다.

이 경우 문제를 해결하는 "불변의 객체"방식이 마음에 듭니다. 사각형의 정체성은 길이와 너비입니다. 객체의 아이덴티티를 변경하려고 할 때 실제로 원하는 것은 새로운 객체 라는 것이 합리적입니다 . 기존 고객을 잃고 새로운 고객을 얻는 경우 기존 고객에서 새 고객으로 Customer.Id필드를 변경하지 않고 새 고객 을 만듭니다 Customer.

Liskov 대체 원칙의 위반은 실제 세계에서 일반적입니다. 대부분 많은 코드가 능력이 없거나 시간이 지남에 따라 / 관리 / 실수하지 않는 사람들이 작성했기 때문입니다. 그것은 매우 불쾌한 문제로 이어질 수 있습니다. 대부분의 경우 상속 대신 구성선호 합니다.


7
Liskov는 한 가지 일이며 스토리지는 또 다른 문제입니다. 대부분의 구현에서 Rectangle에서 상속되는 Square 인스턴스는 하나만 필요하더라도 2 차원을 저장할 공간이 필요합니다.
el.pescado

29
이야기를 훌륭하게 사용하여 요점을 설명
Rory Hunter

29
좋은 이야기지만 동의하지 않습니다. 사용 사례는 다음과 같습니다. 사각형의 영역을 변경합니다. 수정은 사각형에 특화된 사각형에 재정의 가능한 메서드 'ChangeArea'를 추가해야합니다. 이것은 상속 체인을 끊지 않고 사용자가 원하는 것을 명시 적으로 언급하며 언급 된 수정 사항 (적절한 준비 영역에서 잡혔을 것)으로 인한 버그를 유발하지 않았을 것입니다.
Roy T.

33
@RoyT .: 사각형 이 영역 을 설정 하는 방법을 알아야하는 이유는 무엇 입니까? 그것은 길이와 너비에서 완전히 파생 된 속성입니다. 더 나아가서 길이, 너비 또는 둘 다의 치수를 변경해야합니까?
cHao

32
@Roy T. 문제를 다르게 해결했다는 것은 매우 기쁜 일이지만 사실은 개발자가 레거시 제품을 유지 관리 할 때 매일 직면하는 실제 상황의 예라는 사실입니다. 그리고 그 메소드를 구현하더라도 상속자가 LSP를 위반하고 이와 유사한 버그를 도입하는 것을 막을 수는 없습니다. 이것이 바로 .NET 프레임 워크의 거의 모든 클래스가 봉인 된 이유입니다.
Stephen

30

모든 객체가 불변이라면 아무런 문제가 없습니다. 모든 광장은 또한 사각형입니다. 사각형의 모든 속성은 사각형의 속성이기도합니다.

개체를 수정하는 기능을 추가하면 문제가 시작됩니다. 또는 실제로-속성 getter를 읽는 것이 아니라 객체에 인수를 전달하기 시작할 때.

너비 또는 높이 변경과 같이 Rectangle 클래스의 모든 불변은 유지하지만 모든 사각형 불변은 유지하지 않는 Rectangle에 대한 수정이 있습니다. 분명히 Rectangle의 동작은 속성 일뿐만 아니라 가능한 수정이기도합니다. Rectangle에서 얻는 것뿐만 아니라에 넣을 수도 있습니다 .

Rectangle setWidth에 너비를 변경하고 높이를 수정하지 않은 것으로 문서화 된 방법이있는 경우 Square는 호환되는 방법을 가질 수 없습니다. 높이가 아닌 너비를 변경하면 더 이상 유효한 정사각형이 아닙니다. 를 사용할 때 사각형의 너비와 높이를 모두 수정하기로 선택한 경우 setWidthRectangle 's 사양을 구현하지 않은 것 setWidth입니다. 당신은 이길 수 없습니다.

Rectangle과 Square에 "입력"할 수있는 내용, 어떤 메시지를 보낼 수 있는지 살펴보면 Square로 올바르게 보낼 수있는 모든 메시지를 Rectangle로 보낼 수도 있습니다.

공분산 대 반 분산의 문제입니다.

수퍼 클래스가 예상되는 모든 경우에 인스턴스를 사용할 수있는 적절한 서브 클래스의 메소드는 다음을 수행해야합니다.

  • 수퍼 클래스가 리턴 할 값만 리턴하십시오. 즉, 리턴 유형은 수퍼 클래스 메소드 리턴 유형의 하위 유형이어야합니다. 반품은 공변량입니다.
  • 수퍼 타입이 허용하는 모든 값을 승인하십시오. 즉, 인수 유형은 수퍼 클래스 메소드의 인수 유형의 수퍼 타입이어야합니다. 인수는 반 변형입니다.

따라서 Rectangle 및 Square로 돌아 가기 : Square가 Rectangle의 하위 클래스가 될 수 있는지 여부는 Rectangle의 메소드에 따라 달라집니다.

Rectangle에 너비와 높이에 대한 개별 세터가 있으면 Square는 좋은 하위 클래스를 만들지 않습니다.

마찬가지로 compareTo(Rectangle)Rectangle 및 compareTo(Square)Square에서 와 같이 인수에서 일부 메서드 를 공변량으로 만들면 Square를 Rectangle로 사용하는 데 문제가 있습니다.

Square와 Rectangle이 호환되도록 디자인하면 작동 할 수 있지만 함께 개발해야합니다. 그렇지 않으면 작동하지 않을 것입니다.


"모든 객체가 불변이라면 아무런 문제가 없다" -이것은이 질문과 관련하여 폭과 높이에 대한 세터를 명시 적으로 언급하는 무의미한 진술이다
gnat

11
나는 그것이 "명백하게 관련이없는"경우에도이 흥미로운 것을 발견했다
Jesvin Jose

14
@ gnat 두 가지 유형 사이에 유효한 하위 유형 관계가있을 때 질문의 실제 가치가 인식되고 있기 때문에 관련성이 있다고 주장합니다. 이는 수퍼 타입이 선언하는 연산에 따라 달라 지므로 뮤 테이터 메소드가 사라지면 문제가 해결 될 가치가 있습니다.
Doval

1
@gnat도 setter는 mutators 이므로 lrn은 본질적으로 "그렇게 하지 말고 문제가되지 않는다"고 말합니다. 간단한 유형의 불변성에 동의하지만 좋은 점을 지적합니다. 복잡한 객체의 경우 문제가 그렇게 간단하지 않습니다.
Patrick M

1
이런 식으로 생각하면 'Rectangle'클래스에 의해 보장되는 동작은 무엇입니까? 너비와 높이를 서로 바꿀 수 있습니다. (즉, setWidth 및 setHeight) 메소드. Square가 Rectangle에서 파생 된 경우 Square는이 동작을 보장해야합니다. square는이 동작을 보장 할 수 없으므로 상속이 잘못되었습니다. 그러나 setWidth / setHeight 메서드가 Rectangle 클래스에서 제거되면 이러한 동작이 없으므로 Rectangle에서 Square 클래스를 파생시킬 수 있습니다.
Nitin Bhide

17

여기에 좋은 대답이 많이 있습니다. 특히 Stephen의 대답은 대체 원칙 위반이 팀 간의 실제 충돌로 이어지는 이유를 잘 설명해줍니다.

나는 LSP의 다른 위반에 대한 은유로 사용하지 않고 사각형과 사각형의 특정 문제에 대해 간단히 이야기 할 수 있다고 생각했습니다.

사각형의 사각형은 거의 언급되지 않는 추가적인 문제가 있는데, 왜 우리가 사각형과 사각형으로 멈추는가 ? 정사각형이 특별한 종류의 사각형이라고 기꺼이 말하면 기꺼이 말할 것입니다.

  • 사각형은 특별한 종류의 마름모입니다. 사각형 각도의 마름모입니다.
  • 마름모는 특별한 종류의 평행 사변형입니다. 그것은 같은 변을 가진 평행 사변형입니다.
  • 사각형은 특별한 종류의 평행 사변형입니다. 정사각형의 평행 사변형입니다.
  • 직사각형, 정사각형 및 평행 사변형은 모두 특별한 종류의 사다리꼴입니다. 두 세트의 평행 한면이있는 사다리꼴입니다.
  • 위의 모든 것은 특별한 종류의 사변형입니다.
  • 위의 모든 것은 특별한 종류의 평면 모양입니다.
  • 등등; 나는 한동안 여기에 갈 수 있었다.

지구상의 모든 관계가 여기에 있어야합니까? C # 또는 Java와 같은 클래스 상속 기반 언어는 여러 종류의 제약 조건과 이러한 복잡한 관계를 나타내도록 설계되지 않았습니다. 이러한 것들을 모두 서브 타이핑 관계가있는 클래스로 나타내려고하지 않음으로써 질문을 완전히 피하는 것이 가장 좋습니다.


3
모양 객체가 불변 인 IShape경우 경계 상자를 포함 하는 유형을 가질 수 있으며 그리기, 크기 조정 및 직렬화 될 수 있으며 IPolygon정점 수를보고하는 메소드와을 반환하는 메소드 가있는 하위 유형이있을 수 IEnumerable<Point>있습니다. 하나는 가질 수 IQuadrilateral에서 파생 된 하위 유형을 IPolygon, IRhombus그리고 IRectangle그에서 파생 및 ISquare파생 IRhombusIRectangle. 변경 가능성은 모든 것을 창 밖으로 던져 버리고 다중 상속은 클래스에서 작동하지 않지만 변경 불가능한 인터페이스에서는 괜찮습니다.
supercat

나는 여기서 Eric과 효과적으로 동의하지 않는다 (-1은 충분하지 않다!). @supercat가 언급했듯이 모든 관계는 (아마도) 관련이 있습니다. 그것은 단지 YAGNI 문제입니다. 필요할 때까지 구현하지 마십시오.
Mark Hurd

아주 좋은 답변입니다! 더 높아야합니다.
andrew.fox

1
@MarkHurd-YAGNI 문제가 아닙니다. 제안 된 상속 기반 계층 구조는 설명 된 분류 체계와 유사하지만이를 정의하는 관계를 보장하기 위해 작성할 수는 없습니다. 어떻게 IRhombus모든 것을 보장 Point으로부터 반환 Enumerable<Point>에 의해 정의 된 IPolygon동일한 길이의 가장자리에 해당? IRhombus인터페이스 의 구현 만으로도 구체적인 객체가 마름모임을 보장 할 수 없으므로 상속이 답이 될 수 없습니다.
A. Rager

14

수학적 관점에서 정사각형은 사각형입니다. 수학자가 사각형을 수정하여 더 이상 사각형 계약을 준수하지 않으면 사각형으로 바뀝니다.

그러나 OO 디자인에서는 이것이 문제입니다. 객체가 무엇인지, 여기에는 상태뿐만 아니라 동작 도 포함됩니다. 사각형 개체를 보유하고 있지만 다른 사람이 사각형으로 수정하면 내 잘못없이 사각형의 계약을 위반하는 것입니다. 이로 인해 모든 종류의 나쁜 일이 발생합니다.

여기서 핵심 요소는 가변성 입니다. 구성한 후에 모양을 변경할 수 있습니까?

  • 변경 가능 : 일단 구성한 후에 모양을 변경할 수 있으면 사각형은 사각형과 is-a 관계를 가질 수 없습니다. 직사각형의 수축에는 반대쪽의 길이가 같아야하지만 인접한 변의 길이는 같아야한다는 제약 조건이 포함됩니다. 정사각형에는 네 개의 동일한 변이 있어야합니다. 사각형 인터페이스를 통해 사각형을 수정하면 사각형 계약을 위반할 수 있습니다.

  • 변경할 수 없음 : 일단 생성 된 모양이 변경되지 않으면 사각형 개체도 항상 사각형 계약을 충족해야합니다. 사각형은 사각형과 is-a 관계를 가질 수 있습니다.

두 경우 모두 하나 이상의 변화가있는 상태를 기반으로 새로운 모양을 생성하도록 사각형에 요청할 수 있습니다. 예를 들어 "대변 A와 C의 길이가 두 배인 것을 제외하고이 사각형을 기반으로 새 직사각형을 만듭니다"라고 말할 수 있습니다. 새로운 물체가 건설되고 있기 때문에, 원래 사각형은 계속 계약을 준수합니다.


1
This is one of those cases where the real world is not able to be modeled in a computer 100%. 왜 그래? 우리는 여전히 정사각형과 직사각형의 기능 모델을 가질 수 있습니다. 유일한 결과는이 두 객체를 추상화하기 위해 더 간단한 구성을 찾아야한다는 것입니다.
Simon Bergot

6
사각형과 사각형 사이에는 공통점이 더 많습니다. 문제는 사각형의 정체와 사각형의 정체가 측면 길이 (각 교차점의 각도)라는 것입니다. 여기서 가장 좋은 해결책은 사각형을 사각형에서 상속하지만 둘 다 변경할 수 없도록 만드는 것입니다.
Stephen

3
@ 스티븐 동의. 실제로, 그것들을 불변으로 만드는 것은 서브 타이핑 문제와 상관없이 합리적인 일입니다. 그것들을 변경시킬 이유가 없습니다. 새로운 사각형이나 사각형을 변경하는 것보다 그것을 변형시키는 것보다 어렵지 않은 이유는 무엇입니까? 이제 앨리어싱 / 부모에 대해 걱정할 필요가 없으며 필요에 따라 맵 / 딕션의 키로 사용할 수 있습니다. 어떤 사람들은 "성능"이라고 말하고, 실제로 핫스팟이 형태 코드에 있다는 것을 측정하고 증명할 때까지 "조기 최적화"라고 말할 것입니다.
Doval

죄송합니다. 늦었고 답변을 쓸 때 매우 피곤했습니다. 나는 내가 정말로 의미하는 바를 말하기 위해 다시 썼다.

13

사각형이 필요한 곳이라면 어디에서 Square를 사용할 수 있습니까?

그것이 하위 유형이라는 의미의 일부이기 때문에 (Liskov 대체 원칙 참조). 당신은 할 수 있고, 이것을 할 수 있어야합니다 :

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

실제로 OOP를 사용할 때 항상 (보다 암시 적으로)이 작업을 수행합니다.

Square에 대해 SetWidth 및 SetHeight 메서드를 재정의하면 왜 문제가 발생합니까?

에 대한 내용을 현명하게 재정의 할 수 없기 때문입니다 Square. 사각형 "사각형처럼 크기를 조정할 수 없습니다 ". 사각형의 높이가 변해도 너비는 동일하게 유지됩니다. 그러나 사각형의 높이가 변경되면 너비가 그에 따라 변경되어야합니다. 이 문제는 크기를 조정할 수있는 것이 아니라 두 차원에서 독립적으로 크기를 조정할 수 있습니다.


많은 언어에서 Rect r = s;회선이 필요하지 않으며 doSomethingWith(s), 런타임과 s가상 Square메소드 를 분석하기 위해 런타임을 호출 합니다.
Patrick M

1
@PatrickM 서브 타이핑이 포함 된 제정신의 언어로는 필요하지 않습니다. 나는 명쾌하게 표현하기 위해 그 줄을 포함시켰다.

따라서 너비와 높이를 모두 재정의 setWidth하고 setHeight변경하십시오.
ApproachingDarknessFish

@ValekHalfHeart 정확히 내가 고려중인 옵션입니다.

7
@ValekHalfHeart : Liskov 대체 원칙을 위반하는 것은 2 년 후 코드 작동 방식을 잊어 버렸을 때 이상한 버그를 찾으려고 잠을 자지 못하게하는 Liskov 대체 원칙을 위반하는 것입니다.
Jan Hudec

9

당신이 설명하는 것은 Liskov 대체 원칙에 위배됩니다 . LSP의 기본 개념은 특정 클래스의 인스턴스를 사용할 때마다 버그를 발생 시키지 않고 해당 클래스의 하위 클래스 인스턴스를 항상 교체 할 수 있어야한다는 것 입니다.

Rectangle-Square 문제는 Liskov를 소개하는 좋은 방법이 아닙니다. 실제로는 미묘한 예를 사용하여 광범위한 원리를 설명하려고 시도하며 모든 수학에서 가장 일반적인 직관적 정의 중 하나를 무시합니다. 어떤 사람들은 그런 이유로 타원-원 문제라고 부릅니다. 그러나 이것이가는 한 약간 더 좋습니다. 더 나은 접근 방법은 내가 평행 사변형-사각형 문제라고 부르는 것을 사용하여 약간 뒤로 물러서는 것입니다. 이것은 일을 이해하기 훨씬 쉽게 만듭니다.

평행 사변형은 두 쌍의 평행 한면이있는 사변형입니다. 또한 두 쌍의 합동 각이 있습니다. 다음 행을 따라 평행 사변형 객체를 상상하기는 어렵지 않습니다.

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

사각형을 생각하는 일반적인 방법 중 하나는 직각을 가진 평행 사변형입니다. 언뜻보기에 Rectangle을 Parallelogram 에서 상속받을 수있는 좋은 후보로 만들 수 있으므로 맛있는 코드를 모두 재사용 할 수 있습니다. 하나:

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

이 두 함수가 Rectangle에 버그를 일으키는 이유는 무엇입니까? 문제는 직사각형에서 각도를 변경할 수 없다는 것입니다. 각도는 항상 90 도로 정의되어 있으므로이 인터페이스는 실제로 평행 사변형에서 상속되는 Rectangle에서 작동하지 않습니다. Rectangle을 Parallelogram을 기대하는 코드로 바꾸고 그 코드가 각도를 변경하려고 시도하면 거의 확실하게 버그가 있습니다. 우리는 서브 클래스에서 쓸 수있는 것을 취해서 그것을 읽기 전용으로 만들었습니다. 그리고 그것은 Liskov 위반입니다.

자, 이것이 어떻게 사각형과 사각형에 적용됩니까?

우리가 가치를 설정할 수 있다고 말할 때, 우리는 일반적으로 가치를 쓸 수있는 것보다 조금 더 강한 것을 의미합니다. 우리는 어느 정도의 독점 성을 암시합니다 . 값을 설정하고 특별한 상황 을 배제 하면 다시 설정할 때까지 해당 값을 유지합니다. 쓸 수는 있지만 설정되지 않은 값은 많이 사용하지만 한 번 설정 한 위치에 머무르는 값에 의존하는 경우도 많습니다. 그리고 우리는 또 다른 문제에 직면합니다.

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

우리 Square 클래스는 Rectangle에서 버그를 물려 받았지만 새로운 버그가 있습니다. setSideA 및 setSideB의 문제는 더 이상 둘 중 어느 것도 더 이상 설정할 수 없다는 것입니다. 어느 쪽에도 값을 쓸 수는 있지만 다른쪽에 쓰면 아래에서 바뀔 것입니다. 서로 독립적으로면을 설정할 수있는 코드의 평행 사변형으로 이것을 바꾸면 괴물이 될 것입니다.

이것이 문제이며, Resangle-Square를 Liskov에 대한 소개로 사용하는 데 문제가있는 이유입니다. Rectangle-Square는 어떤 것에 쓸 수 있는 것과 그것을 설정할 수있는 것의 차이에 달려 있으며, 그것은 무언가를 설정할 수 있는 것보다 읽기 전용이되는 것보다 훨씬 미묘한 차이입니다. Rectangle-Square는 여전히 주목해야 할 상당히 일반적인 문제를 문서화하고 있기 때문에 여전히 예제로 가치가 있지만 소개 예제 로 사용해서는 안됩니다 . 학습자가 기본 사항을 먼저 이해 한 다음 더 열심히 던지도록하십시오.


8

서브 타이핑은 행동에 관한 것입니다.

type이 type B의 하위 유형이 A되려면 유형 A이 동일한 의미로 지원하는 모든 작업을 지원해야합니다 ( "동작"에 대한 멋진 대화). 모든 B가 A 라는 이론적 근거를 사용하는 것은 효과 가 없습니다. 행동 호환성은 최종적으로 말합니다. 대부분의 경우 "B는 일종의 A"이며 "B는 A처럼 행동"과 겹치지 만 항상 그런 것은 아닙니다 .

예를 들면 :

실수 세트를 고려하십시오. 모든 언어에서, 우리는 그들이 작업을 지원하기 위해 기대할 수 +, -, *,와 /. 이제 양의 정수 세트 ({1, 2, 3, ...})를 고려하십시오. 분명히 모든 양의 정수는 실수입니다. 그러나 양의 정수 유형은 실수 유형의 하위 유형입니까? 네 가지 연산을 살펴보고 양의 정수가 실수와 같은 방식으로 작동하는지 확인하십시오.

  • +: 문제없이 양의 정수를 추가 할 수 있습니다.
  • -: 양의 정수를 모두 뺄 때 양의 정수가되는 것은 아닙니다. 예 3 - 5.
  • *: 문제없이 양의 정수를 곱할 수 있습니다.
  • /: 항상 양의 정수를 나누고 양의 정수를 얻을 수는 없습니다. 예 5 / 3.

따라서 양의 정수는 실수의 부분 집합이지만 하위 유형이 아닙니다. 유한 크기의 정수에 대해서도 비슷한 주장을 할 수 있습니다. 분명히 모든 32 비트 정수는 64 비트 정수이지만 32_BIT_MAX + 1각 유형에 대해 다른 결과를 제공합니다. 따라서 일부 프로그램을 제공하고 모든 32 비트 정수 변수의 유형을 64 비트 정수로 변경하면 프로그램이 다르게 동작 할 가능성이 큽니다 (거의 항상 잘못됨 ).

물론 +결과는 64 비트 정수가되도록 32 비트 int를 정의 할 수 있지만 이제 두 개의 32 비트 숫자를 추가 할 때마다 64 비트의 공간을 예약해야합니다. 메모리 요구에 따라 허용되거나 허용되지 않을 수 있습니다.

이것이 왜 중요한가?

프로그램이 정확해야합니다. 프로그램이 갖는 가장 중요한 속성 일 것입니다. 프로그램이 특정 유형 A에 대해 올바른 B경우 모든 하위 유형 에 대해 B작동하는 경우 프로그램이 일부 하위 유형에 대해 계속 올바른지 확인하는 유일한 A방법입니다.

따라서 Rectangles사양의 측면은 독립적으로 변경할 수 있다고합니다. Rectangles구현 을 사용 하고 사양을 따르는 것으로 가정하는 일부 프로그램을 작성했습니다 . 그런 다음 Square측면의 크기를 독립적으로 조정할 수없는 하위 유형을 도입했습니다 . 결과적으로 사각형의 크기를 조정하는 대부분의 프로그램이 잘못되었습니다.


6

사각형이 사각형의 유형보다 사각형이 사각형에서 상속 할 수없는 이유는 무엇입니까? 아니면 왜 나쁜 디자인입니까?

우선 먼저 사각형이 사각형이라고 생각하는 이유를 스스로에게 물어보십시오 .

물론 대부분의 사람들은 초등학교에서 그 사실을 알게되었습니다. 사각형은 90도 각도의 4면 모양이며 사각형은 모든 속성을 충족시킵니다. 사각형이 아닌가?

그러나 그것은 모두 객체를 그룹화하기위한 초기 기준이 무엇인지, 객체를보고있는 컨텍스트에 달려 있다는 것입니다. 기하학에서 모양은 점, 선 및 천사의 특성에 따라 분류됩니다.

당신도 말을하기 전에 그래서 "정사각형이 직사각형의 유형입니다"먼저 자신에게 물어, 이 내가 걱정하는 기준에 따라입니다 .

대부분의 경우에 당신이 전혀 신경 쓰지 않을 것입니다. GUI, 그래픽 및 비디오 게임과 같은 모양을 모델링하는 대부분의 시스템은 주로 객체의 기하학적 그룹화와 관련이 없지만 동작입니다. 정사각형이 기하학적 의미에서 직사각형 유형이라는 것이 중요한 시스템에서 작업 한 적이 있습니까? 그것이 4면과 90도 각도를 가지고 있다는 것을 알고 당신에게 무엇을 줄 것입니까?

당신은 정적 시스템을 모델링하지 않고, 일이 일어날 곳 (모양이 생성, 파괴, 변경, 그려지는 동적 시스템)을 모델링하고 있습니다. 이러한 맥락에서 객체 간의 공유 동작에 관심이 있습니다. 주요 관심사는 모양으로 수행 할 수있는 작업, 일관성있는 시스템을 유지하기 위해 유지해야하는 규칙입니다.

이 문맥에서 사각형은 사각형이 아닙니다. 사각형을 변경하는 방법을 결정하는 규칙 은 사각형같지 않기 때문입니다. 그래서 그들은 같은 유형의 것이 아닙니다.

어떤 경우에는 그렇게 모델링하지 마십시오. 왜 당신은? 불필요한 제한 외에는 아무것도 얻지 못합니다.

Square 객체를 생성하고 Square에 대한 SetWidth 및 SetHeight 메소드를 재정의하는 경우에만 문제가 발생하는 것보다 사용할 수 있습니다.

그렇게하면 실제로 코드에서 동일한 것이 아니라고 진술합니다. 코드는 사각형이 이런 식으로 동작하고 사각형은 그런 식으로 동작하지만 여전히 동일하다고 말합니다.

두 가지 행동을 정의했기 때문에 관심있는 맥락에서 분명히 동일하지 않습니다. 그렇다면 관심이없는 상황에서만 비슷한 점이 동일한 척하는 이유는 무엇입니까?

이는 개발자가 모델링하려는 도메인에 올 때 중요한 문제를 강조합니다. 도메인의 객체에 대해 생각하기 전에 어떤 컨텍스트에 관심이 있는지 명확히하는 것이 중요합니다. 어떤면에 관심이 있습니까? 수천 년 전에 그리스인들은 선과 모양의 천사가 공유하는 속성을 염두에두고이를 기반으로 그룹화했습니다. 그렇다고해서 관심이없는 그룹 (99 %의 시간을 신경 쓰지 않는 소프트웨어)으로 그룹화를 계속해야한다는 의미는 아닙니다.

이 질문에 대한 많은 답변은 그룹화 행동에 관한 하위 유형에 초점을 맞추고 있습니다 .

그러나 규칙을 따르기 위해이 작업을 수행하지 않는다는 것을 이해하는 것이 중요합니다. 대부분의 경우에 이것이 실제로 관심을 갖는 것이므로이 작업을 수행하고 있습니다. 정사각형과 직사각형이 동일한 내부 천사를 공유하는지는 중요하지 않습니다. 정사각형과 직사각형이면서 그들이 할 수있는 일에 관심이 있습니다. 객체의 동작 규칙에 따라 시스템을 변경하는 데 중점을 둔 시스템을 모델링하기 때문에 객체의 동작에주의를 기울입니다.


type의 변수가 Rectangle 을 나타내는 데만 사용되는 경우 클래스 가 계약 을 상속 하고 완전히 준수 할 수 있습니다 . 불행하게도 많은 언어는 값을 캡슐화하는 변수와 엔티티를 식별하는 변수를 구별하지 않습니다. SquareRectangle
supercat

아마도, 그러나 왜 처음부터 귀찮게합니까? 사각형 / 사각 문제의 요점은 "사각형은 사각형이다"관계를 작동시키는 방법을 알아 내려는 것이 아니라 객체를 사용하는 상황에서 실제로 관계가 존재하지 않는다는 것을 인식하는 것입니다. (행동 적으로), 도메인과 관련이없는 관계를 강요하지 않는다는 경고입니다.
Cormac Mulhall

또는 다른 방법으로 넣으십시오 : 숟가락을 구부리지 마십시오. 불가능합니다. 대신 숟가락이 없다는 진실을 깨닫기 위해 노력하십시오. :-)
Cormac Mulhall 8:14에

1
불변 Square유형에서 상속 되는 불변 유형을 갖는 것은 Rectnagle사각형에서만 수행 할 수있는 작업이있는 경우 유용 할 수 있습니다. 개념의 현실적인 예로, ReadableMatrix유형 [기본 유형은 드물게 포함하여 다양한 방식으로 저장 될 수있는 직사각형 배열]과 ComputeDeterminant방법을 고려하십시오. 이 가지고있는 의미 할 수도 있습니다 ComputeDeterminant만 함께 작업 ReadableSquareMatrix에서 파생의 형태 ReadableMatrix나는 예라고 생각 것 Square에서 파생를 Rectangle.
supercat

5

사각형이 사각형의 유형보다 사각형이 사각형에서 상속 할 수없는 이유는 무엇입니까?

문제는 사물이 실제로 어떤 식 으로든 관련되어 있다면 모델링 후 정확히 같은 방식으로 관련되어야한다고 생각하는 데 있습니다.

모델링에서 가장 중요한 것은 공통 속성과 공통 동작을 식별하고 기본 클래스에서 정의하고 하위 클래스에 추가 속성을 추가하는 것입니다.

귀하의 예와 관련된 문제는 완전히 추상적입니다. 아무도 모르는 한, 그 클래스를 사용할 계획이라면 어떤 디자인을 만들어야하는지 추측하기가 어렵습니다. 그러나 실제로 높이, 너비 및 크기 조정 만 원한다면 다음과 같은 것이 더 논리적입니다.

  • width매개 변수 resize(double factor)를 사용하여 지정된 클래스만큼 너비를 조정하여 기본 클래스로 Square를 정의 하십시오.
  • Rectangle 클래스와 Square의 하위 클래스를 정의합니다. 다른 속성을 추가 height하고 해당 resize함수를 재정의합니다.이 함수 super.resize는 지정된 인수에 의해 높이 를 호출 한 다음 크기를 조정합니다.

프로그래밍 관점에서 보면 사각형에는 사각형이 없습니다. 사각형을 사각형의 하위 클래스로 만드는 것은 의미가 없습니다.


+1 정사각형은 수학에서 특별한 종류의 rect라고해서 OO에서 동일하다는 것을 의미하지는 않습니다.
Lovis

1
사각형은 사각형이고 사각형은 사각형입니다. 그들 사이의 관계는 모델링에서도 유지되어야합니다. 그렇지 않으면 모델이 매우 좋지 않습니다. 실제 문제는 다음과 같습니다. 1) 변경 가능하게하면 더 이상 사각형과 사각형을 모델링하지 않습니다. 2) 어떤 종류의 "a"관계가 두 종류의 물체 사이에 존재한다고 가정 할 때, 무차별 적으로 하나를 다른 것으로 대체 할 수 있습니다.
Doval

4

LSP에 의해, 둘 사이의 상속 관계를 생성하고 재정의 setWidth하고 setHeight사각형이 동일하게 만들기 때문에 혼란스럽고 직관적이지 않은 동작이 발생합니다. 코드가 있다고 가정 해 봅시다.

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

그러나 메소드가 createRectangle리턴 Square되면 Squarefrom 을 상속 함으로써 가능하기 때문입니다 Rectange. 그러면 기대가 깨집니다. 이 코드에서는 너비 또는 높이를 설정하면 너비 또는 높이 만 변경 될 것으로 예상합니다. OOP의 요점은 수퍼 클래스로 작업 할 때 하위 클래스에 대한 지식이 전혀 없다는 것입니다. 그리고 서브 클래스가 슈퍼 클래스에 대한 기대치에 맞지 않도록 동작을 변경하면 버그가 발생할 가능성이 높습니다. 그리고 이런 종류의 버그는 디버깅 및 수정이 어렵습니다.

OOP에 대한 주요 아이디어 중 하나는 상속 된 데이터가 아니라 행동이라는 것입니다 (이는 주요 오해 중 하나이기도 함). 정사각형과 직사각형을 보면 상속 관계와 관련이있는 행동 자체가 없습니다.


2

LSP가 말하는 것은 상속받은 것은 Rectangle반드시이어야 한다는 것입니다 Rectangle. 즉, 무엇을 하든지해야합니다 Rectangle.

아마도에 대한 문서 RectangleRectangle명명 된 의 동작이 r다음과 같다고 쓰여졌습니다 .

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

Square의 동작이 동일하지 않으면처럼 동작하지 않습니다 Rectangle. 그래서 LSP는 그것을 상속해서는 안된다고 말합니다 Rectangle. 이 규칙은 메서드 재정의에서 잘못된 일을 막을 수 없기 때문에이 규칙을 적용 할 수 없지만 "언어가 메서드를 재정의 할 수 있기 때문에 괜찮습니다"를 의미하는 것은 아닙니다.

지금, 될 에 대한 문서를 작성하는 Rectangle위의 코드는 아마 당신은이 경우, 10을 인쇄된다는 사실을 의미하지는 않습니다 방식으로 Square이 될 수 있습니다 Rectangle. "이 작업은 X를 수행합니다. 또한이 클래스의 구현은 Y를 수행합니다"와 같은 문서를 볼 수 있습니다. 그렇다면 클래스에서 인터페이스를 추출하고 인터페이스가 보장하는 것과 클래스가 보장하는 것을 구별하는 좋은 사례가 있습니다. 그러나 사람들이 "변경 가능한 사각형은 변경 가능한 사각형이 아니라 변경 불가능한 사각형은 변경할 수없는 사각형"이라고 말할 때 기본적으로 위의 내용이 변경 가능한 사각형의 합리적인 정의의 일부라고 가정합니다.


이것은 단지 5 시간 전에 게시
gnat

@ gnat : 다른 대답을 대략적으로 간결하게 편집하고 싶습니까? ;-) 다른 답변자가 질문에 대답하는 데 필요하다고 생각하지만 그렇지 않다고 생각하는 점을 제거하지 않고는 할 수 없다고 생각합니다.
Steve Jessop


1

하위 유형과 확장 적으로 OO 프로그래밍은 종종 Liskov 대체 원칙에 의존하기 때문에 A <= B 인 경우 B가 필요한 경우 A 유형의 모든 값을 사용할 수 있습니다. 이는 OO 아키텍처의 기본 원칙입니다. 모든 서브 클래스에이 속성이있을 것으로 가정합니다 (그렇지 않은 경우 서브 타입은 버그가 있으며 수정해야 함).

그러나이 원칙은 대부분의 코드를 비현실적이거나 표현하지 않았거나 실제로는 사소한 경우를 충족시키는 것이 불가능하다는 것이 밝혀졌습니다! 사각형-사각형 문제 또는 원형 타원 문제 ( http://en.wikipedia.org/wiki/Circle-ellipse_problem ) 로 알려진이 문제 는 처리하기가 얼마나 어려운지를 보여주는 유명한 예입니다.

우리 관찰 적으로 동등한 정사각형과 직사각형을 구현할 있지만 구별이 쓸모 없을 때까지 점점 더 많은 기능을 버려야 만합니다.

예를 들어 http://okmij.org/ftp/Computation/Subtyping/을 참조하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.