문제

LSP(리스코프 대체 원리)가 객체 지향 설계의 기본 원리라고 들었습니다.그것은 무엇이며 그 사용 예는 무엇입니까?

도움이 되었습니까?

해결책 2

리스코프 대체 원리(LSP, )는 다음과 같은 객체 지향 프로그래밍의 개념입니다.

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

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

이 점을 설명하기 위해 제가 본 가장 효과적인 방법은 다음과 같습니다. 헤드퍼스트 OOA&D.그들은 전략 게임을 위한 프레임워크를 구축하는 프로젝트의 개발자인 시나리오를 제시합니다.

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

Class Diagram

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

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

언뜻 보면 이것은 좋은 결정처럼 보입니다. Board 두 가지 모두를 제공합니다 Height 그리고 Width 속성과 ThreeDBoard Z축을 제공합니다.

그것이 세분화되는 부분은 상속받은 다른 모든 구성원을 볼 때입니다. Board.에 대한 방법 AddUnit, GetTile, GetUnits 등등 모두 X와 Y 매개변수를 모두 사용합니다. Board 수업이지만 ThreeDBoard Z 매개변수도 필요합니다.

따라서 Z 매개변수를 사용하여 해당 메서드를 다시 구현해야 합니다.Z 매개변수에는 Board 클래스와 상속된 메서드 Board 수업의 의미가 사라집니다.사용을 시도하는 코드 단위 ThreeDBoard 클래스를 기본 클래스로 사용 Board 정말 운이 좋지 않을 것입니다.

어쩌면 우리는 다른 접근법을 찾아야 할 것 같습니다.연장하는 대신 Board, ThreeDBoard 로 구성되어야 한다 Board 사물.하나 Board Z축 단위당 객체.

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

다른 팁

LSP(최근에 들은 팟캐스트에서 Bob 삼촌이 제공한)를 설명하는 좋은 예는 자연 언어에서는 올바르게 들리는 것이 코드에서는 제대로 작동하지 않는 경우가 있다는 것입니다.

수학에서는 SquareRectangle.실제로 그것은 직사각형의 전문화입니다."is a"는 상속을 통해 이를 모델링하고 싶게 만듭니다.그러나 코드에서 만든 경우 Square ~에서 유래하다 Rectangle, 그 다음에 Square 기대하는 어느 곳에서나 사용할 수 있어야 합니다. Rectangle.이로 인해 이상한 동작이 발생합니다.

당신이 가지고 있다고 상상해보십시오 SetWidth 그리고 SetHeight 당신의 방법 Rectangle 기본 클래스;이것은 완벽하게 논리적인 것 같습니다.그러나 만약 당신의 Rectangle 참조는 a를 가리킨다. Square, 그 다음에 SetWidth 그리고 SetHeight 하나를 설정하면 다른 하나도 일치하도록 변경되기 때문에 의미가 없습니다.이 경우 Square Liskov 대체 테스트에 실패했습니다. Rectangle 그리고 가지고 있다는 추상화 Square ~로부터 상속받다 Rectangle 나쁜 것입니다.

enter image description here

너희들은 다른 귀중한 것들을 확인해 봐야 해 SOLID 원칙 동기 부여 포스터.

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{}

Ostrich는 새이지만 날 수 없습니다. Ostrich 클래스는 Bird 클래스의 하위 유형이지만 파리 메서드를 사용할 수 없습니다. 이는 LSP 원칙을 위반하고 있음을 의미합니다.

좋은 예

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

로버트 마틴은 훌륭한 Liskov 대체 원칙에 관한 논문.원칙을 위반할 수 있는 미묘하고 그리 미묘하지 않은 방법에 대해 논의합니다.

논문의 일부 관련 부분(두 번째 예는 매우 요약되어 있습니다):

LSP 위반의 간단한 예

이 원칙의 가장 눈부신 위반 중 하나는 C ++ 런타임 유형 정보 (RTTI)를 사용하여 객체 유형에 따라 함수를 선택하는 것입니다.즉.:

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. [...]

