Liskov 대체 원칙의 예는 무엇입니까?


908

Liskov 대체 원칙 (LSP)이 객체 지향 디자인의 기본 원칙이라고 들었습니다. 그것은 무엇이며 그 사용의 예는 무엇입니까?


LSP의 준수 및 위반의 더 많은 예제 여기
StuartLC

1
이 질문은 무한히 많은 정답을 가지고 있으며 너무 광범위 합니다.
Raedwald

답변:


892

LSP (Univer Bob이 최근에 들었던 포드 캐스트에서 제공 한)를 보여주는 훌륭한 예는 자연 언어로 들리는 것이 코드에서 제대로 작동하지 않는 경우입니다.

수학에서 a Square는입니다 Rectangle. 실제로 그것은 사각형의 전문화입니다. "is a"는 상속으로 이것을 모델링하려고합니다. 그러나 코드에서 만든 코드 Square에서 Rectanglea Square를 사용하면 어디에서나 사용할 수 있습니다 Rectangle. 이것은 이상한 행동을합니다.

당신이 가지고 상상 SetWidth하고 SetHeight온 방법 Rectangle기본 클래스; 이것은 완벽하게 논리적 인 것 같습니다. 귀하의 경우 Rectangle참조가 지적 Square, 다음 SetWidthSetHeight하나를 설정하면 그것을 일치 다른 변화 때문에 이해가되지 않습니다. 이 경우 SquareLiskov 대체 테스트에 실패하고 상속 RectangleSquare받는 추상화 Rectangle는 좋지 않습니다.

여기에 이미지 설명을 입력하십시오

모두 귀중한 다른 SOLID 원칙 동기 부여 포스터를 확인하십시오 .


19
@ m-sharp SetWidth와 SetHeight 대신 불변의 사각형이라면 GetWidth와 GetHeight 메소드를 대신 사용할 수 있습니까?
Pacerier

139
이야기의 도덕 : 속성이 아닌 행동을 기반으로 수업을 모델링하십시오. 행동이 아닌 속성을 기반으로 데이터를 모델링합니다. 오리처럼 행동한다면 분명히 새입니다.
Sklivvz

193
정사각형은 현실 세계에서 사각형의 유형입니다. 코드에서이를 모델링 할 수 있는지 여부는 사양에 따라 다릅니다. LSP가 나타내는 것은 하위 유형 동작이 기본 유형 사양에 정의 된 기본 유형 동작과 일치해야한다는 것입니다. 사각형 기본 유형 사양에 높이와 너비를 독립적으로 설정할 수 있다고 표시되면 LSP는 사각형이 하위 유형의 사각형이 될 수 없다고 말합니다. 사각형 사양에 사각형을 변경할 수 없다고 표시되면 사각형은 사각형의 하위 유형이 될 수 있습니다. 기본 유형에 지정된 동작을 유지 관리하는 하위 유형에 관한 것입니다.
SteveT

63
@Pacerier 불변의 경우 아무런 문제가 없습니다. 여기서 실제 문제는 사각형을 모델링하는 것이 아니라 오히려 "예상 가능한 사각형", 즉 생성 후 너비 나 높이를 수정할 수있는 사각형 (그리고 여전히 동일한 객체라고 생각하는 사각형)이라는 것입니다. 이런 식으로 사각형 클래스를 보면 사각형은 "모두 가능한 사각형"이 아닙니다. 사각형은 모양을 바꿀 수없고 여전히 사각형입니다 (일반적으로). 수학적으로는 가변성이 수학적 맥락에서 의미가 없기 때문에 문제가 보이지 않습니다.
asmeurer

14
나는 그 원칙에 대해 하나의 질문이있다. 다음 Square.setWidth(int width)과 같이 구현 된 경우 왜 문제가 발생 this.width = width; this.height = width;합니까? 이 경우 너비는 높이와 동일합니다.
MC 황제

488

Liskov 대체 원리 (LSP, )는 객체 지향 프로그래밍의 개념으로 다음과 같은 내용을 나타냅니다.

기본 클래스에 대한 포인터 또는 참조를 사용하는 함수는 몰래 파생 클래스의 객체를 사용할 수 있어야합니다.

LSP의 핵심은 인터페이스와 계약뿐만 아니라 수업을 연장 할시기를 결정하는 방법과 목표를 달성하기 위해 작곡과 같은 다른 전략을 사용하는 방법에 관한 것입니다.

이 점을 설명하는 가장 효과적인 방법은 Head First OOA & D 였습니다. 전략 게임을위한 프레임 워크를 구축하는 프로젝트의 개발자 인 시나리오를 제시합니다.

그들은 다음과 같은 보드를 나타내는 클래스를 제시합니다.

클래스 다이어그램

모든 메소드는 X 및 Y 좌표를 매개 변수로 사용하여의 2 차원 배열에서 타일 위치를 찾습니다 Tiles. 이를 통해 게임 개발자는 게임 도중 보드의 유닛을 관리 할 수 ​​있습니다.

이 책은 게임 프레임 작업이 비행중인 게임을 수용하기 위해 3D 게임 보드도 지원해야한다는 요구 사항을 변경합니다. 그래서 ThreeDBoard확장 하는 클래스가 도입되었습니다 Board.

언뜻보기에 이것은 좋은 결정처럼 보입니다. Board제공 양 HeightWidth성질과 ThreeDBoardZ 축을 제공한다.

고장난 곳은에서 상속받은 다른 모든 멤버를 볼 때입니다 Board. 의 방법은 AddUnit, GetTile, GetUnits등, 모두에서 X 및 Y 매개 변수를 모두 가지고 Board클래스 만이 ThreeDBoard아니라 Z 매개 변수를 필요로한다.

따라서 Z 매개 변수를 사용하여 해당 메소드를 다시 구현해야합니다. Z 매개 변수는 Board클래스에 대한 컨텍스트가 없으며 클래스에서 상속 된 메서드는 Board의미를 잃습니다. ThreeDBoard클래스를 기본 클래스로 사용하려는 코드 단위 Board는 운이 좋지 않습니다.

다른 접근법을 찾아야 할 수도 있습니다. 대신 연장으로 Board, ThreeDBoard구성된되어야 Board개체. BoardZ 축 단위당 하나의 객체.

이를 통해 캡슐화 및 재사용과 같은 우수한 객체 지향 원칙을 사용할 수 있으며 LSP를 위반하지 않습니다.


10
비슷하지만 간단한 예제는 Wikipedia의 Circle-Ellipse Problem 을 참조하십시오 .
Brian

@NotMySelf의 인용문 : "이 예제는 단순히 ThreeDBoard의 맥락에서 보드로부터 상속하는 것이 의미가 없으며 모든 메소드 서명이 Z 축으로는 의미가 없음을 보여주기위한 것이라고 생각합니다."
Contango

1
따라서 Child 클래스에 다른 메소드를 추가했지만 Parent의 모든 기능이 여전히 Child 클래스에서 의미가 있다면 LSP를 깨뜨릴 수 있습니까? 한편으로 우리는 Child를 조금만 사용하도록 인터페이스를 수정 한 반면 Child를 부모로 캐스팅하면 부모가 잘 작동 할 것으로 예상되는 코드가 수정됩니다.
Nickolay Kondratyev

