Programing

Liskov Substitution Principle의 예는 무엇입니까?

lottogame 2020. 9. 29. 07:09
반응형

Liskov Substitution Principle의 예는 무엇입니까?


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


LSP (내가 최근에 들었던 팟 캐스트에서 Bob 아저씨가 제공함)를 보여주는 좋은 예는 때때로 자연어에서 제대로 들리는 무언가가 코드에서 제대로 작동하지 않는 경우였습니다.

수학에서 a SquareRectangle. 사실 그것은 직사각형의 전문화입니다. "is a"는 상속을 통해이를 모델링하도록합니다. 코드에서 당신이 만든 경우 Square에서 파생 Rectangle, 다음은 Square당신이 예상 가능한 어느 곳이어야한다 Rectangle. 이로 인해 이상한 동작이 발생합니다.

기본 클래스 SetWidthSetHeight메서드 가 있다고 상상해보십시오 Rectangle. 이것은 완벽하게 논리적으로 보입니다. 귀하의 경우 Rectangle참조가 지적 Square, 다음 SetWidthSetHeight하나를 설정하면 그것을 일치 다른 변화 때문에 이해가되지 않습니다. 이 경우 SquareLiskov Substitution Test에 실패하고 상속 RectangleSquare받는 추상화 Rectangle는 잘못된 것입니다.

여기에 이미지 설명 입력

다른 귀중한 SOLID Principles Motivational Posters를 확인해야합니다 .


리스 코프 치환 원칙 (LSP, ) 그 상태를 프로그래밍 객체 지향의 개념이다 :

기본 클래스에 대한 포인터 또는 참조를 사용하는 함수는 자신도 모르게 파생 클래스의 개체를 사용할 수 있어야합니다.

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

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

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

클래스 다이어그램

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

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

언뜻보기에 이것은 좋은 결정처럼 보입니다. 속성을 Board모두 제공하고 Z 축을 제공합니다.HeightWidthThreeDBoard

분류되는 부분은에서 상속 된 다른 모든 구성원을 볼 때입니다 Board. 의 방법은 AddUnit, GetTile, GetUnits등, 모두에서 X 및 Y 매개 변수를 모두 가지고 Board클래스 만이 ThreeDBoard아니라 Z 매개 변수를 필요로한다.

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

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

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


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

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

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

class Square : Rectangle { }

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

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

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


대체 가능성은 컴퓨터 프로그램에서 S가 T의 하위 유형이면 T 유형의 개체가 S 유형의 개체로 대체 될 수 있다는 것을 나타내는 개체 지향 프로그래밍의 원칙입니다.

Java로 간단한 예를 들어 보겠습니다.

나쁜 예

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

오리는 새이기 때문에 날 수 있지만 이건 어떨까요?

public class Ostrich extends Bird{}

타조는 새이지만 날 수없고, 타조 클래스는 Bird 클래스의 하위 유형이지만, 플라이 방법을 사용할 수 없습니다. 즉, LSP 원칙을 위반하는 것입니다.

좋은 예

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

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

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

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의 명백한 위반입니다. 이 함수는 인수의 파생물에 대해 작동하지 않습니다.

[...]


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

예를 들어, 입력 매개 변수 유형 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

불변 필드가 있는 유형조차도 불변성을 가지고 있습니다. 예를 들어 불변 Rectangle setter는 차원이 독립적으로 수정 될 것으로 예상하지만 불변 Square setter는이 기대치를 위반합니다.

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. 실제로의 해당 메서드를 호출 할 때 STi입력 인수는 Si입력 매개 변수에 So할당되고 출력은 유형에 할당됩니다 To. 따라서에 Si반 변성이 아니라면 Ti하위 유형 (의 하위 유형 Xi이 아닐 Si것임)이에 할당 될 수 있습니다 Ti.

또한 유형 다형성 매개 변수 (예 : 제네릭)에 대한 정의 사이트 분산 주석이있는 언어 (예 : Scala 또는 Ceylon)의 경우 해당 유형의 각 유형 매개 변수에 대한 분산 주석의 공동 또는 반대 방향은 반대 또는 동일한 방향 T이어야합니다. 유형 매개 변수의 유형을 가진 모든 입력 매개 변수 또는 출력 (의 모든 메소드 ) 에 각각 .T

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