Square 상속받게 될 것이다 SetWidth 그리고 SetHeight 기능.이러한 기능은 a Square, 정사각형의 너비와 높이는 동일하기 때문입니다.이것은 디자인에 문제가 있다는 중요한 단서가되어야합니다.그러나 문제를 회피하는 방법이 있습니다.우리는 재정의할 수 있었습니다 SetWidth 그리고 SetHeight [...]

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

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

우리가 a에 대한 참조를 전달하면 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

있는 타입이라도 변경할 수 없는 필드 여전히 불변성이 있습니다.그만큼 불변 직사각형 설정자는 치수가 독립적으로 수정될 것으로 예상하지만 불변 정사각형 세터는 이러한 기대를 위반합니다.

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, 동일하거나 하위 유형 유형의 To 상위 유형의 해당 메소드의 해당 출력 T.

호출자가 유형이 있다고 생각하기 때문입니다. T, 다음 메소드를 호출하고 있다고 생각합니다. T, 그런 다음 유형의 인수를 제공합니다. Ti 출력을 유형에 할당합니다. To.실제로 해당 메서드를 호출할 때 S, 그러면 각각 Ti 입력 인수는 Si 입력 매개변수 및 So 출력은 유형에 할당됩니다. To.따라서 만약 Si w.r.t.와는 반공변적이지 않았습니다.에게 Ti, 하위 유형 Xi—이것은 다음의 하위 유형이 아닙니다. Si—다음에 할당될 수 있음 Ti.

또한 언어(예:유형 다형성 매개변수에 대한 정의 사이트 분산 주석이 있는 Scala 또는 Ceylon)(예:제네릭), 유형의 각 유형 매개변수에 대한 분산 주석의 공동 또는 반대 방향 T 이어야 한다 반대 또는 모든 입력 매개변수 또는 출력에 대해 각각 동일한 방향(모든 방법의 T) 유형 매개변수의 유형을 가지고 있습니다.

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


하위 입력이 적절함 불변성이 열거될 수 있는 곳.

불변성을 모델링하여 컴파일러에서 적용하는 방법에 대한 많은 연구가 진행 중입니다.

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

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

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

이것이 Halting 정리가 존재하는 이유입니다.Turing-complete 프로그래밍 언어에서 가능한 모든 프로그램이 종료되는지 여부는 결정할 수 없습니다.일부 특정 프로그램(모든 가능성이 정의되고 계산된 프로그램)이 종료된다는 것이 입증될 수 있습니다.그러나 해당 프로그램의 확장 가능성이 튜링 완전이 아닌 한(예:종속 입력을 통해).튜링 완전성에 대한 기본 요구 사항은 다음과 같습니다. 무제한 재귀, 괴델의 불완전성 정리와 러셀의 역설이 확장에 어떻게 적용되는지 이해하는 것은 직관적입니다.

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

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

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

의사 파이썬에서

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

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

Derived 객체에서 Foo를 호출할 때마다 arg가 동일한 한 Base 객체에서 Foo를 호출하는 것과 정확히 동일한 결과를 제공하는 경우 LSP를 충족합니다.

기본 클래스에 대한 포인터나 참조를 사용하는 함수는 파생 클래스의 개체를 알지 못한 채 사용할 수 있어야 합니다.

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

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

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

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

이러한 지식을 바탕으로 LSP 준수를 평가하는 것은 상속보다는 기존 기능을 확장하기 위한 더 적절한 메커니즘이 언제인지 결정하는 데 훌륭한 도구가 될 수 있습니다.

리스코프 위반 여부를 판단할 수 있는 체크리스트가 있습니다.

  • 다음 항목 중 하나를 위반하는 경우 -> 리스코프를 위반하는 것입니다.
  • 위반하지 않으면 -> 아무것도 결론을 내릴 수 없습니다.

체크리스트:

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