5
이것은리스 코프 안티 예제입니다. Liskov는 사각형에서 사각형을 파생시킵니다. 더 적은 매개 변수 클래스에서 더 많은 매개 변수 클래스 그리고 당신은 그것이 나쁘다는 것을 멋지게 보여주었습니다. 답변으로 표시하고 liskov 질문에 대해 안티리스 코프 답변의 200 배를 공표 한 것은 정말 좋은 농담입니다. Liskov 원칙이 실제로 오류입니까?
Gangnus

3
상속이 잘못된 방식으로 작동하는 것을 보았습니다. 다음은 예입니다. 기본 클래스는 3DBoard 및 파생 클래스 보드 여야합니다. 보드는 여전히 Z 축이 Max (Z) = Min (Z) = 1입니다.
Paulustrious 14.52에

169

대체 가능성은 컴퓨터 프로그램에서 S가 T의 하위 유형 인 경우 T 유형의 객체를 S 유형의 객체로 대체 할 수 있다는 객체 지향 프로그래밍의 원칙입니다.

Java로 간단한 예를 보자.

나쁜 예

public class Bird{
    public void fly(){}
}
public class Duck extends Bird{}

오리는 새이기 때문에 날 수 있습니다.

public class Ostrich extends Bird{}

타조는 새이지만 날 수는 없지만 타조 클래스는 버드 클래스의 하위 유형이지만 플라이 방법을 사용할 수 없으므로 LSP 원칙을 위반한다는 의미입니다.

좋은 예

public class Bird{
}
public class FlyingBirds extends Bird{
    public void fly(){}
}
public class Duck extends FlyingBirds{}
public class Ostrich extends Bird{} 

3
좋은 예,하지만 클라이언트가있는 경우 당신은 어떻게 할 것인지 Bird bird. 플라이를 사용하려면 객체를 FlyingBirds로 캐스팅해야합니다.
무디

17
아니요. 클라이언트에있는 경우 Bird bird사용할 수 없습니다 fly(). 그게 다야. 를 전달해도이 Duck사실 은 변하지 않습니다. 클라이언트가를 가지고 있다면, FlyingBirds bird전달 되더라도 Duck항상 같은 방식으로 작동해야합니다.
Steve Chamaillard

9
이것이 인터페이스 분리에 대한 좋은 예가되지 않습니까?
Saharsh

뛰어난 예를 감사하는 사람 (남자)
Abdelhadi 아브

6
인터페이스 'Flyable'을 사용하는 것은 어떻습니까 (더 나은 이름은 생각할 수 없습니다). 이런 식으로 우리는이 엄격한 계층 구조에 전념하지 않습니다. 우리가 정말로 필요로하지 않는 한.
Thirdy

132

LSP는 불변에 관한 것입니다.

고전적인 예제는 다음 의사 코드 선언으로 구현됩니다 (구현 생략).

class Rectangle {
    int getHeight()
    void setHeight(int value)
    int getWidth()
    void setWidth(int value)
}

class Square : Rectangle { }

이제 인터페이스가 일치하지만 문제가 있습니다. 그 이유는 정사각형과 직사각형의 수학적 정의에서 비롯된 불변량을 위반했기 때문입니다. 게터와 세터가 작동하는 방식 Rectangle은 다음과 같은 불변을 충족시켜야합니다.

void invariant(Rectangle r) {
    r.setHeight(200)
    r.setWidth(100)
    assert(r.getHeight() == 200 and r.getWidth() == 100)
}

그러나이 불변 의 올바른 구현에 의해 위반 되어야Square 하므로의 올바른 대체가 아닙니다 Rectangle.


35
따라서 "OO"를 사용하여 실제로 모델링하려는 모든 것을 모델링하기가 어렵습니다.
DrPizza

9
@ DrPizza : 물론입니다. 그러나 두 가지가 있습니다. 첫째, 이러한 관계는 불완전하거나 더 복잡한 우회를 사용하여 OOP에서 모델링 할 수 있습니다 (문제에 적합한 것을 선택하십시오). 둘째, 더 나은 대안이 없습니다. 다른 매핑 / 모델링에는 동일하거나 유사한 문제가 있습니다. ;-)
Konrad Rudolph

7
@NickW 어떤 경우에는 (위의 설명이 아닌) 상속 체인을 논리적으로 뒤집을 수 있습니다. 논리적으로 말하면 2D 점은 3D 점입니다. 여기서 3 차원은 무시됩니다 (또는 0 – 모든 점이 동일한 평면에 있음) 3D 공간). 그러나 이것은 실제로 실용적이지 않습니다. 일반적으로 이것은 상속이 실제로 도움이되지 않는 경우 중 하나이며 엔티티간에 자연스러운 관계가 없습니다. 그것들을 별도로 모델링하십시오 (적어도 더 좋은 방법은 모르겠습니다).
Konrad Rudolph

7
OOP는 데이터가 아닌 동작을 모델링하기위한 것입니다. 클래스는 LSP를 위반하기 전에도 캡슐화를 위반합니다.
Sklivvz

2
@AustinWBryan Yep; 이 분야에서 더 오래 일할수록 인터페이스와 추상 기본 클래스에만 상속을 사용하고 나머지는 컴포지션을 더 많이 사용하는 경향이 있습니다. 때로는 약간 더 많은 작업 (현명한 타이핑)이지만 많은 문제를 피하고 다른 숙련 된 프로그래머가 널리 알리는 조언입니다.
Konrad Rudolph

77

Robert Martin은 Liskov 대체 원칙에 관한 훌륭한 논문을 보유하고 있습니다. 이 원칙을 위반할 수있는 미묘하고 미묘한 방법에 대해 설명합니다.

논문의 일부 관련 부분 (두 번째 예는 크게 요약되어 있음) :

LSP 위반의 간단한 예

이 원칙의 가장 눈에 띄는 위반 중 하나는 C ++ RTTI (Run-Time Type Information)를 사용하여 객체 유형에 따라 함수를 선택하는 것입니다. 즉 :

void DrawShape(const Shape& s)
{
  if (typeid(s) == typeid(Square))
    DrawSquare(static_cast<Square&>(s)); 
  else if (typeid(s) == typeid(Circle))
    DrawCircle(static_cast<Circle&>(s));
}

분명히 DrawShape기능이 잘못 형성되었습니다. Shape클래스 의 가능한 모든 파생물에 대해 알아야 하며 새로운 파생물 Shape이 작성 될 때마다 변경해야합니다 . 실제로 많은 사람들이이 기능의 구조를 객체 지향 설계에 대한 혐오로보고 있습니다.

정사각형과 직사각형, 더 미묘한 위반.

그러나 LSP를 위반하는 훨씬 더 미묘한 다른 방법이 있습니다. Rectangle아래에 설명 된대로 클래스 를 사용하는 응용 프로그램을 고려하십시오 .

class Rectangle
{
  public:
    void SetWidth(double w) {itsWidth=w;}
    void SetHeight(double h) {itsHeight=w;}
    double GetHeight() const {return itsHeight;}
    double GetWidth() const {return itsWidth;}
  private:
    double itsWidth;
    double itsHeight;
};

[...] 언젠가 사용자가 사각형 외에 사각형을 조작하는 기능을 요구한다고 상상해보십시오. [...]

분명히 사각형은 모든 일반적인 의도와 목적을위한 사각형입니다. ISA 관계가 유지되므로 Square 클래스를 파생 된 것으로 모델링하는 것이 논리적 입니다 Rectangle. [...]

