상속을 계속 활용한다면 규격(조건)이 바뀔 때마다 서브클래스의 메서드를 전부 살펴보고 상황에 따라 오버라이드해야 하기 때문입니다.
위 클래스 다이어그램과 같이 Duck 이라는 슈퍼클래스를 상속하는 여러 오리 클래스가 있다고 합시다. 여기서 오리를 날게 하려면 어떻게 해야 할까요?
위 다이어그램처럼 모든 오리 클래스들은 Duck 클래스를 상속 받으니까 Duck 클래스에 fly() 메서드만 추가하면 되는걸까요?
만약 Duck 클래스를 상속하는 고무 오리 클래스가 있다면 어떻게 해야할까요? 날면 안되고 '꽥꽥'이 아니라 '삑삑' 소리가 나야 합니다. quack() 메서드를 오버라이드한 것처럼 fly() 메서드도 오버라이드해서 아무 동작도 하지 않게 해야할까요? 수백가지의 오리 클래스가 있다면, 오리마다 나는 방식이 달라지면 모든 오리 클래스를 확인해가면서 수정해야 합니다.
이처럼 상속은 변화하는 조건들에 대응하기엔 좋은 해결책이 아닙니다.
캡슐화
이제 해결책을 생각해봅시다. 이 상황에 딱 어울리는 디자인 원칙이 있습니다.
디자인 원칙
애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.
다시 말해 코드에 새로운 요구 사항이 있을 때마다 바뀌는 부분이 있다면 분리해야 합니다.
바뀌는 부분을 캡슐화해야 한다는 말과 같습니다. 그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 변경되는 부분만 수정하거나 확장할 수 있습니다.
이제 Duck 클래스에서 변화하는 부분들을 뽑아봅시다.
변화하지 않는 부분
변화하는 부분
swim(), display()
quack(), fly()
quack() 메서드와 fly() 메서드를 Duck 클래스에서 분리하기 위해 각 행동을 나타낼 클래스 집합을 새로 만듭니다.
인터페이스를 통해 2개의 메서드를 분리했습니다. 이제 다른 형식의 객체에서도 나는 행동과 꽥꽥 거리는 행동을 재사용할 수 있습니다. 그리고 기존의 행동 클래스를 수정하거나 날아다니는 행동을 사용하는 Duck 클래스를 전혀 건드리지 않고도 새로운 행동을 추가할 수 있습니다. 그런데 왜 인터페이스를 사용했을까요?
인터페이스
인터페이스를 통해 변화하는 부분을 분리하는 방법은 앞서 살펴본 Duck 클래스에서 quck() 이나 fly() 메서드를 구체적으로 구현하거나 서브 클래스 자체에서 별도로 구현하는 방법과는 상반된 방법입니다. 특정 구현에 의존하지 않고 코드 추가 없이 행동을 변경할 수 있습니다.
따라서 Duck 클래스와 서브 클래스들은 변화하는 행동들을 구체적으로 구현할 필요 없이 필요한 행동 인터페이스만 참조하면 됩니다.
디자인 원칙
구현보다는 인터페이스에 맞춰서 프로그래밍한다.
그럼 이제 코드로 구현해볼까요?
코드 예시
우선 Duck 클래스에 각 인터페이스 형식의 인스턴스 변수를 추가합니다. 각 오리 객체에서는 실행 시에 이 변수에 특정 행동 형식의 레퍼런스를 다형적으로 설정합니다. 그리고 Duck 클래스와 모든 서브 클래스에서 fly() 와 quack() 메서드를 제거합니다.