Liskov 대체 원칙을 위반하면 무엇이 잘못 될 수 있습니까?


27

나는 Liskov 대체 원칙에 대한 위반 가능성에 대해이 매우 투표가 많은 질문 을 따랐습니다. 나는 Liskov 대체 원칙이 무엇인지 알고 있지만, 여전히 내 마음에 분명하지 않은 것은 개발자가 객체 지향 코드를 작성하는 동안 원칙에 대해 생각하지 않으면 잘못 될 수 있다는 것입니다.


6
LSP를 따르지 않으면 무엇이 잘못 될 수 있습니까? 최악의 시나리오 : Code-thulhu를 소환합니다! ;)
FrustratedWithFormsDesigner

1
원래 질문의 저자로서 나는 그것이 꽤 학문적 인 질문이라고 덧붙여 야합니다. 위반으로 인해 코드에 오류가 발생할 수 있지만 LSP 위반으로 내려갈 수있는 심각한 버그 또는 유지 관리 문제가 없었습니다.
Paul T Davies

2
@Paul 그래서 기본 클래스의 목적에 대해 확신이없는 사람들에 의해 계약이 왼쪽과 오른쪽으로 깨진 복잡한 OO 계층 구조 (당신이 직접 디자인하지 않았지만 확장해야 했음)로 인해 프로그램에 아무런 문제가 없었습니다. 우선 첫째로? 부럽다! :)
Andres F.

@PaulT 결과의 심각도는 사용자 (라이브러리를 사용하는 프로그래머)가 라이브러리 구현에 대한 자세한 지식을 가지고 있는지 (즉, 라이브러리 코드에 액세스하고 익숙한 지 여부)에 따라 달라집니다. 결국 사용자는 수십 개의 조건부 검사를 작성하거나 래퍼를 작성합니다. 비 LSP (클래스 별 동작)를 설명하기 위해 라이브러리 주변. 라이브러리가 비공개 소스 상용 제품인 경우 최악의 시나리오가 발생합니다.
rwong

@Andres와 rwong은 그 문제에 대한 답을 보여주십시오. 받아 들여진 대답은 Paul Davies를 거의 지원합니다. 결과는 좋은 컴파일러, 정적 분석기 또는 최소 단위 테스트가있는 경우 결과가 신속하게 발견되고 수정되는 사소한 것입니다 (예외).
user949300 2016 년

답변:


31

나는 그 질문에 아주 잘 언급되어 있다고 생각합니다.

이제 Task에서 Close ()를 호출하면 시작 상태의 ProjectTask 인 경우 기본 Task 인 경우 호출이 실패 할 가능성이 있습니다.

당신이 할 것이라고 상상해보십시오 :

public void ProcessTaskAndClose(Task taskToProcess)
{
    taskToProcess.Execute();
    taskToProcess.DateProcessed = DateTime.Now;
    taskToProcess.Close();
}

이 메소드에서 때때로 .Close () 호출이 발생하기 때문에 파생 유형의 구체적인 구현을 기반으로 Task에 하위 유형이없는 경우이 메소드가 작성되는 방식에서이 메소드의 동작 방식을 변경해야합니다. 이 방법을 전달했습니다.

liskov 대체 위반으로 인해 형식을 사용하는 코드는 파생 형식의 내부 작업에 대해 명시 적으로 알고 있어야이를 다르게 처리 할 수 ​​있습니다. 이것은 코드를 밀접하게 결합하며 일반적으로 구현을 일관되게 사용하기 어렵게 만듭니다.


그것은 자식 클래스가 부모 클래스에서 선언되지 않은 자신의 공개 메소드를 가질 수 없다는 것을 의미합니까?
Songo

@Songo : 반드시 그런 것은 아니지만, 기본 포인터 (또는 참조 또는 변수 또는 사용하는 언어)에서 해당 메소드에 "도달 할 수 없음"개체의 유형을 쿼리하려면 런타임 유형 정보가 필요합니다. 해당 함수를 호출하기 전에. 그러나 이것은 언어 구문 및 의미와 밀접한 관련이 있습니다.
Emilio Garavaglia

2
아니요. 이것은 자식 클래스가 부모 클래스의 유형 인 것처럼 참조되는 경우에 해당하며,이 경우 부모 클래스에서 선언되지 않은 멤버는 액세스 할 수 없습니다.
Chewy Gumball