SquareSetWidthSetHeight함수 를 상속합니다 . Square사각형의 너비와 높이가 동일하기 때문에 이러한 함수는에 적합 하지 않습니다. 이것은 디자인에 문제가 있다는 중요한 실마리가되어야합니다. 그러나 문제를 회피 할 수있는 방법이 있습니다. 우리는 무시 SetWidth하고 SetHeight[...]

그러나 다음 기능을 고려하십시오.

void f(Rectangle& r)
{
  r.SetWidth(32); // calls Rectangle::SetWidth
}

Square객체에 대한 참조를 이 함수에 전달 Square하면 높이가 변경되지 않기 때문에 객체가 손상됩니다. 이것은 LSP의 명백한 위반입니다. 이 함수는 인수의 파생물에는 작동하지 않습니다.

[...]


14
늦었지만 나는 이것이이 논문에서 흥미로운 인용이라고 생각했다. Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one. 만약 자녀 클래스 전제 조건이 부모 클래스 전제 조건보다 강하다면, 당신은 전제 조건을 위반하지 않고 부모를 자녀로 대체 할 수 없었다. 따라서 LSP.
user2023861

@ user2023861 당신은 완벽하게 맞습니다. 이것을 바탕으로 답을 쓰겠습니다.
inf3rno

40

LSP는 일부 코드가이 유형의 메소드를 호출 생각하는 경우 필요 T하고, 무의식적 유형의 메소드를 호출 할 수있다 S, S extends T(즉, S상속, 도출에서, 또는 상위 유형의 하위 유형입니다 T).

예를 들어, 이것은 type의 입력 매개 변수를 가진 함수가 type T의 인수 값으로 호출되는 경우에 발생합니다 S. 또는 유형 식별자에 유형 T값이 할당됩니다 S.

val id : T = new S() // id thinks it's a T, but is a S

LSP는 유형의 방법에 대한 기대 (즉, 불변)가 필요합니다 T(예를 들어 Rectangle), 유형의 방법 경우에 위반되지 않습니다 S(예 :가 Square) 대신이라고합니다.

val rect : Rectangle = new Square(5) // thinks it's a Rectangle, but is a Square
val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

불변의 필드를 가진 유형조차도 불변성을 가지고 있습니다. 예를 들어 불변의 사각형 세터는 치수가 독립적으로 수정 될 것으로 예상하지만 불변의 사각형 세터는 이러한 기대를 위반합니다.

class Rectangle( val width : Int, val height : Int )
{
   def setWidth( w : Int ) = new Rectangle(w, height)
   def setHeight( h : Int ) = new Rectangle(width, h)
}

class Square( val side : Int ) extends Rectangle(side, side)
{
   override def setWidth( s : Int ) = new Square(s)
   override def setHeight( s : Int ) = new Square(s)
}

LSP를 사용하려면 S부속 유형의 각 방법에 공변량 입력 매개 변수와 공변량 출력이 있어야합니다.

분산은 상속의 방향에 반대되는 Contravariant 수단은, 유형, 즉 Si, 서브 타입들 각각에있어서의 각 입력 변수의 S동일 또는이어야 수퍼 타입의 Ti수퍼 타입의 대응에있어서의 대응하는 입력 파라미터 T.

공분산은 분산이 상속의 동일한 방향에 있음을 의미합니다. 즉, So하위 유형의 각 방법 출력의 유형 은 수퍼 유형의 해당 방법의 해당 출력 유형 과 S동일하거나 하위 유형 이어야합니다 .ToT

호출자가 타입을 가지고 T있다고 생각하고 메소드를 호출한다고 생각하면 타입의 T인수를 제공 Ti하고 출력을 타입 에 할당하기 때문이다 To. 실제로의 해당 메소드를 호출하는 S경우 각 Ti입력 인수가 Si입력 매개 변수에 So지정되고 출력이 유형에 지정됩니다 To. 따라서에 Si반 변형이 아닌 경우 Ti하위 유형 Xi이 아닌 하위 유형 을에 Si할당 할 수 있습니다 Ti.

또한 유형 다형성 매개 변수 (예 : 제네릭)에 대한 정의 사이트 분산 주석이있는 언어 (예 : 스칼라 또는 실론)의 경우, 유형의 각 유형 매개 변수에 대한 분산 주석의 동일 또는 반대 방향은 반대 방향이거나 동일한 방향 T이어야합니다. 유형 매개 변수 유형이있는 모든 입력 매개 변수 또는 출력 (의 모든 메소드 중 ) 에 각각T

또한 기능 유형이있는 각 입력 매개 변수 또는 출력에 대해 필요한 분산 방향이 반대입니다. 이 규칙은 재귀 적으로 적용됩니다.


불변량을 열거 할 수있는 경우 서브 타이핑이 적합 합니다.

불변량을 모델링하는 방법에 대한 많은 연구가 진행되어 컴파일러에 의해 시행됩니다.

Typestate (3 페이지 참조)는 type에 직교하는 상태 불변을 선언하고 시행합니다. 또는 어설 션을 유형 으로 변환하여 변형을 적용 할 수 있습니다 . 예를 들어 파일을 닫기 전에 파일이 열려 있다고 확인하기 위해 File.open ()은 File에서 사용할 수없는 close () 메서드가 포함 된 OpenFile 형식을 반환 할 수 있습니다. 틱택 토 API는 컴파일시 불변을 적용 할 입력을 사용하는 또 다른 예가 될 수있다. 타입 시스템은 심지어 튜링 완료 (예 : Scala) 일 수도 있습니다 . 의존적으로 타이핑 된 언어와 정리는 고차 타이핑 모델을 공식화합니다.

확장을 통해 의미론을 추상화 해야 할 필요가 있기 때문에 , 불변량을 모델링하기 위해 타이핑을 사용하는 것, 즉 통합 고차원의 의미 론적 의미론이 Typestate보다 우수 할 것으로 예상합니다. '확장'은 조정되지 않은 모듈 식 개발의 무한한 순열 구성을 의미합니다. 통일의 반설이자 자유도 인 것처럼 보이므로 공유 의미론을 표현하기 위해 서로 의존하는 두 가지 모델 (예 : 유형 및 유형 상태)을 갖는 것은 확장 가능한 구성을 위해 서로 통합 할 수 없습니다. . 예를 들어, 서브 타이핑, 함수 오버로딩 및 파라 메트릭 타이핑 도메인에서 Expression Problem 유사 확장이 통합되었습니다.

저의 이론적 입장은 지식이 존재 하기 위해 ( "중앙화는 맹목적이고 부적합합니다"섹션을 참조하십시오), 튜링-완전 컴퓨터 언어로 모든 가능한 불변량을 100 % 적용 할 수있는 일반적인 모델 은 결코 없을 것 입니다. 지식이 존재하기 위해서는 예상치 못한 가능성, 즉 무질서와 엔트로피가 항상 증가해야합니다. 이것은 엔트로피 힘입니다. 잠재적 인 확장의 가능한 모든 계산을 증명하려면 가능한 모든 확장을 우선적으로 계산해야합니다.