불변을 열거 할 수있는 경우 하위 유형 지정이 적합 합니다.

불변을 모델링하는 방법에 대한 많은 연구가 진행 중이므로 컴파일러에 의해 적용됩니다.

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

확장에 대한 추상화를 위한 의미론이 필요하기 때문에 , 불변을 모델링하기 위해 타이핑을 사용하는 것, 즉 통합 된 고차 표시 의미론이 Typestate보다 우월하다고 생각합니다. '확장'은 조정되지 않은 모듈 식 개발의 무한하고 순열 된 구성을 의미합니다. 내가보기에 통일의 반대 인 것 같아서 자유도, 확장 가능한 구성을 위해 서로 통일 될 수없는 공유 된 의미를 표현하기위한 두 개의 상호 의존적 모델 (예 : 유형 및 유형 상태)을 갖는 것 . 예를 들어, 표현 문제 와 같은 확장은 하위 유형, 함수 오버로딩 및 매개 변수 유형 도메인에서 통합되었습니다.

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

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

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

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

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

의사 파이썬에서

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

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

Derived 개체에 대해 Foo를 호출 할 때마다 arg가 동일하면 기본 개체에 대해 Foo를 호출하는 것과 정확히 동일한 결과를 제공하면 LSP를 충족합니다.


기본 클래스에 대한 포인터 또는 참조를 사용하는 함수는 자신도 모르게 파생 클래스의 개체를 사용할 수 있어야합니다.

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

개념에 대해 더 많이 읽은 후 LSP가 일반적으로 그보다 더 광범위하게 해석된다는 것을 알았습니다.

간단히 말해서 클라이언트 코드가 포인터 뒤에있는 개체가 포인터 형식이 아닌 파생 형식이라는 것을 "알고"있다는 의미는 형식 안전성에 제한되지 않습니다. LSP 준수 여부는 개체의 실제 동작을 조사하여 테스트 할 수도 있습니다. 즉, 개체의 상태 및 메서드 인수가 메서드 호출 결과 또는 개체에서 throw되는 예외 유형에 미치는 영향을 검사합니다.

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

이러한 지식을 바탕으로 LSP 준수를 평가하는 것은 컴포지션이 상속이 아닌 기존 기능을 확장하는 데 더 적합한 메커니즘 인시기를 결정하는 데 훌륭한 도구가 될 수 있습니다.


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

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

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

이를 실현하는 방법은 McGregor가 "테스트를위한 병렬 계층 구조"라고 부르는 것을 구축하는 ATest것입니다 . 내 클래스는 BTest. 그런 다음 테스트 케이스가 B 유형이 아닌 A 유형의 객체와 함께 작동하는지 확인하기 위해 어떤 형태의 주입이 필요합니다 (간단한 템플릿 메서드 패턴이 가능합니다).

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

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


Liskov를 위반하는지 여부를 결정하는 체크리스트가 있습니다.

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

체크리스트 :

  • 파생 클래스에서 새 예외를 throw해서는 안됩니다 . 기본 클래스에서 ArgumentNullException 이 발생 하면 하위 클래스는 ArgumentNullException 유형의 예외 나 ArgumentNullException에서 파생 된 모든 예외 만 throw 할 수 있습니다. IndexOutOfRangeException을 던지는 것은 Liskov의 위반입니다.
  • 전제 조건을 강화할 수 없습니다 : 기본 클래스가 멤버 int와 함께 작동한다고 가정합니다. 이제 하위 유형은 int가 양수 여야합니다. 이것은 강화 된 전제 조건이며 이전에는 음의 정수로 완벽하게 작동했던 코드가 손상되었습니다.
  • 사후 조건은 약화 될 수 없습니다 : 메서드가 반환되기 전에 기본 클래스가 데이터베이스에 대한 모든 연결을 닫아야한다고 가정합니다. 하위 클래스에서 해당 메서드를 무시하고 추가 재사용을 위해 연결을 열어 둡니다. 당신은 그 방법의 사후 조건을 약화 시켰습니다.
  • 불변은 보존되어야합니다 : 가장 어렵고 고통스러운 제약. 불변은 기본 클래스에 어느 정도 숨겨져 있으며이를 드러내는 유일한 방법은 기본 클래스의 코드를 읽는 것입니다. 기본적으로 메서드를 재정의 할 때 재정의 된 메서드가 실행 된 후에 변경할 수없는 것은 변경되지 않은 상태로 유지되어야합니다. 내가 생각할 수있는 가장 좋은 방법은 기본 클래스에서이 불변의 제약을 적용하는 것이지만 쉽지는 않습니다.
  • History Constraint : 메서드를 재정의 할 때 기본 클래스에서 수정할 수없는 속성을 수정할 수 없습니다. 이 코드를 살펴보면 Name이 수정 불가능 (개인 집합)으로 정의되어 있지만 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 # 개발자입니다) 그래서 신경 쓰지 않습니다.