1
@ 필 예; 이것은 밀접한 결합의 정의입니다. 한 가지를 바꾸면 다른 것이 바뀝니다. 느슨하게 연결된 클래스는 코드를 변경하지 않고도 구현을 변경할 수 있습니다. 이것이 계약이 좋은 이유이며, 고객의 소비자에 대한 변경을 요구하지 않는 방법을 안내합니다. 계약을 충족하면 소비자는 수정이 필요하지 않으므로 느슨한 결합이 이루어집니다. 소비자가 계약이 아닌 구현에 코드를 작성해야하는 경우 이는 밀접한 결합이며 LSP를 위반할 때 필요합니다.
Jimmy Hoffa

1
@ user949300 소프트웨어를 성공적으로 수행하는 것이 소프트웨어의 품질, 장기 또는 단기 비용을 측정하는 것은 아닙니다. 설계 원칙은 소프트웨어를 "작업"으로 만들지 않고 소프트웨어의 장기 비용을 줄이기위한 지침을 제시하려는 시도입니다. 사람들은 여전히 ​​작동하는 솔루션을 구현하지 못한 채 원하는 모든 원칙을 따르거나 작동하지 않는 솔루션을 구현할 수 있습니다. Java 콜렉션은 많은 사람들에게 효과적 일 수 있지만, 장기적으로 협업하는 데 드는 비용이 저렴하다는 것을 의미하지는 않습니다.
Jimmy Hoffa

13

기본 클래스에 정의 된 계약을 이행하지 않으면 결과가 꺼져있을 때 자동으로 실패 할 수 있습니다.

Wikipedia State의 LSP

  • 하위 유형에서는 전제 조건을 강화할 수 없습니다.
  • 하위 유형에서는 사후 조건을 약화시킬 수 없습니다.
  • 상위 유형의 변형은 하위 유형으로 유지되어야합니다.

이 중 하나라도 보유하지 않으면 발신자는 예상치 못한 결과를 얻을 수 있습니다.


1
이것을 보여주는 구체적인 예를 생각할 수 있습니까?
Mark Booth

1
@MarkBooth 원형 타원 / 사각 사각형 문제가이를 설명하는 데 유용 할 수 있습니다. wikipedia 기사는 시작하기에 좋은 곳입니다. en.wikipedia.org/wiki/Circle-ellipse_problem
Ed Hastings

7

인터뷰 질문의 연대기에서 고전적인 경우를 고려하십시오. Ellipse에서 Circle을 파생했습니다. 왜? 물론 원은 타원이기 때문에!

타원에는 두 가지 기능이 있습니다.

Ellipse.set_alpha_radius(d)
Ellipse.set_beta_radius(d)

분명히 원의 반지름이 균일하기 때문에 원에 대해 다시 정의해야합니다. 두 가지 가능성이 있습니다.

  1. set_alpha_radius 또는 set_beta_radius를 호출 한 후 둘 다 같은 양으로 설정됩니다.
  2. set_alpha_radius 또는 set_beta_radius를 호출하면 객체가 더 이상 Circle이 아닙니다.

대부분의 OO 언어는 두 번째 언어를 지원하지 않으며 정당한 이유가 있습니다. Circle이 더 이상 Circle이 아니라는 것은 놀라운 일입니다. 첫 번째 옵션이 가장 좋습니다. 그러나 다음 기능을 고려하십시오.

some_function(Ellipse byref e)

some_function이 e.set_alpha_radius를 호출한다고 상상해보십시오. 그러나 e는 실제로 Circle이기 때문에 놀랍게도 베타 반경도 설정되어 있습니다.

그리고 여기에는 대체 원칙이 있습니다 : 서브 클래스는 수퍼 클래스를 대신 할 수 있어야합니다. 그렇지 않으면 놀라운 일이 발생합니다.


1
가변 객체를 사용하면 문제가 발생할 수 있다고 생각합니다. 원도 타원입니다. 그러나 원이기도 한 타원을 다른 타원 (세터 방법을 사용하여 수행하는 작업)으로 바꾸면 새 타원도 원이 될 것이라는 보장이 없습니다 (원은 타원의 적절한 하위 집합 임).
Giorgio

2
순전히 기능적인 세계 (불변의 객체가있는)에서 set_alpha_radius (d) 메소드는 리턴 유형 타원 (타원과 원 클래스 모두)을 갖습니다.
Giorgio

@Giorgio 네,이 문제는 가변 객체에서만 발생한다고 언급 했어야합니다.
Kaz Dragon

@KazDragon : 타원이 원이 아니라는 것을 알면 왜 타원을 원 개체로 대체합니까? 누군가 그렇게하면 모델링하려는 엔터티를 올바르게 이해하지 못합니다. 그러나 이러한 대체를 허용함으로써 소프트웨어에서 모델링하려는 기본 시스템에 대한 이해를 돕지 않아서 잘못된 소프트웨어를 만드는 것이 아닙니까?
maverick