이것이 Halting Theorem이 존재하는 이유입니다. 즉 Turing-complete 프로그래밍 언어에서 가능한 모든 프로그램이 종료되는지 여부를 결정할 수 없습니다. 일부 특정 프로그램 (모든 가능성이 정의되고 계산 된 프로그램)이 종료되었음을 증명할 수 있습니다. 그러나 해당 프로그램의 확장 가능성이 완전하지 않은 경우 (예 : 의존형 입력을 통해)가 아니라면 해당 프로그램의 가능한 모든 확장이 종료되었음을 증명할 수 없습니다. Turing-completeness의 기본 요구 사항은 무한 재귀 이므로 Gödel의 불완전 성 이론과 Russell의 역설이 확장에 어떻게 적용되는지 이해하는 것이 직관적입니다.

이 정리에 대한 해석은 엔트로피 힘에 대한 일반화 된 개념 이해에 이들을 통합합니다.

  • 고델의 불완전 성 이론 : 모든 산술적 진실이 입증 될 수있는 공식적인 이론은 일관성이 없다.
  • Russell의 역설 : 세트를 포함 할 수있는 세트의 모든 멤버십 규칙은 각 멤버의 특정 유형을 열거하거나 자체를 포함합니다. 따라서 세트를 확장 할 수 없거나 무한한 재귀입니다. 예를 들어, 찻 주전자가 아닌 모든 것이 포함됩니다. 따라서 규칙이 집합을 포함하고 특정 유형을 열거하지 않고 (즉, 모든 지정되지 않은 유형을 허용 함) 무제한 확장을 허용하지 않는 경우 규칙이 일치하지 않습니다. 이것은 자신의 구성원이 아닌 세트의 집합입니다. 가능한 모든 확장에 대해 일관되고 완전하게 열거 할 수없는 이러한 고델의 불완전 성 이론은 고델의 불완전 성 이론이다.
  • Liskov Substition Principle : 일반적으로 어떤 집합이 다른 집합의 부분 집합인지, 즉 상속이 일반적으로 결정 불가능한지는 결정 불가능한 문제입니다.
  • Linsky Referencing : 무언가의 계산이 묘사되거나 인식 될 때, 즉 지각 (실제)이 절대적인 참조 지점을 갖지 않는 것은 무엇인지 계산할 수 없습니다.
  • Coase 's 정리 : 외부 기준점이 없기 때문에 무한한 외부 가능성에 대한 장벽은 실패합니다.
  • 열역학 제 2 법칙 : 전체 우주 (폐쇄 된 시스템, 즉 모든 것)는 최대 장애, 즉 최대 독립 가능성으로 경향이있다.

17
@ 쉘비 : 당신은 너무 많은 것들을 혼합했습니다. 당신이 말한 것만 큼 혼란스럽지 않습니다. 많은 이론적 주장은 '지식이 존재하기 위해 예상치 못한 가능성이 많이 존재한다 ...'와 같은 희미한 근거에 서있다. 상속은 일반적으로 결정 불가능하다. 이러한 각 지점에 대해 별도의 블로그를 시작할 수 있습니다. 어쨌든, 당신의 주장과 가정은 의문의 여지가 있습니다. 자신이 모르는 것을 사용해서는 안됩니다!
aknon

1
@aknon 이 문제에 대해 더 자세히 설명 하는 블로그 가 있습니다. 무한한 시공간에 대한 나의 TOE 모델은 무한한 주파수입니다. 재귀 유도 함수가 끝 값이 무한한 알려진 시작 값을 가지고 있거나 일치 함수가 끝 값을 알지 못하고 시작 경계가 알려진 것은 혼란스럽지 않습니다. 재귀가 도입되면 상대성이 문제입니다. 이것이 Turing complete가 무한 재귀와 같은 이유 입니다.
쉘비 무어 III

4
@ShelbyMooreIII 당신은 너무 많은 방향으로 가고 있습니다. 이것은 답이 아닙니다.
Soldalma

1
@Soldalma 그것은 답입니다. 답변 섹션에 표시되지 않습니다. 귀하의 의견은 의견 섹션에 있으므로 귀하의 의견입니다.
Shelby Moore III

1
스칼라 세계와의 혼합처럼!
Ehsan M. Kermani 2016 년

24

모든 답변에 사각형과 사각형이 있으며 LSP를 위반하는 방법이 있습니다.

LSP가 실제 예제와 어떻게 일치하는지 보여주고 싶습니다.

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return $result; 
    }
}

이 디자인은 우리가 사용하기로 선택한 구현에 관계없이 동작이 변경되지 않기 때문에 LSP를 준수합니다.

그리고 예, 다음과 같이 간단한 변경을 수행 하여이 구성에서 LSP를 위반 할 수 있습니다

<?php

interface Database 
{
    public function selectQuery(string $sql): array;
}

class SQLiteDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // sqlite specific code

        return $result;
    }
}

class MySQLDatabase implements Database
{
    public function selectQuery(string $sql): array
    {
        // mysql specific code

        return ['result' => $result]; // This violates LSP !
    }
}

이제 하위 유형은 더 이상 동일한 결과를 생성하지 않으므로 동일한 방식으로 사용할 수 없습니다.


6
모든 DB 엔진에서 Database::selectQuery지원하는 SQL의 하위 집합 만 지원 하도록 의미를 제한하는 한이 예제는 LSP를 위반하지 않습니다 . 그것은 실용적이지 않습니다 ... 즉, 예제는 여기에 사용 된 대부분의 다른 것보다 여전히 이해하기 쉽습니다.
Palec

5
나는이 대답이 나머지를 파악하는 것이 가장 쉽다는 것을 알았습니다.
Malcolm Salvador

23

Liskov를 위반했는지 여부를 확인하는 검사 목록이 있습니다.

  • 다음 항목 중 하나를 위반하면-> Liskov를 위반하게됩니다.
  • 위반하지 않으면-> 결론을 내릴 수 없습니다.

