리스코프 치환 원칙(영어: Liskov substitution principle, LSP)은 바바라 리스코프가 자료 추상화와 계층 (Data abstraction and hierarchy)이라는 제목으로 기조연설을 한 1987년 컨퍼런스에서 처음 소개한 내용
자료형 {S}가 자료형 {T}의 하위형이라면, 프로그램에서 자료형 {T}의 객체는 프로그램의 속성을 변경하지 않고 자료형 {S}의 객체로 교체할 수 있다.
public interface Animal {
public void cry();
public void move();
public void MoveFast();
}
public class Dog implements Animal {
private String sould = "bowwow";
public void cry() {
System.out.println(sould);
}
public void move() {
this.walk();
}
public void MoveFast() {
this.run();
}
private void walk() {
System.out.println("Walking");
}
private void run() {
System.out.println("Running");
}
}
public class HoundDog extends Dog {
private String sould = "growl";
public void cry() {
System.out.println(sould);
}
}
public class Duck implements Animal {
private String sould = "Quack";
public void cry() {
System.out.println(sould);
}
public void move() {
this.swim();
}
public void moveFast() {
this.fly();
}
private void swim() {
System.out.println("Swimming Duck");
}
private void fly() {
System.out.println("Flying Duck");
}
}
public class TestAnimal {
public void main(string[] args) {
Animal animal = new HoudDog();
animal.cry();
animal.move();
animal.moveFast();
animal = new Duck();
animal.cry();
animal.move();
animal.moveFast();
}
}
표준적인 요구사항
하위형에서 메서드 인수(파라미터)는 하위호환만 허용한다.
하위형에서 메서드의 인수(파라미터)는 하위호환(확장한 객체로 변경) 가능하다.
- 메서드 인수(파라미터)는 상위형에 선언된 인수 또는 해당 인수를 확장(Extends)한 경우에만 허용된다.
- Animal > Dog > HoundDog
- 상위 인수가 Dog 인 경우 하위 인수인 Animal을 사용할 수 없다.
- 왜? 모든 Animal이 Dog이 될 수 없으니까 부모에서 정의한 메소드의 오바리이딩이 안됨
public abstract class Animal {
protected String barkSound = "No Sound";
public void bark(Animal animal){
System.out.println(this.barkSound);
}
}
//동물을 상속받은 클래스 개
public class Dog extends Animal {
public Dog() {
this.barkSound = "Bow Wow";
}
public void getSound(Dog dog)
{
System.out.println("Dog : " + dog.barkSound);
}
}
메서드 인수(파라미터)의 반공변성(하위호환) 위반 사례 케이스
개(Dog) 클래스를 확장해서 사냥개(HoudDog)을 만드는데, getSound의 파라미터를 Dog 보다 상위의 Animal로 인자를 변경해서 오버라이드 시도하면, 아래와 같이 super.getSound(dog) 부분에서 부모 메소드를 찾을 수 없어 에러가 발생함.
메서드 인수(파라미터)는 상위형에 선언된 인수 또는 해당 인수를 확장(Extends)한 경우에만 허용한다는 규칙의 위반
아래와 같이 하위클래스로 선언하거나, 동일한 레벨로 선언하면 오류가 사라진다.
하위형에서 반환형(return)은 하위호완은 허용된다.
하위형에서 메소드의 반환형은 상위형으로만 변경 가능하다. ???- Dog를 반환해야 하는 메소드에서 Animal을 반환하는 것도 불가. 모든 동물이 개는 아니니까
- Dog를 반환해야 하는 메소드에서 사냥개를 반환하는 것은 허용됨
- 하위형에서 메서드는 상위형 메서드에서 던져진 예외의 하위형을 제외하고 새로운 예외를 던지면 안된다.
public interface WhoAmI {
public Dog call();
}
public class Dog extends Animal implements WhoAmI {
public Dog() {
this.barkSound = "Bow Wow";
}
public void getSound(Dog dog)
{
System.out.println("Dog : " + dog.barkSound);
}
// 1번. 허용 or 불가
public Dog call()
{
return this;
}
// 2번. 허용 or 불가
public Dog call()
{
return new HoundDog();
}
// 3번 허용 or 불가
public HoundDog call()
{
return new HoundDog();
}
// 4번 허용 or 불가
public HoundDog call()
{
return new Dog();
}
// 5번 허용 or 불가
public Animal call()
{
return new Animal();
}
}
- 하위형에서 선행조건은 강화될 수 없다.
상위형에서 정의된 인스턴스의 초기화 조건 등을 하위형에서 더 강력하게 제제하면 초기화에 실패하거나, 상위형에 정의된 기능을 사용할 때 정상 동작하지 않음 - 하위형에서 후행조건은 약화될 수 없다.
메모리 정리 등 후행 기능을 없애는 등의 행위는 객체(또는 로직)의 정상 동작을 방해한다. - 하위형에서 상위형의 불변조건은 반드시 유지되어야 한다.
동물로 상속받은 객체에서 동물의 필수 조건을 변경하면 안됨 - 상위형에서 예외를 던지는 경우 하위형에서는 상위형에서 던지는 예외보다 하위형의 예외만 던져야 한다.
- 상위형에서 NumberFormatException을 던지를 로직인데, 하위형에서는 상위형에서 던지지 않던 NullPointException을 던지면 안됨
- 상위형에서 IOException을 던지는 경우, 하위형에서 보다 보다 구체적인 FileIOException을 던지는 것은 OK
/*
정답
1. 허용 - 인터페이스 정의와 동일한 선언
2. 허용 - 인터페이스 정의보다 하위형의 객체 반환 가능
3. 허용 - 인터페이스 선언보다 하위형 반환으로 선언 가능
4. 불가 - 반환형 보다 상위형은 반환 불가
5. 불가 - 인터페이스 선언보다 상위형으로 선언 불가
*/
리스코프 치환 법칙을 지키지 않은 사례 - 직사각형과 정사각형
import org.springframework.stereotype.Service;
// 직사각형
// 가로, 세로를 입력받는다.
// 면적을 계산한다.
@Service
public class Rectangle {
int width = 0;
int height = 0;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
// 면적
public int getArea() {
return width * height;
}
}
import org.springframework.stereotype.Service;
/** 정사각형
직사각형을 확장해서 폭과 높이가 같도록 메소드를 오버라이드 한다.
*/
@Service
public class Square extends Rectangle {
public void setWidth(int width) {
this.width = width;
this.height = width;
}
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static junit.framework.TestCase.assertEquals;
@RunWith(SpringRunner.class)
@SpringBootTest
public class RectangleTest {
@After
public void cleanup ()
{
return;
}
@Test
public void 직사각형_정사각형_면적테스트 () throws Exception {
//given
Rectangle rect = new Rectangle();
//선언은 직사각형이지만, 하위 클래스인 정사각형으로 생성
Rectangle rect2 = new Square();
//when
rect.setWidth(10);
rect.setHeight(20);
rect2.setWidth(10);
rect2.setHeight(20);
System.out.println("Rect area : " + rect.getArea());
System.out.println("Rect2 area : " + rect2.getArea());
//then
assertEquals(200, rect.getArea());
assertEquals(200, rect2.getArea()); //실패 직사각형으로 선언했지만, 가로,세로가 같은 정사각형이됨
}
}
그럼 어떻게 이 문제를 해결할 것인가?
// 직사각형
// 가로, 세로를 입력받는다.
// 면적을 계산한다.
@Service
public abstract class Tetragon {
int width = 0;
int height = 0;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
// 면적
public int getArea() {
return width * height;
}
}
// 직사각형
@Service
public class Rectangle extends Tetragon {
}
// 정사각형
@Service
public class Square extends Tetragon {
public void setWidth(int width) {
this.width = width;
this.height = width;
}
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
테스트 케이스 작성
Square(정사각형)은 Rectangle(직사각형)으로 형변환이 불가능하게 된다.
테스트 케이스 수정
@SpringBootTest
public class NewRectangleTest {
@After
public void cleanup ()
{
return;
}
@Test
public void 직사각형_정사각형_면적테스트 () throws Exception {
//given
Rectangle rect = new Rectangle();
Square newSquare = new Square();
//when
rect.setWidth(10);
rect.setHeight(20);
newSquare.setWidth(10);
newSquare.setHeight(20);
System.out.println("Rect area : " + rect.getArea());
System.out.println("Rect2 area : " + newSquare.getArea());
//then
assertEquals(200, rect.getArea());
assertEquals(400, newSquare.getArea());
}
}
정사각형 객체를 직사각형으로 형변환해서 발생하는 Side Effect을 방지할 수 있다.
전체적인 코드의 복잡도는 거의 증가하지 않았다.
[참고 - 뭘 표현하려는 걸까?]
한국이나 외국이나 기술자는 용어를 어렵게 쓰고, 설명도 어렵게 하는게 국룰인가보다~
'프로그래밍 > OOP_Pattern_TDD' 카테고리의 다른 글
디자인패턴 - 팩토리 메소드 vs 추상 팩토리 (0) | 2022.07.27 |
---|---|
OOP - SOLID 원칙 5.의존관계 역전 원칙, Dependency Inversion Principle (0) | 2022.07.26 |
OOP - SOLID 원칙 4.인터페이스 분리 원칙(ISP, Interface Segregation Principle) (0) | 2022.07.26 |
OOP - SOLID 원칙 2.개방-폐쇄의 원칙(OCP, Open-Closed Principle) (0) | 2022.07.20 |
OOP - SOLID 원칙 1.단일 책임의 원칙(SRP, Single Responsibility Principle) (0) | 2022.07.20 |