깨끗한 코드를 작성하기 위해 SOLID 원칙을 배웠습니다 ③ ~리스코프의 대체 원칙~

이번은 SOLID의 리스코프의 치환 원칙에 대해 정리했습니다.

그 외의 기사는 이하.
깨끗한 코드를 작성하기 위해 SOLID 원칙을 배웠습니다 ① ~단일 책임의 원칙~
깨끗한 코드를 작성하기 위해 SOLID 원칙을 배웠습니다 ② ~ 오픈 폐쇄 원칙 ~
깨끗한 코드를 작성하기 위해 SOLID 원칙을 배웠습니다 ④ ~ 인터페이스 분리의 원칙 ~
깨끗한 코드를 작성하기 위해 SOLID 원칙을 배웠습니다 ⑤ ~ 의존성 역전의 원칙 ~

리스코프의 대체 원칙



Object should be replaceable with their subtypes without affecting the correctness of the program

이것을 직역하면 "객체는 프로그램의 정확성에 영향을주지 않고 그 서브 타입과 교환 가능해야합니다."라는 의미가됩니다.

서브타입은, 부모 클래스를 상속한 클래스를 말합니다. 요점은 [서브클래스]는 [부모클래스]라는 관계입니다.
ex.) 소형차는 차이다, 타조는 새이다


Bird(새) 클래스와 Bird 클래스를 계승한 Ostrich(타조) 클래스를 생각해 보겠습니다.
public class Bird {
  public void fly() {
    // Fly high!
  }
}

public class Ostrich extends Bird {
  public void fly() {
    // Unimplemented
    throw new RuntimeException();
  }

  public void run() {
    // Run fast!
  }
}

상속에 의해, Ostrich 클래스는 Bird 클래스의 메소드를 그대로 사용할 수 있습니다만, 타조는 하늘을 날 수 없기 때문에, fly 메소드를 예외로서 덧쓰기하고 있습니다.

아래에서 Bird 클래스의 인스턴스를 2개, Ostrich 클래스의 인스턴스를 하나 만들고 fly 메서드를 출력하는 BirdHabits라는 클래스를 생각해 보겠습니다.
public class BirdHabits {
  public static void main(String[] args) {
    Bird first = new Bird();
    Bird second = new Bird();
    Bird third = new Ostrich();
  }

  List<Bird> myBirds = new ArrayList<>();
  myBirds.add(first);
  myBirds.add(second);
  myBirds.add(third);

  for(Bird bird: myBirds) {
    System.out.println(bird.fly());
  }
}

Ostrich 클래스에서 만든 third 인스턴스의 fly가 호출될 때만 예외 처리가 실행됩니다.

이제 부모 클래스를 서브클래스로 바꾸자.
Ostrich 클래스의 인스턴스
Bird third = new Ostrich();

Bird 클래스의 인스턴스로 대체하면 어떻게됩니까?
Bird third = new Bird();

이하의 부분에서 3회 모두 Bird의 fly 메소드가 실행되어 버려, 클래스를 옮겨놓기 전과 프로그램의 행동이 바뀌어 버립니다.
for(Bird bird: myBirds) {
  System.out.println(bird.fly());
}

따라서 위의 예는 리스코프의 대체 원칙을 위반합니다.
이와 같이 타조(Ostrich)는 새(Bird)인 것은 확실합니다만, 새로부터 계승할 수 없는 행동(메소드)이 포함되어 있는 한, 리스코프의 치환 원칙적으로는 바람직하지 않은 설계라고 한다 됩니다.

해결책: 더 일반적인 부모 클래스를 생각합니다.



그렇다면 어떻게 원칙을 만족시키는 이상적인 설계가 되는가?
해결책은 더 일반적인 부모 클래스를 준비하는 것입니다.

지금까지의 예로 생각하면, 「새도 타조도 동물이다」이므로 Animal(동물) 클래스라고 하는 것을 새롭게 준비해 주게 됩니다.


Animal 클래스에서는 새롭게 move 메소드라고 하는 것을 만들어, 상속처의 클래스(Bird, Ostrich)로 각각의 메소드(fly, run)를 돌려줍니다.
public class Animal {
  public void move() {
    // move
  }
}

public class Bird extends Animal {
  public void move() {
    return this.fly();
  }
  public void fly() {
    // Fly high!
  }
}

public class Ostrich extends Bird {
  public void move() {
    return this.run();
  }

  public void run() {
    // Run fast!
  }
}

이렇게 하면 AnimalHabits 클래스 내에서 만든 인스턴스가 Bird 클래스의 것이든 Ostrich 클래스의 것이든 move 메서드는 문제없이 호출됩니다.
public class AnimalHabits {
  public static void main(String[] args) {
    Animal first = new Bird();
    Animal second = new Bird();
    Animal third = new Ostrich();
  }

  List<Animal> myAnimals = new ArrayList<>();
  myAnimals.add(first);
  myAnimals.add(second);
  myAnimals.add(third);

  for(Animal animal: myAnimals) {
    System.out.println(animal.move());
  }
}

이것으로 원칙을 만족시키는 설계가 되었습니다.

결론



함부로 와 계승을 잡으면 아픈 눈을 볼 가능성이 있다는 이야기군요. 앞으로 조심해.

참고서적・동영상



Crean Architecture Robert.C.Martin
SOLID Principles: Introducing Software Architecture & Design Sujith George

좋은 웹페이지 즐겨찾기