체크리스트 :

  • 파생 클래스에는 새로운 예외가 발생하지 않아야 합니다. 기본 클래스가 ArgumentNullException을 발생시킨 경우 하위 클래스는 ArgumentNullException 유형의 예외 또는 ArgumentNullException에서 파생 된 예외 만 처리 할 수 ​​있습니다. IndexOutOfRangeException을 던지는 것은 Liskov의 위반입니다.
  • 사전 조건을 강화할 수 없음 : 기본 클래스가 멤버 int와 작동한다고 가정하십시오. 이제 하위 유형은 int가 양수 여야합니다. 이것은 전제 조건이 강화되었으며 이제 음수 정수를 사용하기 전에 완벽하게 작동하는 모든 코드가 손상되었습니다.
  • 사후 조건을 약화시킬 수 없음 : 기본 클래스가 메소드를 리턴하기 전에 데이터베이스에 대한 모든 연결을 닫아야한다고 가정하십시오. 하위 클래스에서는 해당 메서드를 무시하고 추가 재사용을 위해 연결을 열어 두었습니다. 해당 방법의 사후 조건을 약화 시켰습니다.
  • 불변은 보존되어야한다 : 충족하기 가장 어렵고 고통스러운 제약. 불변은 기본 클래스에 숨겨져 있으며이를 드러내는 유일한 방법은 기본 클래스의 코드를 읽는 것입니다. 기본적으로 메소드를 재정의 할 때 재정의 된 메소드가 실행 된 후에 변경 불가능한 것은 변경되지 않아야합니다. 내가 생각할 수있는 가장 좋은 것은 기본 클래스 에서이 불변 제약을 적용하는 것이지만 쉽지는 않습니다.
  • 기록 제약 : 메서드를 재정의하는 경우 기본 클래스에서 수정할 수없는 속성을 수정할 수 없습니다. 이 코드를 살펴보면 Name이 수정 불가능한 것으로 정의되어 있음을 알 수 있지만 (Subprivate) SubType은 리플렉션을 통해 수정할 수있는 새로운 방법을 소개합니다.

    public class SuperType
    {
        public string Name { get; private set; }
        public SuperType(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
    public class SubType : SuperType
    {
        public void ChangeName(string newName)
        {
            var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName);
        }
    }
    

: 항목이 다른 사람이있다 Contravariance 방법의 인수공분산 반환 형식의이 . 그러나 C # (나는 C # 개발자입니다)에서는 가능하지 않으므로 신경 쓰지 않습니다.

참고:


저는 C # 개발자이기도하며 Visual Studio 2010에서 .Net 4.0 프레임 워크를 사용하여 마지막 내용이 사실이 아니라고 말할 것입니다. 리턴 유형의 공분산은 인터페이스에서 정의 된 것보다 더 파생 된 리턴 유형을 허용합니다. 예 : 예 : IEnumerable <T> (T는 공변량) IEnumerator <T> (T는 공변량) IQueryable <T> (T는 공변량) IGrouping <TKey, TElement> (TKey 및 TElement는 공변량) IComparer <T> (T IEqualityComparer <T> (T는 불변) IComparable <T> (T는
불변

1
훌륭하고 집중적 인 답변 (원래 질문은 규칙 이상의 예에 관한 것이었지만).
Mike

22

LSP는 계약의 계약에 대한 규칙입니다. 기본 클래스가 계약을 충족시키는 경우 LSP 파생 클래스도 해당 계약을 충족해야합니다.

의사 파이썬에서

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

파생 객체에서 Foo를 호출 할 때마다 arg가 동일한 한 기본 객체에서 Foo를 호출하는 것과 동일한 결과를 제공하는 경우 LSP를 충족시킵니다.


9
그러나 ... 항상 동일한 행동을 취하면 파생 클래스를 갖는 요점은 무엇입니까?
Leonid

2
당신은 요점을 놓쳤다 : 그것은 관찰 된 행동 과 동일하다 . 예를 들어, O (n) 성능을 가진 기능을 기능적으로 동등한 것으로 대체하지만 O (lg n) 성능을 가진 것을 대체 할 수 있습니다. 또는 MySQL로 구현 된 데이터에 액세스하는 것을 대체하고 인 메모리 데이터베이스로 대체 할 수 있습니다.
Charlie Martin

@Charlie Martin, 구현이 아닌 인터페이스로 코딩-나는 그것을 발굴합니다. 이것은 OOP에 고유하지 않습니다. Clojure와 같은 기능적 언어도이를 촉진합니다. Java 또는 C #의 관점에서도 추상적 클래스와 클래스 계층 구조를 사용하는 대신 인터페이스를 사용하는 것이 제공하는 예제에서 자연 스러울 것이라고 생각합니다. 파이썬은 강력하게 입력되지 않았으며 적어도 명시 적으로 인터페이스가 없습니다. 어려운 점은 SOLID를 준수하지 않고 몇 년 동안 OOP를 해왔다는 것입니다. 내가 그것을 만났으므로 제한적이고 거의 모순되는 것처럼 보입니다.
Hamish Grubijan

글쎄, 당신은 돌아가서 바바라의 원본을 확인해야합니다. reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps 인터페이스의 관점에서 실제로 언급 된 것은 아니며, 어떤 관계에서도 유지되는 (또는 그렇지 않은) 논리적 관계입니다. 상속 형태가있는 프로그래밍 언어.
Charlie Martin

1
@HamishGrubijan 나는 파이썬이 강하게 타이핑되지 않았다고 말했지만 누가 당신에게 거짓말을하고 있는지 알지 못합니다 (그리고 당신이 나를 믿지 않으면 파이썬 인터프리터를 실행하고 시도하십시오 2 + "2"). "강력한 유형의"를 "정적 유형의"과 혼동 할 수 있습니까?
asmeurer

21

부모 클래스를 확장 할 때 짧은 이야기,하자가, 실제적인 예를 들어 사각형의 직사각형 및 정사각형 사각형을두고, 당신은 정확한 부모 API를 유지하거나 확장하는 중에있다.

기본 ItemsRepository 가 있다고 가정 해 봅시다 .

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

그리고 그것을 확장하는 하위 클래스 :

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

그런 다음 클라이언트 가 Base ItemsRepository API를 사용하여이를 사용하도록 할 수 있습니다.

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

LSP는 때 고장 대체 부모 로모그래퍼 클래스를 서브 클래스 나누기 API의 계약 .

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

https://www.udemy.com/enterprise-php/ 내 과정에서 유지 관리 가능한 소프트웨어 작성에 대해 자세히 알아볼 수 있습니다.


20

기본 클래스에 대한 포인터 또는 참조를 사용하는 함수는 몰래 파생 클래스의 객체를 사용할 수 있어야합니다.

LSP에 대해 처음 읽었을 때 이것은 매우 엄격한 의미로 간주되어 본질적으로 인터페이스 구현 및 유형 안전 캐스팅과 동일하다고 가정했습니다. 이는 언어 자체에 의해 LSP가 보장되는지 여부를 의미합니다. 예를 들어,이 엄격한 의미에서 컴파일러에 관한 한 ThreeDBoard는 확실히 Board를 대체 할 수 있습니다.

LSP가 일반적으로 그보다 더 광범위하게 해석된다는 것을 알았지 만 개념에 대해 더 자세히 읽은 후에.

간단히 말해서, 클라이언트 코드가 포인터 뒤의 오브젝트가 포인터 유형이 아니라 파생 된 유형임을 "알고"있다는 것은 유형 안전에 제한되지 않습니다. LSP 준수는 객체의 실제 동작을 조사하여 테스트 할 수도 있습니다. 즉, 메소드 호출 결과 또는 오브젝트에서 발생한 예외 유형에 대한 오브젝트 상태 및 메소드 인수의 영향을 조사합니다.

다시 예제로 돌아가서 이론적 으로 보드 메소드는 ThreeDBoard에서 잘 작동하도록 만들 수 있습니다. 그러나 실제로는 ThreeDBoard가 추가하려는 기능을 방해하지 않으면 서 클라이언트가 제대로 처리하지 못하는 동작의 차이를 방지하기가 매우 어려울 것입니다.

이러한 지식을 바탕으로 LSP 준수 평가는 컴포지션이 상속이 아닌 기존 기능을 확장하는 데 더 적합한 메커니즘인지 판단하는 데 유용한 도구가 될 수 있습니다.


19

모든 사람들이 LSP가 기술적으로 무엇인지 다룬 것 같습니다. 기본적으로 하위 유형 세부 사항에서 추상화하고 수퍼 유형을 안전하게 사용할 수 있기를 원합니다.

따라서 Liskov에는 3 가지 기본 규칙이 있습니다.

  1. 서명 규칙 : 하위 유형의 모든 수퍼 유형의 구문을 구문 적으로 올바르게 구현해야합니다. 컴파일러가 확인할 수있는 것입니다. 더 적은 수의 예외를 발생시키고 최소한 수퍼 타입 ​​메소드만큼 액세스 할 수있는 규칙이 있습니다.

  2. 방법 규칙 : 이러한 작업의 구현은 의미 적으로 양호합니다.

    • 약한 전제 조건 : 하위 유형 함수는 적어도 슈퍼 유형이 입력으로 취한 것을 취해야합니다.
    • 강력한 사후 조건 : 수퍼 타입 ​​메소드가 생성 한 출력의 서브 세트를 생성해야합니다.
  3. 속성 규칙 : 개별 함수 호출 이상의 기능을 수행합니다.

    • 불변 : 항상 참인 것은 참으로 유지되어야합니다. 예 : 세트의 크기는 절대 음수가 아닙니다.
    • 진화 적 속성 : 일반적으로 불변성 또는 객체가 존재할 수있는 상태의 종류와 관련이 있습니다.

이러한 모든 속성을 유지해야하며 추가 하위 유형 기능이 수퍼 유형 속성을 위반하지 않아야합니다.

이 세 가지를 처리하면 기본 항목에서 추상화되어 느슨하게 연결된 코드를 작성하는 것입니다.

출처 : Java의 프로그램 개발-Barbara Liskov


18

LSP 사용 의 중요한 예는 소프트웨어 테스트 입니다.

클래스 A가 B의 LSP 호환 서브 클래스 인 경우 B의 테스트 스위트를 재사용하여 A를 테스트 할 수 있습니다.

서브 클래스 A를 완전히 테스트하려면 테스트 케이스를 몇 개 더 추가해야하지만 최소한 모든 슈퍼 클래스 B의 테스트 케이스를 재사용 할 수 있습니다.

내 : 실현에 대한 방법은 무엇 맥그리거는 "테스트하기 위해 병렬 계층 구조의"를 호출 구축이입니다 ATest클래스에서 상속합니다 BTest. 그런 다음 테스트 케이스가 B 유형이 아닌 A 유형의 개체와 함께 작동하도록하려면 일부 형식의 주입이 필요합니다 (간단한 템플릿 방법 패턴이 수행함).

모든 서브 클래스 구현에 수퍼 테스트 스위트를 재사용하는 것은 실제로 이러한 서브 클래스 구현이 LSP 호환인지 테스트하는 방법입니다. 따라서 하위 클래스의 맥락에서 수퍼 클래스 테스트 스위트를 실행 해야 한다고 주장 할 수도 있습니다 .

Stackoverflow 질문 " 인터페이스 구현을 테스트하기 위해 일련의 재사용 가능한 테스트를 구현할 수 있습니까? "에 대한 답변도 참조하십시오.


13

자바로 설명해 봅시다 :

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

문제 없어요? 자동차는 분명히 운송 수단이며, 여기서는 슈퍼 클래스의 startEngine () 메소드를 재정의한다는 것을 알 수 있습니다.

다른 운송 장치를 추가합시다 :

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

모든 것이 계획대로 진행되지 않습니다! 예, 자전거는 운송 수단이지만 엔진이 없으므로 startEngine () 메소드를 구현할 수 없습니다.

이것들은 Liskov 대체 원칙을 위반하는 문제의 종류이며, 일반적으로 아무것도하지 않거나 구현할 수없는 방법으로 인식 될 수 있습니다.

이러한 문제에 대한 해결책은 올바른 상속 계층 구조이며 우리의 경우 엔진이 있거나없는 운송 장치 클래스를 차별화하여 문제를 해결할 것입니다. 자전거는 운송 수단이지만 엔진이 없습니다. 이 예에서 운송 장치에 대한 정의가 잘못되었습니다. 엔진이 없어야합니다.

다음과 같이 TransportationDevice 클래스를 리팩터링 할 수 있습니다.

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

이제 우리는 비 동력 장치를 위해 TransportationDevice를 확장 할 수 있습니다.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

자동화 된 장치를 위해 TransportationDevice를 확장하십시오. Engine 객체를 추가하는 것이 더 적합합니다.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

따라서 우리 자동차 등급은 Liskov 대체 원칙을 준수하면서 더욱 전문화됩니다.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

또한 자전거 클래스는 Liskov 대체 원칙을 준수합니다.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

9

이 LSP 공식은 너무 강력합니다.

S 유형의 각 객체 o1에 대해 T로 정의 된 모든 프로그램 P에 대해 o1이 o2로 대체 될 때 P의 동작이 변경되지 않도록 T 유형의 객체 o2가있는 경우 S는 T의 하위 유형입니다.

이것은 기본적으로 S가 T와 완전히 동일한 캡슐화 된 또 다른 구현을 의미합니다. 대담하고 성능이 P의 행동의 일부라고 결정할 수 있습니다 ...

따라서 기본적으로 후기 바인딩을 사용하면 LSP에 위배됩니다. 우리가 한 종류의 물건을 다른 종류의 물건으로 대체 할 때 다른 행동을 얻는 것이 OO의 요점입니다!

이 속성은 상황에 따라 다르며 프로그램의 전체 동작을 반드시 포함 할 필요는 없으므로 wikipedia에서 인용 공식 이 더 좋습니다.


2
음, 그 공식은 Barbara Liskov 자신의 것입니다. Barbara Liskov,“데이터 추상화 및 계층 구조”SIGPLAN Notices, 23,5 (1988 년 5 월). 그것은 "너무 튼튼하지 않다", "정확히 옳다", 그리고 당신이 생각하는 의미가 없습니다. 그것은 강하지 만 적절한 양의 힘을 가지고 있습니다.
DrPizza

그런 다음 실제 생활에는 하위 유형이 거의 없습니다 :)
Damien Pollet

