답변:
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 원칙 동기 부여 포스터를 확인하십시오 .
Square.setWidth(int width)
과 같이 구현 된 경우 왜 문제가 발생 this.width = width; this.height = width;
합니까? 이 경우 너비는 높이와 동일합니다.
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{}
Bird bird
. 플라이를 사용하려면 객체를 FlyingBirds로 캐스팅해야합니다.
Bird bird
사용할 수 없습니다 fly()
. 그게 다야. 를 전달해도이 Duck
사실 은 변하지 않습니다. 클라이언트가를 가지고 있다면, FlyingBirds bird
전달 되더라도 Duck
항상 같은 방식으로 작동해야합니다.
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의 명백한 위반입니다. 이 함수는 인수의 파생물에는 작동하지 않습니다.[...]
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.
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의 역설이 확장에 어떻게 적용되는지 이해하는 것이 직관적입니다.
이 정리에 대한 해석은 엔트로피 힘에 대한 일반화 된 개념 이해에 이들을 통합합니다.
모든 답변에 사각형과 사각형이 있으며 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 !
}
}
이제 하위 유형은 더 이상 동일한 결과를 생성하지 않으므로 동일한 방식으로 사용할 수 없습니다.
Database::selectQuery
지원하는 SQL의 하위 집합 만 지원 하도록 의미를 제한하는 한이 예제는 LSP를 위반하지 않습니다 . 그것은 실용적이지 않습니다 ... 즉, 예제는 여기에 사용 된 대부분의 다른 것보다 여전히 이해하기 쉽습니다.
Liskov를 위반했는지 여부를 확인하는 검사 목록이 있습니다.
체크리스트 :
기록 제약 : 메서드를 재정의하는 경우 기본 클래스에서 수정할 수없는 속성을 수정할 수 없습니다. 이 코드를 살펴보면 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 # 개발자입니다)에서는 가능하지 않으므로 신경 쓰지 않습니다.
참고:
LSP는 계약의 계약에 대한 규칙입니다. 기본 클래스가 계약을 충족시키는 경우 LSP 파생 클래스도 해당 계약을 충족해야합니다.
의사 파이썬에서
class Base:
def Foo(self, arg):
# *... do stuff*
class Derived(Base):
def Foo(self, arg):
# *... do stuff*
파생 객체에서 Foo를 호출 할 때마다 arg가 동일한 한 기본 객체에서 Foo를 호출하는 것과 동일한 결과를 제공하는 경우 LSP를 충족시킵니다.
2 + "2"
). "강력한 유형의"를 "정적 유형의"과 혼동 할 수 있습니까?
롱 부모 클래스를 확장 할 때 짧은 이야기,하자가, 실제적인 예를 들어 사각형의 직사각형 및 정사각형 사각형을두고, 당신은 정확한 부모 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가 보장되는지 여부를 의미합니다. 예를 들어,이 엄격한 의미에서 컴파일러에 관한 한 ThreeDBoard는 확실히 Board를 대체 할 수 있습니다.
LSP가 일반적으로 그보다 더 광범위하게 해석된다는 것을 알았지 만 개념에 대해 더 자세히 읽은 후에.
간단히 말해서, 클라이언트 코드가 포인터 뒤의 오브젝트가 포인터 유형이 아니라 파생 된 유형임을 "알고"있다는 것은 유형 안전에 제한되지 않습니다. LSP 준수는 객체의 실제 동작을 조사하여 테스트 할 수도 있습니다. 즉, 메소드 호출 결과 또는 오브젝트에서 발생한 예외 유형에 대한 오브젝트 상태 및 메소드 인수의 영향을 조사합니다.
다시 예제로 돌아가서 이론적 으로 보드 메소드는 ThreeDBoard에서 잘 작동하도록 만들 수 있습니다. 그러나 실제로는 ThreeDBoard가 추가하려는 기능을 방해하지 않으면 서 클라이언트가 제대로 처리하지 못하는 동작의 차이를 방지하기가 매우 어려울 것입니다.
이러한 지식을 바탕으로 LSP 준수 평가는 컴포지션이 상속이 아닌 기존 기능을 확장하는 데 더 적합한 메커니즘인지 판단하는 데 유용한 도구가 될 수 있습니다.
모든 사람들이 LSP가 기술적으로 무엇인지 다룬 것 같습니다. 기본적으로 하위 유형 세부 사항에서 추상화하고 수퍼 유형을 안전하게 사용할 수 있기를 원합니다.
따라서 Liskov에는 3 가지 기본 규칙이 있습니다.
서명 규칙 : 하위 유형의 모든 수퍼 유형의 구문을 구문 적으로 올바르게 구현해야합니다. 컴파일러가 확인할 수있는 것입니다. 더 적은 수의 예외를 발생시키고 최소한 수퍼 타입 메소드만큼 액세스 할 수있는 규칙이 있습니다.
방법 규칙 : 이러한 작업의 구현은 의미 적으로 양호합니다.
속성 규칙 : 개별 함수 호출 이상의 기능을 수행합니다.
이러한 모든 속성을 유지해야하며 추가 하위 유형 기능이 수퍼 유형 속성을 위반하지 않아야합니다.
이 세 가지를 처리하면 기본 항목에서 추상화되어 느슨하게 연결된 코드를 작성하는 것입니다.
출처 : Java의 프로그램 개발-Barbara Liskov
LSP 사용 의 중요한 예는 소프트웨어 테스트 입니다.
클래스 A가 B의 LSP 호환 서브 클래스 인 경우 B의 테스트 스위트를 재사용하여 A를 테스트 할 수 있습니다.
서브 클래스 A를 완전히 테스트하려면 테스트 케이스를 몇 개 더 추가해야하지만 최소한 모든 슈퍼 클래스 B의 테스트 케이스를 재사용 할 수 있습니다.
내 : 실현에 대한 방법은 무엇 맥그리거는 "테스트하기 위해 병렬 계층 구조의"를 호출 구축이입니다 ATest
클래스에서 상속합니다 BTest
. 그런 다음 테스트 케이스가 B 유형이 아닌 A 유형의 개체와 함께 작동하도록하려면 일부 형식의 주입이 필요합니다 (간단한 템플릿 방법 패턴이 수행함).
모든 서브 클래스 구현에 수퍼 테스트 스위트를 재사용하는 것은 실제로 이러한 서브 클래스 구현이 LSP 호환인지 테스트하는 방법입니다. 따라서 하위 클래스의 맥락에서 수퍼 클래스 테스트 스위트를 실행 해야 한다고 주장 할 수도 있습니다 .
Stackoverflow 질문 " 인터페이스 구현을 테스트하기 위해 일련의 재사용 가능한 테스트를 구현할 수 있습니까? "에 대한 답변도 참조하십시오.
자바로 설명해 봅시다 :
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() { ... }
}
이 LSP 공식은 너무 강력합니다.
S 유형의 각 객체 o1에 대해 T로 정의 된 모든 프로그램 P에 대해 o1이 o2로 대체 될 때 P의 동작이 변경되지 않도록 T 유형의 객체 o2가있는 경우 S는 T의 하위 유형입니다.
이것은 기본적으로 S가 T와 완전히 동일한 캡슐화 된 또 다른 구현을 의미합니다. 대담하고 성능이 P의 행동의 일부라고 결정할 수 있습니다 ...
따라서 기본적으로 후기 바인딩을 사용하면 LSP에 위배됩니다. 우리가 한 종류의 물건을 다른 종류의 물건으로 대체 할 때 다른 행동을 얻는 것이 OO의 요점입니다!
이 속성은 상황에 따라 다르며 프로그램의 전체 동작을 반드시 포함 할 필요는 없으므로 wikipedia에서 인용 한 공식 이 더 좋습니다.
아주 간단한 문장으로, 우리는 말할 수 있습니다 :
자식 클래스는 기본 클래스 특성을 위반하지 않아야합니다. 가능해야합니다. 서브 타이핑과 동일하다고 말할 수 있습니다.
리스 코프의 대체 원칙 (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 원칙의 확장 일 뿐이므로 새로운 파생 클래스가 동작을 변경하지 않고 기본 클래스를 확장하고 있는지 확인해야합니다.
참조 : 공개 원칙
더 나은 구조를위한 유사한 개념들 : 컨벤션에 대한 협약
일부 부록 :
파생 클래스가 준수 해야하는 기본 클래스의 Invariant, 전제 조건 및 사후 조건에 대해 아무도 작성하지 않은 이유가 궁금합니다. 파생 클래스 D를 기본 클래스 B에서 완전히 대체 할 수 있으려면 클래스 D가 특정 조건을 준수해야합니다.
따라서 파생자는 기본 클래스가 부과하는 위의 세 가지 조건을 알고 있어야합니다. 따라서 하위 유형 지정 규칙이 미리 결정됩니다. 즉, 'IS A'관계는 특정 규칙이 하위 유형에 의해 준수되는 경우에만 준수해야합니다. 이 규칙은 고정, 사전 승인 및 사후 조건의 형식으로 공식적인 ' 디자인 계약 '에 의해 결정되어야합니다 .
내 블로그에서 사용할 수있는 추가 토론 : Liskov 대체 원칙
간단히 말해서 LSP 는 같은 수퍼 클래스의 객체를 아무 것도 깨지 않고 서로 교환 할 수 있어야한다고 말합니다 .
우리는이 예를 들어, Cat
그리고 Dog
에서 파생 된 클래스 Animal
클래스, 동물 클래스를 사용하여 모든 기능을 사용할 수 있어야 Cat
또는 Dog
정상적으로 동작합니다.
보드 배열 측면에서 ThreeDBoard를 구현하는 것이 유용할까요?
아마도 다양한 평면에서 ThreeDBoard 조각을 보드로 취급 할 수 있습니다. 이 경우 여러 구현을 허용하기 위해 Board의 인터페이스 (또는 추상 클래스)를 추상화 할 수 있습니다.
외부 인터페이스와 관련하여 TwoDBoard 및 ThreeDBoard에 대한 보드 인터페이스를 제외 할 수 있습니다 (위의 방법 중 어느 것도 적합하지는 않음).
사각형은 너비가 높이와 같은 사각형입니다. 사각형이 너비와 높이에 대해 서로 다른 두 가지 크기를 설정하면 사각형 불변을 위반합니다. 이것은 부작용을 도입하여 해결됩니다. 그러나 사각형에 사전 조건이 0 <height 및 0 <width 인 setSize (height, width)가있는 경우. 파생 된 하위 유형 방법에는 높이 == 너비가 필요합니다. 더 강한 전제 조건 (그리고 그것은 lsp를 위반합니다). 이것은 사각형이 사각형이지만 전제 조건이 강화 되었기 때문에 유효한 부속 유형이 아님을 보여줍니다. 해결 방법 (일반적으로 나쁜 것)은 부작용을 일으키고 이것은 포스트 상태를 약화시킵니다 (lsp를 위반 함). 베이스의 setWidth는 사후 조건 0 <너비를 갖습니다. 파생은 높이 == 너비로 약화시킵니다.
따라서 크기 조정 가능한 사각형은 크기 조정 가능한 사각형이 아닙니다.
이 원칙은 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라는 또 다른 위반에 직면하게됩니다. 실제로 파생 클래스 사각형을 만들면 기본 클래스 사각형이 변경됩니다.
내가 지금까지 찾은 LSP에 대한 가장 명확한 설명은 "Liskov 대체 원칙은 파생 클래스의 객체가 시스템에서 오류를 일으키거나 기본 클래스의 동작을 수정하지 않고 기본 클래스의 객체를 대체 할 수 있어야한다고 말합니다. " 여기에서 . 이 기사는 LSP를 위반하고 수정하는 코드 예제를 제공합니다.
코드에서 사각형을 사용한다고 가정 해 봅시다.
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
없는 새로운 전제 조건 이 있기 때문 입니다. LSP에 따르면 인스턴스는 서브 클래스 인스턴스 로 대체 가능해야 합니다. 이러한 인스턴스는 인스턴스 유형 검사를 통과 하여 코드에서 예기치 않은 오류가 발생하기 때문입니다.Rectangle
width == height
Rectangle
Rectangle
Rectangle
이것은 위키 기사의 "하위 유형에서 전제 조건을 강화할 수 없습니다" 부분의 예 입니다 . 결론적으로 LSP를 위반하면 코드에 오류가 발생할 수 있습니다.
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 대체 원칙 위반 (LSP) 기사를 읽는 것이 좋습니다 .
Liskov 대체 원칙이 무엇인지, 이미 위반했는지 추측하는 데 도움이되는 일반적인 단서와 클래스 계층 구조를보다 안전하게 만드는 데 도움이되는 접근법의 예를 찾을 수 있습니다.
LISKOV SUBSTITUTION PRINCIPLE (Mark Seemann 저서)은 클라이언트 나 구현을 중단하지 않고 인터페이스의 한 구현을 다른 구현으로 교체 할 수 있어야한다고 말합니다. 오늘 그들을 예견하십시오.
벽에서 컴퓨터를 분리하면 (구현) 벽 콘센트 (인터페이스) 또는 컴퓨터 (클라이언트)가 고장 나지 않습니다 (실제로 랩톱 컴퓨터 인 경우 일정 시간 동안 배터리를 사용할 수 있음) . 그러나 소프트웨어를 사용하면 클라이언트는 종종 서비스가 제공 될 것으로 기대합니다. 서비스가 제거되면 NullReferenceException이 발생합니다. 이러한 유형의 상황을 처리하기 위해 "아무것도하지 않는"인터페이스의 구현을 만들 수 있습니다. 이것은 Null Object라고 알려진 디자인 패턴이며, [4] 벽에서 컴퓨터를 분리하는 것과 대략 일치합니다. 우리는 느슨한 결합을 사용하기 때문에 실제 구현을 문제없이 아무것도하지 않는 것으로 대체 할 수 있습니다.
이 게시물 에서 발췌 한 내용은 다음과 같습니다 .
[..] 몇 가지 원칙을 이해하기 위해서는 그것이 위반 된 시점을 인식하는 것이 중요합니다. 이것이 내가 지금 할 일입니다.
이 원칙을 위반한다는 것은 무엇을 의미합니까? 인터페이스로 표현 된 추상화에 의해 부과 된 계약을 객체가 충족시키지 못함을 의미합니다. 즉, 추상화를 잘못 식별했음을 의미합니다.
다음 예제를 고려하십시오.
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를 위반하는 것"이라고 말합니다. 그러나 자녀가 부모의 계약을 위반하지 않는 한 그렇지 않습니다.