2개의 다른 항목이 있습니다: 메소드 인수의 반공변성 그리고 반환 유형의 공분산.하지만 C#에서는 불가능하므로(저는 C# 개발자입니다) 신경 쓰지 않습니다.

참조:

의 중요한 예 사용 LSP의 소프트웨어 테스팅.

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

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

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

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

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

나는 모든 사람들이 LSP가 기술적으로 무엇인지 다뤘다고 생각합니다.기본적으로 하위 유형 세부 정보를 추상화하고 상위 유형을 안전하게 사용할 수 있기를 원합니다.

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

  1. 서명 규칙:하위 유형의 상위 유형에 대한 모든 연산은 구문적으로 유효하게 구현되어야 합니다.컴파일러가 당신을 위해 확인할 수 있는 것이 있습니다.더 적은 수의 예외를 발생시키고 적어도 상위 유형 메소드만큼 액세스 가능하다는 약간의 규칙이 있습니다.

  2. 메소드 규칙:이러한 작업의 구현은 의미상 타당합니다.

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

    • 불변성:항상 진실인 것은 진실로 남아 있어야 합니다.예.세트의 크기는 결코 음수가 아닙니다.
    • 진화적 특성:일반적으로 불변성 또는 객체가 가질 수 있는 상태의 종류와 관련이 있습니다.아니면 객체가 커지기만 하고 줄어들지 않아서 하위 유형 메서드가 이를 만들 수 없을 수도 있습니다.

이러한 모든 속성은 보존되어야 하며 추가 하위 유형 기능은 상위 유형 속성을 위반해서는 안 됩니다.

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

원천:Java 프로그램 개발 - Barbara Liskov

간단히 말해, 직사각형, 직사각형, 정사각형은 그대로 두겠습니다. 상위 클래스를 확장할 때 실제적인 예를 들자면 정확한 상위 API를 유지하거나 확장해야 합니다.

당신이 가지고 있다고 가정 해 봅시다 베이스 항목 저장소.

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의 이 공식은 너무 강력합니다.

S 유형의 각 개체 o1에 대해 T 유형의 개체 o2가 있어 T로 정의된 모든 프로그램 P에 대해 o1이 o2를 대체할 때 P의 동작이 변경되지 않으면 S는 T의 하위 유형입니다.

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

따라서 기본적으로 후기 바인딩을 사용하면 LSP를 위반하게 됩니다.한 종류의 객체를 다른 종류의 객체로 대체할 때 다른 동작을 얻는 것이 OO의 핵심입니다!

인용된 공식 위키피디아에 따르면 속성은 상황에 따라 달라지며 프로그램의 전체 동작을 반드시 포함하지는 않으므로 더 좋습니다.

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

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

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

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

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

하위 클래스는 기본 클래스 특성을 위반해서는 안 됩니다.그것은 능력이 있어야합니다.하위 타이핑과 동일하다고 말할 수 있습니다.

모든 답변에는 직사각형과 정사각형이 표시되어 있으며 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 !
    }
}

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

Liskov의 대체 원리(LSP)

항상 프로그램 모듈을 설계하고 클래스 계층을 만듭니다.그런 다음 파생 된 클래스를 만드는 일부 수업을 확장합니다.

새로운 파생 클래스가 구식 기능을 대체하지 않고 확장해야합니다.그렇지 않으면 새로운 클래스는 기존 프로그램 모듈에 사용될 때 바람직하지 않은 효과를 생성 할 수 있습니다.

Liskov의 대체 원칙에 따르면 프로그램 모듈이 기본 클래스를 사용하는 경우 기본 클래스에 대한 참조를 프로그램 모듈의 기능에 영향을 미치지 않고 파생 클래스로 대체 할 수 있습니다.

예:

아래는 Liskov의 대체 원칙을 위반한 전형적인 예입니다.이 예에서는 2개의 클래스가 사용됩니다.직사각형과 정사각형.Rectangle 개체가 응용 프로그램의 어딘가에서 사용된다고 가정해 보겠습니다.애플리케이션을 확장하고 Square 클래스를 추가합니다.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.
    }
}

