Liskov 대체 원칙 (LSP)이 객체 지향 디자인의 기본 원칙이라고 들었습니다. 그것은 무엇이며 그 사용의 예는 무엇입니까?
답변
LSP (Univer Bob이 최근에 들었던 포드 캐스트에서 제공 한)를 보여주는 훌륭한 예는 자연 언어로 들리는 것이 코드에서 제대로 작동하지 않는 경우입니다.
수학에서 a Square
는입니다 Rectangle
. 실제로 그것은 사각형의 전문화입니다. “is a”는 상속으로 이것을 모델링하려고합니다. 그러나 코드에서 만든 코드 Square
에서 Rectangle
a Square
를 사용하면 어디에서나 사용할 수 있습니다 Rectangle
. 이것은 이상한 행동을합니다.
당신이 가지고 상상 SetWidth
하고 SetHeight
온 방법 Rectangle
기본 클래스; 이것은 완벽하게 논리적 인 것 같습니다. 귀하의 경우 Rectangle
참조가 지적 Square
, 다음 SetWidth
과 SetHeight
하나를 설정하면 그것을 일치 다른 변화 때문에 이해가되지 않습니다. 이 경우 Square
Liskov 대체 테스트에 실패하고 상속 Rectangle
을 Square
받는 추상화 Rectangle
는 좋지 않습니다.
모두 귀중한 다른 SOLID 원칙 동기 부여 포스터를 확인하십시오 .
답변
Liskov 대체 원리 (LSP, lsp)는 객체 지향 프로그래밍의 개념으로 다음과 같은 내용을 나타냅니다.
기본 클래스에 대한 포인터 또는 참조를 사용하는 함수는 몰래 파생 클래스의 객체를 사용할 수 있어야합니다.
LSP의 핵심은 인터페이스와 계약뿐만 아니라 수업을 연장 할시기를 결정하는 방법과 목표를 달성하기 위해 작곡과 같은 다른 전략을 사용하는 방법에 관한 것입니다.
이 점을 설명하는 가장 효과적인 방법은 Head First OOA & D 였습니다. 전략 게임을위한 프레임 워크를 구축하는 프로젝트의 개발자 인 시나리오를 제시합니다.
그들은 다음과 같은 보드를 나타내는 클래스를 제시합니다.
모든 메소드는 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를 위반하지 않습니다.
답변
대체 가능성은 컴퓨터 프로그램에서 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{}
답변
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
.
답변
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
. […]
Square
SetWidth
및SetHeight
함수 를 상속합니다 .Square
사각형의 너비와 높이가 동일하기 때문에 이러한 함수는에 적합 하지 않습니다. 이것은 디자인에 문제가 있다는 중요한 실마리가되어야합니다. 그러나 문제를 회피 할 수있는 방법이 있습니다. 우리는 무시SetWidth
하고SetHeight
[…]그러나 다음 기능을 고려하십시오.
void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }
Square
객체에 대한 참조를 이 함수에
전달Square
하면 높이가 변경되지 않기 때문에 객체가 손상됩니다. 이것은 LSP의 명백한 위반입니다. 이 함수는 인수의 파생물에는 작동하지 않습니다.[…]
답변
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
동일하거나 하위 유형 이어야합니다 .To
T
호출자가 타입을 가지고 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 법칙 : 전체 우주 (폐쇄 된 시스템, 즉 모든 것)는 최대 장애, 즉 최대 독립 가능성으로 경향이있다.
답변
모든 답변에 사각형과 사각형이 있으며 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 !
}
}
이제 하위 유형은 더 이상 동일한 결과를 생성하지 않으므로 동일한 방식으로 사용할 수 없습니다.