@maverick 나는 당신이 거꾸로 설명한 관계를 읽었다 고 생각합니다. 제안 된 is-a 관계는 다른 방법입니다. 원은 타원입니다. 구체적으로, 원은 알파 및 베타 반경이 동일한 타원입니다. 따라서 타원을 매개 변수로 사용하는 함수는 모두 원을 가질 수 있습니다. calculate_area (Ellipse)를 고려하십시오. 그것에 원을 전달하면 동일한 결과를 얻을 수 있습니다. 그러나 문제는 Ellipse의 돌연변이 기능의 행동이 Circle의 사람들에게는 대체 할 수 없다는 것입니다.
Kaz Dragon

6

평신도의 말로 :

코드에는 수많은 CASE / 스위치 절이 있습니다.

이러한 CASE / 스위치 조항 중 하나는 때때로 새로운 사례를 추가해야합니다. 즉, 코드 기반은 확장 성 및 유지 관리가 쉽지 않습니다.

LSP를 사용하면 코드가 하드웨어처럼 작동 할 수 있습니다.

기존의 외부 스피커와 새로운 외부 스피커가 동일한 인터페이스를 사용하기 때문에 iPod이 원하는 기능을 잃지 않으면 서 서로 교환 할 수 있기 때문에 새로운 외부 스피커 쌍을 구입했기 때문에 iPod을 수정할 필요가 없습니다.


2
-1 : 나쁜 답변 주위
Thomas Eding

3
@Thomas 나는 동의하지 않습니다. 좋은 비유입니다. 그는 LSP가 기대하는 것을 깨뜨리지 않는 것에 대해 이야기합니다. (케이스 / 스위치에 관한 부분은 약간 약하지만 동의합니다)
Andres F.

2
그리고 나서 애플은 커넥터를 바꾸어 LSP를 깨뜨렸다. 이 대답은 계속됩니다.
Magus

LSP와 관련하여 switch 문과 관련이 없습니다. typeof(someObject)"허용 된"것을 결정하기 위해 전환하는 것을 언급한다면 , 확실히, 그것은 또 다른 반 패턴입니다.
sara

스위치 설명의 양이 크게 줄어드는 것은 LSP의 바람직한 부작용입니다. 객체는 동일한 인터페이스를 확장하는 다른 객체를 나타낼 수 있으므로 특별한 경우를 처리 할 필요가 없습니다.
Tulains Córdova

1

java의 UndoManager 로 실제 예제를 제공합니다.

그것은 상속에서 AbstractUndoableEdit그 계약 지정은이 개 상태를 가지고 (실행 취소 및 재실행) 및 싱글 호출로 그들 사이에 갈 수 undo()redo()

그러나 UndoManager에는 더 많은 상태가 있으며 실행 취소 버퍼처럼 작동합니다 (각 호출은 undo일부 편집 을 취소 하지만 모든 조건 을 취소 하여 사후 조건을 약화시킵니다)

이로 인해 호출하기 전에 UndoManager를 CompoundEdit에 추가하고 end()해당 CompoundEdit에서 undo를 호출하면 undo()편집이 부분적으로 취소 된 상태로 남아있는 각 편집 을 호출하게 됩니다.

나는 그것을 UndoManager피하기 위해 내 자신 을 굴렸다 (아마도 이름을 바꿔야한다 UndoBuffer)


1

예 : UI 프레임 워크를 사용하고 있으며 Control기본 클래스 를 서브 클래 싱하여 고유 한 사용자 정의 UI 제어를 작성합니다 . Control기본 클래스는 방법을 정의 한다 중첩 컨트롤 컬렉션 (있는 경우)를 리턴한다. 그러나 실제로 미국 대통령의 생년월일 목록을 반환하는 방법을 재정의합니다.getSubControls()

그렇다면 무엇이 잘못 될 수 있습니까? 예상대로 컨트롤 목록을 반환하지 않기 때문에 컨트롤 렌더링에 실패합니다. UI가 충돌 할 가능성이 높습니다. 당신은하는 계약 위반 컨트롤의 서브 클래스가 준수 할 것으로 예상된다.


0

모델링 관점에서 볼 수도 있습니다. 클래스의 인스턴스가 클래스의 인스턴스라고 말할 때 " A클래스의 인스턴스의 B관찰 가능한 동작은 클래스의 인스턴스의 A관찰 가능한 동작으로 분류 될 수도 있습니다 B"(클래스 B가 덜 구체적 일 경우에만 가능함) 클래스 A.)