결론:

이 원칙은 개방형 근접 원칙의 확장 일 뿐이며 새로운 파생 클래스가 행동을 바꾸지 않고 기본 클래스를 확장하고 있는지 확인해야합니다.

또한보십시오: 개방형 폐쇄 원칙

더 나은 구조를 위한 몇 가지 유사한 개념: 구성에 대한 컨벤션

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

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

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

정사각형은 너비와 높이가 같은 직사각형입니다.정사각형이 너비와 높이에 대해 서로 다른 두 가지 크기를 설정하면 정사각형 불변성을 위반합니다.이는 부작용을 도입하여 해결됩니다.그러나 직사각형에 전제 조건이 0 < 높이이고 0 < 너비인 setSize(높이, 너비)가 있는 경우.파생 하위 유형 방법에는 높이 == 너비가 필요합니다.더 강한 전제 조건(그리고 이는 lsp를 위반함)이는 square가 직사각형이지만 전제 조건이 강화되었기 때문에 유효한 하위 유형이 아님을 보여줍니다.해결 방법(일반적으로 나쁜 것)은 부작용을 일으키고 이는 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);
    }
} 

우리가 교체하면 Rectangle ~와 함께 Square 첫 번째 코드에서는 다음과 같이 중단됩니다.

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를 위반하면 어느 시점에서 코드에 오류가 발생할 수 있습니다.

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

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

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 대체 원칙을 준수하면서 더욱 전문화됩니다.

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

그리고 우리 자전거 수업도 Liskov 대체 원칙을 준수합니다.

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

다음 기사를 읽어보시기 바랍니다. Liskov 대체 원칙(LSP) 위반.

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

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

LISKOV SUBSTITUTION PRINCIPLE(Mark Seemann 책에서 발췌)은 클라이언트나 구현을 손상시키지 않고 인터페이스의 한 구현을 다른 구현으로 교체할 수 있어야 한다고 명시합니다. 오늘은 예측할 수 없습니다.

컴퓨터를 벽에서 뽑으면(구현), 벽면 콘센트(인터페이스)도, 컴퓨터(클라이언트)도 고장 나지 않습니다. (사실 노트북이라면 일정 시간 동안 배터리로도 작동할 수 있습니다.) .그러나 소프트웨어를 사용하면 클라이언트가 서비스를 사용할 수 있을 것으로 기대하는 경우가 많습니다.서비스가 제거되면 NullReferenceException이 발생합니다.이러한 유형의 상황을 처리하기 위해“아무것도하지 않는”인터페이스 구현을 만들 수 있습니다. 이것은 Null 물체로 알려진 설계 패턴이며 [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();
}

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

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에 따르면 Earth 대신 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);
}

짜잔, 이제 계약이 만족되었습니다.

이러한 미묘한 위반으로 인해 클라이언트는 사용된 구체적인 개체 간의 차이를 구분할 수 있는 능력을 갖게 되는 경우가 많습니다.예를 들어, 첫 번째 계정의 계약이 주어지면 다음과 같을 수 있습니다.

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

그리고 이는 자동적으로 개방폐쇄원칙[즉, 자금인출요건에 대한 위반]을 위반하게 됩니다.계약을 위반한 물건에 돈이 충분하지 않으면 어떤 일이 일어날지 결코 알 수 없기 때문입니다.아마도 아무것도 반환하지 않을 것이고 아마도 예외가 발생할 것입니다.그래서 있는지 확인해야합니다 hasEnoughMoney() -- 인터페이스의 일부가 아닙니다.따라서 강제로 실행되는 구체적인 클래스 종속 검사는 OCP 위반입니다.]

이 점은 또한 LSP 위반에 관해 자주 접하게 되는 오해를 다루고 있습니다.그것은“부모의 행동이 아이에서 바뀌면 LSP를 위반한다”고 말합니다. 그러나 자녀가 부모의 계약을 위반하지 않는 한 그렇지 않습니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top