3
"동작이 변경되지 않았다"는 것은 하위 유형이 정확히 동일한 구체적인 결과 값을 제공한다는 의미는 아닙니다. 하위 유형의 동작이 기본 유형에서 예상되는 것과 일치 함을 의미합니다. 예 : 기본 유형 Shape에는 draw () 메서드가있을 수 있으며이 메서드가 모양을 렌더링하도록 지정합니다. Shape의 두 가지 하위 유형 (예 : Square 및 Circle)은 draw () 메서드를 구현하며 결과는 다르게 보입니다. 그러나 (모형을 렌더링하는) 동작이 지정된 Shape 동작과 일치하는 한 LSP에 따라 Square 및 Circle은 Shape의 하위 유형이됩니다.
SteveT

9

아주 간단한 문장으로, 우리는 말할 수 있습니다 :

자식 클래스는 기본 클래스 특성을 위반하지 않아야합니다. 가능해야합니다. 서브 타이핑과 동일하다고 말할 수 있습니다.


9

리스 코프의 대체 원칙 (LSP)

우리는 항상 프로그램 모듈을 디자인하고 클래스 계층을 만듭니다. 그런 다음 일부 클래스를 확장하여 일부 파생 클래스를 만듭니다.

새로운 파생 클래스가 기존 클래스의 기능을 대체하지 않고 확장되도록해야합니다. 그렇지 않으면 새 클래스가 기존 프로그램 모듈에서 사용될 때 원하지 않는 효과를 낼 수 있습니다.

Liskov의 대체 원칙은 프로그램 모듈이 Base 클래스를 사용하는 경우 프로그램 모듈의 기능에 영향을 미치지 않고 Base 클래스에 대한 참조를 Derived 클래스로 대체 할 수 있다고 명시합니다.

예:

다음은 Liskov의 대체 원칙을 위반 한 전형적인 예입니다. 이 예에서는 사각형과 사각형의 두 클래스가 사용됩니다. Rectangle 객체가 응용 프로그램 어딘가에 사용된다고 가정 해 봅시다. 응용 프로그램을 확장하고 Square 클래스를 추가합니다. 정사각형 클래스는 일부 조건에 따라 팩토리 패턴으로 리턴되며 어떤 유형의 오브젝트가 리턴되는지 정확히 알지 못합니다. 그러나 우리는 그것이 Rectangle이라는 것을 알고 있습니다. 직사각형 객체를 얻고 너비를 5로, 높이를 10으로 설정하고 면적을 가져옵니다. 너비가 5이고 높이가 10 인 사각형의 경우 면적은 50이어야합니다. 대신 결과는 100입니다.

    // Violation of Likov's Substitution Principle
class Rectangle {
    protected int m_width;
    protected int m_height;

    public void setWidth(int width) {
        m_width = width;
    }

    public void setHeight(int height) {
        m_height = height;
    }

    public int getWidth() {
        return m_width;
    }

    public int getHeight() {
        return m_height;
    }

    public int getArea() {
        return m_width * m_height;
    }
}

class Square extends Rectangle {
    public void setWidth(int width) {
        m_width = width;
        m_height = width;
    }

    public void setHeight(int height) {
        m_width = height;
        m_height = height;
    }

}

class LspTest {
    private static Rectangle getNewRectangle() {
        // it can be an object returned by some factory ...
        return new Square();
    }

    public static void main(String args[]) {
        Rectangle r = LspTest.getNewRectangle();

        r.setWidth(5);
        r.setHeight(10);
        // user knows that r it's a rectangle.
        // It assumes that he's able to set the width and height as for the base
        // class

        System.out.println(r.getArea());
        // now he's surprised to see that the area is 100 instead of 50.
    }
}

결론:

이 원칙은 Open Close 원칙의 확장 일 뿐이므로 새로운 파생 클래스가 동작을 변경하지 않고 기본 클래스를 확장하고 있는지 확인해야합니다.

참조 : 공개 원칙

더 나은 구조를위한 유사한 개념들 : 컨벤션에 대한 협약


8

Liskov 대체 원리

  • 재정의 된 메소드는 비워 둘 수 없습니다
  • 재정의 된 메소드는 오류를 발생시키지 않아야합니다.
  • 파생 클래스 동작으로 인해 기본 클래스 또는 인터페이스 동작이 수정 (재 작업)으로 진행되어서는 안됩니다.

7

일부 부록 :
파생 클래스가 준수 해야하는 기본 클래스의 Invariant, 전제 조건 및 사후 조건에 대해 아무도 작성하지 않은 이유가 궁금합니다. 파생 클래스 D를 기본 클래스 B에서 완전히 대체 할 수 있으려면 클래스 D가 특정 조건을 준수해야합니다.

  • 기본 클래스의 변형은 파생 클래스에 의해 보존되어야합니다
  • 기본 클래스의 사전 조건은 파생 클래스에 의해 강화되어서는 안됩니다
  • 파생 클래스가 기본 클래스의 사후 조건을 약화해서는 안됩니다.

따라서 파생자는 기본 클래스가 부과하는 위의 세 가지 조건을 알고 있어야합니다. 따라서 하위 유형 지정 규칙이 미리 결정됩니다. 즉, 'IS A'관계는 특정 규칙이 하위 유형에 의해 준수되는 경우에만 준수해야합니다. 이 규칙은 고정, 사전 승인 및 사후 조건의 형식으로 공식적인 ' 디자인 계약 '에 의해 결정되어야합니다 .

내 블로그에서 사용할 수있는 추가 토론 : Liskov 대체 원칙


6

간단히 말해서 LSP 는 같은 수퍼 클래스의 객체를 아무 것도 깨지 않고 서로 교환 할 수 있어야한다고 말합니다 .

우리는이 예를 들어, Cat그리고 Dog에서 파생 된 클래스 Animal클래스, 동물 클래스를 사용하여 모든 기능을 사용할 수 있어야 Cat또는 Dog정상적으로 동작합니다.


4

보드 배열 측면에서 ThreeDBoard를 구현하는 것이 유용할까요?

아마도 다양한 평면에서 ThreeDBoard 조각을 보드로 취급 할 수 있습니다. 이 경우 여러 구현을 허용하기 위해 Board의 인터페이스 (또는 추상 클래스)를 추상화 할 수 있습니다.

외부 인터페이스와 관련하여 TwoDBoard 및 ThreeDBoard에 대한 보드 인터페이스를 제외 할 수 있습니다 (위의 방법 중 어느 것도 적합하지는 않음).


1
예제는 단순히 보드에서 상속하는 것이 ThreeDBoard의 맥락에서 의미가 없으며 모든 메소드 서명이 Z 축으로 의미가 없음을 보여주기위한 것이라고 생각합니다.
NotMyself

4

사각형은 너비가 높이와 같은 사각형입니다. 사각형이 너비와 높이에 대해 서로 다른 두 가지 크기를 설정하면 사각형 불변을 위반합니다. 이것은 부작용을 도입하여 해결됩니다. 그러나 사각형에 사전 조건이 0 <height 및 0 <width 인 setSize (height, width)가있는 경우. 파생 된 하위 유형 방법에는 높이 == 너비가 필요합니다. 더 강한 전제 조건 (그리고 그것은 lsp를 위반합니다). 이것은 사각형이 사각형이지만 전제 조건이 강화 되었기 때문에 유효한 부속 유형이 아님을 보여줍니다. 해결 방법 (일반적으로 나쁜 것)은 부작용을 일으키고 이것은 포스트 상태를 약화시킵니다 (lsp를 위반 함). 베이스의 setWidth는 사후 조건 0 <너비를 갖습니다. 파생은 높이 == 너비로 약화시킵니다.

따라서 크기 조정 가능한 사각형은 크기 조정 가능한 사각형이 아닙니다.


4

이 원칙은 1987 년 Barbara Liskov 에 의해 도입되었으며 수퍼 클래스 및 하위 유형의 동작에 중점을 두어 Open-Closed Principle을 확장합니다.

이를 위반 한 결과를 고려할 때 그 중요성이 분명해집니다. 다음 클래스를 사용하는 응용 프로그램을 고려하십시오.

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

언젠가 클라이언트가 사각형 외에 사각형을 조작 할 수있는 기능을 요구한다고 상상해보십시오. 사각형은 사각형이므로 사각형 클래스는 Rectangle 클래스에서 파생되어야합니다.

public class Square : Rectangle
{
} 

그러나 이렇게하면 두 가지 문제가 발생합니다.

사각형에는 사각형에서 상속 된 높이 및 너비 변수가 모두 필요하지 않으므로 수십만 개의 사각형 개체를 만들어야하는 경우 메모리에 상당한 낭비가 발생할 수 있습니다. 사각형에서 상속 된 너비 및 높이 세터 속성은 사각형의 너비와 높이가 동일하므로 사각형에 적합하지 않습니다. 높이와 너비를 모두 같은 값으로 설정하기 위해 다음과 같이 두 가지 새로운 속성을 만들 수 있습니다.

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

이제 누군가가 사각형 객체의 너비를 설정하면 그 높이가 그에 따라 변경됩니다.

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

앞으로 나아가서이 다른 기능을 고려해 봅시다 :

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