참고:


나는 모든 사람들이 LSP가 기술적으로 무엇인지 다뤘다 고 생각한다. 당신은 기본적으로 서브 타입 세부 사항에서 추상화하고 슈퍼 타입을 안전하게 사용할 수 있기를 원한다.

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

  1. 서명 규칙 : 구문 상 하위 유형의 모든 수퍼 유형 작업에 대한 유효한 구현이 있어야합니다. 컴파일러가 확인할 수있는 것. 더 적은 예외를 던지고 적어도 수퍼 타입 ​​메소드만큼 액세스 할 수있는 것에 대한 약간의 규칙이 있습니다.

  2. 방법 규칙 : 이러한 작업의 구현은 의미 상 건전합니다.

    • 약한 전제 조건 : 하위 유형 함수는 적어도 상위 유형이 입력으로 취한 것을 가져야합니다.
    • 더 강력한 사후 조건 : 상위 유형 메서드가 생성 한 출력의 하위 집합을 생성해야합니다.
  3. 속성 규칙 : 이것은 개별 함수 호출을 넘어 섭니다.

    • 불변성 : 항상 참인 것은 참으로 유지되어야합니다. 예 : 세트의 크기는 절대 음수가 아닙니다.
    • 진화 속성 : 일반적으로 불변성 또는 객체가있을 수있는 상태의 종류와 관련이 있습니다. 또는 객체가 성장 만하고 절대 줄어들지 않으므로 하위 유형 메서드가 만들어서는 안됩니다.

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

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

출처 : 자바 프로그램 개발-Barbara Liskov


부모 클래스를 확장 할 때 짧은 이야기,하자가, 실제적인 예를 들어 사각형의 직사각형 및 정사각형 사각형을두고, 당신은 정확한 부모 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/


나는 모든 답변에서 직사각형과 정사각형을 봅니다. 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 !
    }
}

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


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

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

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

따라서 기본적으로 지연 바인딩 사용은 LSP를 위반합니다. 한 종류의 개체를 다른 종류의 개체로 대체 할 때 다른 동작을 얻는 것이 OO의 요점입니다!

속성이 컨텍스트에 따라 다르며 프로그램의 전체 동작을 반드시 포함하지는 않기 때문에 위키피디아에서 인용 공식 이 더 좋습니다.


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

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

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

이에 대한 추가 토론은 내 블로그에서 확인할 수 있습니다. Liskov Substitution 원리


아주 간단한 문장으로 다음과 같이 말할 수 있습니다.

하위 클래스는 기본 클래스 특성을 위반하지 않아야합니다. 그것으로 가능해야합니다. 하위 입력과 동일하다고 말할 수 있습니다.


Liskov의 대체 원리 (LSP)

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

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

Liskov의 Substitution Principle은 프로그램 모듈이 Base 클래스를 사용하는 경우 Base 클래스에 대한 참조를 프로그램 모듈의 기능에 영향을주지 않고 Derived 클래스로 바꿀 수 있다고 말합니다.

예:

아래는 Liskov의 대체 원칙을 위반 한 전형적인 예입니다. 이 예에서는 Rectangle과 Square의 두 가지 클래스가 사용됩니다. Rectangle 객체가 응용 프로그램의 어딘가에서 사용된다고 가정 해 보겠습니다. 애플리케이션을 확장하고 Square 클래스를 추가합니다. 정사각형 클래스는 일부 조건에 따라 팩토리 패턴으로 반환되며 어떤 유형의 객체가 반환 될지 정확히 알 수 없습니다. 그러나 우리는 그것이 직사각형이라는 것을 압니다. 직사각형 객체를 얻고 너비를 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 Principle의 확장 일 뿐이며 새로운 파생 클래스가 동작을 변경하지 않고 기본 클래스를 확장하는지 확인해야 함을 의미합니다.

참조 : 열기 닫기 원칙

더 나은 구조를위한 유사한 개념 : 구성 보다 규칙


Java로 설명해 보겠습니다.

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 Substitution Principle의 위반으로 이어지는 종류의 문제이며, 일반적으로 아무것도하지 않거나 구현할 수없는 방법으로 인식 할 수 있습니다.

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

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() { ... }
}

따라서 우리의 Car 클래스는 Liskov Substitution Principle을 고수하면서 더욱 전문화됩니다.

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

그리고 우리의 자전거 클래스는 Liskov 대체 원칙을 준수합니다.

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

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

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

외부 인터페이스 측면에서 TwoDBoard 및 ThreeDBoard 모두에 대해 Board 인터페이스를 제외 할 수 있습니다 (위의 방법 중 어느 것도 적합하지 않음).


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

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


코드에서 직사각형을 사용한다고 가정 해 보겠습니다.

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우리가 Rectangle클래스에 없는 새로운 전제 조건 이 있기 때문입니다 : width == height. LSP에 따르면 Rectangle인스턴스는 Rectangle하위 클래스 인스턴스 로 대체 가능해야 합니다. 이는 이러한 인스턴스가 인스턴스에 대한 유형 검사를 통과 Rectangle하여 코드에서 예기치 않은 오류를 발생시키기 때문입니다.

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


기사를 읽어 보시기 바랍니다 : Liskov Substitution Principle (LSP) 위반 .

Liskov Substitution Principle이 무엇인지에 대한 설명, 이미 위반했는지 추측하는 데 도움이되는 일반적인 단서 및 클래스 계층 구조를보다 안전하게 만드는 데 도움이되는 접근 방식의 예를 찾을 수 있습니다.


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


LISKOV SUBSTITUTION PRINCIPLE (From Mark Seemann book)에서는 클라이언트 나 구현을 중단하지 않고 인터페이스의 한 구현을 다른 구현으로 대체 할 수 있어야한다고 말합니다.이 원칙은 우리가 할 수 있더라도 미래에 발생하는 요구 사항을 해결할 수 있도록합니다. 오늘 그들을 예견하십시오.

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


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

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

예-Java의 공변 반환 유형


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();
}

Liskov 대체 원칙

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

인터페이스를 고려해 보겠습니다.

interface Planet{
}

이것은 클래스에 의해 구현됩니다.

class Earth implements Planet {
    public $radius;
    public function construct($radius) {
        $this->radius = $radius;
    }
}

지구를 다음과 같이 사용합니다.

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

이제 지구를 확장하는 클래스를 하나 더 고려하십시오.

class LiveablePlanet extends Earth{
   public function color(){
   }
}

이제 LSP에 따르면 지구 대신 LiveablePlanet을 사용할 수 있어야하며 시스템이 손상되지 않아야합니다. 처럼:

$planet = new LiveablePlanet(6371);  // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

여기 에서 가져온 예


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

[..] 몇 가지 원칙을 이해하기 위해서는 언제 위반되었는지 깨닫는 것이 중요합니다. 이것이 제가 지금 할 일입니다.

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

다음 예를 고려하십시오.

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를 위반하는 것”이라고합니다. 그러나 자녀가 부모의 계약을 위반하지 않는 한 그렇지 않습니다.

참고 URL : https://stackoverflow.com/questions/56860/what-is-an-example-of-the-liskov-substitution-principle

반응형