따라서 LSP를 위반한다는 것은 디자인에 모순이 있음을 의미합니다. 객체에 대한 일부 범주를 정의한 다음 구현에서 해당 범주를 존중하지 않으면 무언가 잘못되어야합니다.

"이 상자에는 파란 공만 들어 있습니다"라는 태그가있는 상자를 만든 다음 빨간 공을 던지는 것과 같습니다. 잘못된 정보가 표시되는 경우 이러한 태그는 어떻게 사용됩니까?


0

최근에 주요 Liskov 위반자가있는 코드베이스를 상속했습니다. 중요한 수업에서. 이로 인해 엄청난 고통이 생겼습니다. 이유를 설명하겠습니다.

나는 Class A에서 유래했다 Class B. Class AClass B특성의 무리 공유 Class A자신의 구현을 재정의. 설정 또는 점점 Class A재산은 설정하거나에서 동일한 속성을 얻기에 다른 효과가 있습니다 Class B.

public Class A
{
    public virtual string Name
    {
        get; set;
    }
}

Class B : A
{
    public override string Name
    {
        get
        {
            return TranslateName(base.Name);
        }
        set
        {
            base.Name = value;
            FunctionWithSideEffects();
        }
    }
}

이것이 .NET에서 번역을 수행하는 완전히 끔찍한 방법이라는 사실을 제외하면이 코드에는 다른 많은 문제가 있습니다.

이 경우 Name여러 곳에서 색인 및 흐름 제어 변수로 사용됩니다. 위의 클래스는 코드베이스 전체에서 원시 및 파생 형식으로 흩어져 있습니다. 이 경우 Liskov 대체 원칙을 위반하면 기본 클래스를 사용하는 각 함수에 대한 모든 단일 호출 컨텍스트를 알아야합니다.

코드 사용은 모두의 객체 Class AClass B나는 간단하게 만들 수 있으므로, Class A사용하는 사람들을 강제로 추상 Class B.

작동하는 매우 유용한 유틸리티 기능과 작동하는 Class A매우 유용한 유틸리티 기능이 있습니다 Class B. 이상적으로는에 작동 할 수있는 유틸리티 기능을 사용할 수 있도록하고 싶습니다 Class A에를 Class B. LSP를 위반하지 않는 경우 많은 기능을 Class B쉽게 수행 할 수 있습니다 Class A.

이것에 대한 최악의 점은 전체 응용 프로그램 이이 두 클래스에 달려 있고 항상 두 클래스 모두에서 작동하며이를 변경하면 백 가지 방식으로 중단 되므로이 특정 사례는 실제로 리팩토링하기가 어렵다는 것입니다. 어쨌든).

이 문제를 해결하기 위해해야 ​​할 일은 NameTranslated속성을 만드는 것 Class B입니다. Name속성 의 버전이 되고 파생 Name속성에 대한 모든 참조를 새 NameTranslated속성 을 사용 하도록 매우 신중하게 변경해야 합니다. 그러나 이러한 참조 중 하나라도 잘못하면 전체 응용 프로그램이 손상 될 수 있습니다.

코드베이스에 단위 테스트가 없기 때문에 개발자가 직면 할 수있는 가장 위험한 시나리오에 가깝습니다. 위반을 변경하지 않으면 각 방법에서 어떤 유형의 물체가 작동하는지 추적하는 데 많은 양의 정신 에너지를 소비해야하며 위반을 해결하면 전체 제품이 부적절하게 폭발 할 수 있습니다.


같은 이름 [예 : 중첩 클래스]를 만들어 새로운 식별자 한 것은 다른 종류의 상속 재산을 그림자 파생 클래스 내에서 어떤 일이 일어날 것입니다 BaseNameTranslatedName액세스 클래스-A 스타일 모두 Name와 클래스 B의 의미를? 그러면 Name변수 유형에 대한 액세스 시도가 B컴파일러 오류와 함께 거부되므로 모든 참조가 다른 형식 중 하나로 변환되었는지 확인할 수 있습니다.
supercat

나는 더 이상 그 장소에서 일하지 않습니다. 수정하기가 매우 어색했을 것입니다. :-)
Stephen

-4

LSP를 위반하는 문제를 느끼고 싶다면 기본 클래스 (소스 코드 없음)의 .dll / .jar 만 있고 새로운 파생 클래스를 만들어야하는 경우 어떻게 될지 생각하십시오. 이 작업을 완료 할 수 없습니다.


1
이것은 답변이 아닌 더 많은 질문을 열어줍니다.
Frank
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.