정사각형 객체에 대한 참조를이 함수에 전달하면 함수가 인수의 파생물에 대해 작동하지 않기 때문에 LSP를 위반하게됩니다. 너비와 높이 속성은 사각형으로 가상으로 선언되지 않기 때문에 다형성이 아닙니다 (높이가 변경되지 않기 때문에 사각형 객체가 손상됨).

그러나 setter 속성을 가상으로 선언하면 OCP라는 또 다른 위반에 직면하게됩니다. 실제로 파생 클래스 사각형을 만들면 기본 클래스 사각형이 변경됩니다.


3

내가 지금까지 찾은 LSP에 대한 가장 명확한 설명은 "Liskov 대체 원칙은 파생 클래스의 객체가 시스템에서 오류를 일으키거나 기본 클래스의 동작을 수정하지 않고 기본 클래스의 객체를 대체 할 수 있어야한다고 말합니다. " 여기에서 . 이 기사는 LSP를 위반하고 수정하는 코드 예제를 제공합니다.


1
stackoverflow에서 코드 예제를 제공하십시오.
sebenalern

3

코드에서 사각형을 사용한다고 가정 해 봅시다.

r = new Rectangle();
// ...
r.setDimensions(1,2);
r.fill(colors.red());
canvas.draw(r);

기하학 클래스에서 우리는 사각형의 너비가 높이와 길이가 같기 때문에 사각형이 특별한 유형의 사각형이라는 것을 배웠습니다. Square이 정보를 바탕으로 수업을 만들어 봅시다 .

class Square extends Rectangle {
    setDimensions(width, height){
        assert(width == height);
        super.setDimensions(width, height);
    }
} 

우리가를 교체 할 경우 RectangleSquare우리의 첫 번째 코드에서, 그것은 중단됩니다

r = new Square();
// ...
r.setDimensions(1,2); // assertion width == height failed
r.fill(colors.red());
canvas.draw(r);

클래스에 Square없는 새로운 전제 조건 이 있기 때문 입니다. LSP에 따르면 인스턴스는 서브 클래스 인스턴스 로 대체 가능해야 합니다. 이러한 인스턴스는 인스턴스 유형 검사를 통과 하여 코드에서 예기치 않은 오류가 발생하기 때문입니다.Rectanglewidth == heightRectangleRectangleRectangle

이것은 위키 기사의 "하위 유형에서 전제 조건을 강화할 수 없습니다" 부분의 예 입니다 . 결론적으로 LSP를 위반하면 코드에 오류가 발생할 수 있습니다.


3

LSP는``객체는 하위 유형으로 대체 가능해야한다 ''고 말합니다. 반면에이 원칙은

자식 클래스는 부모 클래스의 타입 정의를 어 기지 않아야합니다.

다음 예제는 LSP를 더 잘 이해하는 데 도움이됩니다.

LSP없이 :

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

LSP로 수정 :

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

2

Liskov 대체 원칙 위반 (LSP) 기사를 읽는 것이 좋습니다 .

Liskov 대체 원칙이 무엇인지, 이미 위반했는지 추측하는 데 도움이되는 일반적인 단서와 클래스 계층 구조를보다 안전하게 만드는 데 도움이되는 접근법의 예를 찾을 수 있습니다.


2

LISKOV SUBSTITUTION PRINCIPLE (Mark Seemann 저서)은 클라이언트 나 구현을 중단하지 않고 인터페이스의 한 구현을 다른 구현으로 교체 할 수 있어야한다고 말합니다. 오늘 그들을 예견하십시오.

벽에서 컴퓨터를 분리하면 (구현) 벽 콘센트 (인터페이스) 또는 컴퓨터 (클라이언트)가 고장 나지 않습니다 (실제로 랩톱 컴퓨터 인 경우 일정 시간 동안 배터리를 사용할 수 있음) . 그러나 소프트웨어를 사용하면 클라이언트는 종종 서비스가 제공 될 것으로 기대합니다. 서비스가 제거되면 NullReferenceException이 발생합니다. 이러한 유형의 상황을 처리하기 위해 "아무것도하지 않는"인터페이스의 구현을 만들 수 있습니다. 이것은 Null Object라고 알려진 디자인 패턴이며, [4] 벽에서 컴퓨터를 분리하는 것과 대략 일치합니다. 우리는 느슨한 결합을 사용하기 때문에 실제 구현을 문제없이 아무것도하지 않는 것으로 대체 할 수 있습니다.


2

Likov의 대체 원칙 은 프로그램 모듈이 Base 클래스를 사용하는 경우 프로그램 모듈의 기능에 영향을주지 않고 Base 클래스에 대한 참조를 Derived 클래스로 대체 할 수 있다고 명시합니다.

의도-파생 유형은 기본 유형을 완전히 대체 할 수 있어야합니다.

예-Java의 공변량 리턴 유형


1

이 게시물 에서 발췌 한 내용은 다음과 같습니다 .

[..] 몇 가지 원칙을 이해하기 위해서는 그것이 위반 된 시점을 인식하는 것이 중요합니다. 이것이 내가 지금 할 일입니다.

이 원칙을 위반한다는 것은 무엇을 의미합니까? 인터페이스로 표현 된 추상화에 의해 부과 된 계약을 객체가 충족시키지 못함을 의미합니다. 즉, 추상화를 잘못 식별했음을 의미합니다.

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

interface Account
{
    /**
     * Withdraw $money amount from this account.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}
class DefaultAccount implements Account
{
    private $balance;
    public function withdraw(Money $money)
    {
        if (!$this->enoughMoney($money)) {
            return;
        }
        $this->balance->subtract($money);
    }
}

이것이 LSP를 위반합니까? 예. 계정의 계약에 따라 계정이 철회 될 것이라고 알려주기 때문이지만 항상 그런 것은 아닙니다. 문제를 해결하려면 어떻게해야합니까? 계약서 만 수정하면됩니다.

interface Account
{
    /**
     * Withdraw $money amount from this account if its balance is enough.
     * Otherwise do nothing.
     *
     * @param Money $money
     * @return mixed
     */
    public function withdraw(Money $money);
}

Voilà, 이제 계약이 충족되었습니다.

이 미묘한 위반은 종종 고객에게 사용 된 콘크리트 물체의 차이를 구별 할 수있는 능력을 부여합니다. 예를 들어 첫 번째 계정의 계약이 다음과 같이 보일 수 있습니다.

class Client
{
    public function go(Account $account, Money $money)
    {
        if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) {
            return;
        }
        $account->withdraw($money);
    }
}

그리고 이것은 공개 폐쇄 원칙, 즉 돈 인출 요구 사항을 자동으로 위반합니다. 계약을 위반 한 물건에 충분한 돈이 없으면 어떻게 될지 알 수 없기 때문입니다. 아마 아무것도 반환하지 않을 것입니다. 아마도 예외가 발생합니다. 따라서 hasEnoughMoney()인터페이스의 일부가 아닌지 확인해야합니다 . 따라서이 구체적인 등급에 따른 강제 점검은 OCP 위반입니다].

이 요점은 또한 LSP 위반에 대해 자주 발생하는 오해를 다룹니다. "아이의 부모의 행동이 바뀌면 LSP를 위반하는 것"이라고 말합니다. 그러나 자녀가 부모의 계약을 위반하지 않는 한 그렇지 않습니다.

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