추상 클래스보다는 인터페이스를 우선하라
자바가 제공하는 다중 구현 방식은 (1) 인터페이스, (2) 추상 클래스가 있다.
(1) 인터페이스는 선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 어떤 클래스를 상속했든 같은 타입으로 취급된다.
(2) 추상 클래스는 추상클래스에서 정의한 메서드를 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
인터페이스 경우 예를 들어, 다른 부모를 상속하더라도 같은 implEx를 구현한다면 아래 클래스들은 같은 타입으로 취급된다.
public class ImplementExample1 extends parent1 implements implEx{
}
public class ImplementExample2 extends parent2 implements implEx{
}
(1) 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해넣을 수 있다.
인터페이스를 확장할 경우, 그 인터페이스가 요구하는 메서드를 추가하고, 클래스 선언에 implements 구문만 추가하면 끝이다.
반면 추상 클래스의 경우, 추상 클래스는 확장하길 원하는 두 클래스의 공통 조상이어야 한다. 이 방식은 새로 추가된 추상 클래스의 모든 자손이 이를 상속하게 된다.
(2) 믹스인 정의에 안성맞춤이다.
Mixin: 대상 타입의 주된 기능에 선택적 기능을 혼합한 것, 다른 클래스에서 이용할 메소드를 포함한 클래스
기존 클래스에 덧씌울 수 없다는 점, 두 부모를 섬길 수 없다는 점에서 추상 클래스는 적합하지 않다.
(3) 계층 구조가 없는 타입 프레임워크를 만들 수 있다
public interface Singer{
public void sing();
}
public interface SongWriter{
public void compose();
}
public interface SingerSongWriter extends Singer, SongWriter{
public void actSensitive();
}
위와 같이 가수, 작곡가를 확장해 싱어송라이터를 인터페이스로 정의하면 문제가 되지 않으며 새로운 메서드까지 추가한 제3의 인터페이스를 정의할 수도 있다.
만약 추상 클래스로 만들게 된다면, 두 개의 부모를 상속할 수 없기에 가능한 조합 전부를 각각 클래스로 정의한 고도비만 계층구조가 만들어질 수 있다.
(4) 디폴트 메소드
디폴트 메서드를 제공해 프로그래머들의 일감을 덜어줄 수 있다. 하지만 이 방식에도 다음과 같은 제약이 존재한다.
위에서 다뤘던 디폴트 메소드의 제약으로 인한 방식이다. 이 방식은 인터페이스와 추상 클래스의 장점을 모두 취하는 방법이다.
인터페이스로는 타입을 정의하고, 골격 구현 클래스는 나머지 메서드까지 구현한다. (템플릿 메서드 방식)
public interface Character {
public void move();
public void seat();
public void attack();
public void process();
}
//공통되는 부분은 부모 클래스에 캡슐화 시키고 달라지는 부분만 자식 클래스에서 구현
public abstract class AbstractCharacter implements Character{
@Override
public void move() {
System.out.println("걷다");
}
@Override
public void seat() {
System.out.println("앉다");
}
@Override
public void process() {
move();
seat();
attack();
}
}
public class Thief extends AbstractCharacter implements Character{
@Override
public void attack() {
System.out.println("표창을 던진다");
}
}
public class Wizard extends AbstractCharacter implements Character{
@Override
public void attack() {
System.out.println("마법봉을 휘두르다");
}
}
만약 추상 클래스 AbstractCharacter를 생성하지 않았다면, implements로 구현했을 것이며 이 방식은 공통되는 부분이 생길 수 있다.
//각각 구현하는 경우
public class Thief implements Character{
@Override
public void move() {
System.out.println("걷다"); //공통
}
@Override
public void seat() { //공통
System.out.println("앉다");
}
@Override
public void attack() {
System.out.println("표창을 던진다");
}
@Override
public void process() { //공통
move();
seat();
attack();
}
}
public class Wizard implements Character{
@Override
public void move() {
System.out.println("걷다");
}
@Override
public void seat() {
System.out.println("앉다");
}
@Override
public void attack() {
System.out.println("마법봉을 휘두르다");
}
@Override
public void process() {
move();
seat();
attack();
}
}