ITEM 20
추상 클래스보다는 인터페이스를 우선하라.
서론
자바가 제공하는 다중 구현 메커니즘은 인터페이스와 추상 클래스이다.
두 메커니즘의 차이점은 다음과 같다.
추상 클래스: 추상 클래스가 정의한 타입을 구현하는 클래스는 반드시 추상 클래스의 하위 클래스가 되어야 한다.
인터페이스:선언한 메서드를 모두 정의하고 그 일반 규약을 잘 지킨 클래스라면 다른 어떤 클래스를 상속했든 같은 타입으로 취급된다.
이러한 차이점이 생기는 이유는 자바는 다중 상속을 지원하지 않기 때문이다.
해당 부분은 백기선 라이브 스터디 8 주차 인터페이스에서 자세히 설명했습니다.
인터페이스의 장점
- 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.
- 인터페이스가 요구하는 메서드를 추가하고 클래스 선언에 implements 구문만 추가하면 끝이다 (Comparable, Iterable, AutoCloserable)
- 인터페이스는 믹스인(mixin) 정의에 안성맞춤이다.
- 주된 기능에 선택적 기능을 추가하는 것.
-
인터페이스로는 계층구조가 없는 타입 프레임워크를 만들 수 있다.
-
public interface SingerSongwriter extends Singer,Songwriter { void strum(); void actSensitive(); }
-
만약 해당 구조를 추상 클래스로 생성한다면 새로운 추상 클래스를 만들어서 클래스 계층을 표현해야 한다.
-
추상 클래스로 계층구조를 만들기 위해 많은 조합이 필요하고 결국엔 고도비만 계층구조가 만들어진다.
-
- 인터페이스는 기능을 향상하는 안전하고 강력한 수단이 된다.
- 래퍼 클래스 (아이템 18)
인터페이스의 제약
자바 8 이후부터 인터페이스 내에 default 메서드를 정의할 수 있다.
- 디폴트 메서드를 제공할 때는 @implSpec을 붙여 문서화 하자.
- equals와 hashCode는 디폴트 메서드로 정의하면 안 된다.
- 인터페이스는 인스턴스 필드를 가질 수 없다.
- public이 아닌 정적 멤버도 가질 수 없다.(private static 메서드는 예외)
- 내가 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수없다.
추상 클래스의 장점
앞서 살펴본 인터페이스의 제약 사항과 반대되는 몇 가지 특징을 가지고 있다.
- protected 추상 메서드를 정의할 수 있다.
- 인터페이스는 상수 프로퍼티만(public static) 가질 수 있지만 추상 클래스는 아니다.
템플릿 메서드 패턴(인터페이스+추상 골격 구현 클래스)
변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브 클래스에서 만들도록 한다.
템플릿 메서드 패턴을 사용하지 않은 코드
public interface Phone {
void booting(); //전원을 킵니다.
void greeting();// 핸드폰 별 인사말
void display();// 화면을 킵니다.
void process(); //핸드폰 실행 프로세스
}
public class IPhone implements Phone{
@Override
public void booting() {
System.out.println("전원을킵니다");
}
@Override
public void display() {
System.out.println("화면을킵니다");
}
@Override
public void greeting() {
System.out.println("hello IPhone");
}
@Override
public void process() {
booting();
display();
greeting();
}
public class GalaxyPhone implements Phone {
@Override
public void greeting() {
System.out.println("hello Galaxy");
}
@Override
public void booting() {
System.out.println("전원을킵니다");
}
@Override
public void display() {
System.out.println("화면을킵니다");
}
@Override
public void process() {
booting();
display();
greeting();
}
}
코드를 보면 중복 코드가 매우 많다.
해당 코드를 변경해보자.
일단 중복 코드는 booting() , display() , process()이다.
해당 코드를 추상 클래스로 묶어보자.
여기서 네이밍 패턴은 Abstract(name). class이다.
public abstract class AbstractPhone implements Phone{ // Abstract(Phone)
@Override
public void booting() {
System.out.println("booting");
}
@Override
public void display() {
System.out.println("turn on display");
}
@Override
public void process() {
booting();
greeting();
display();
}
}
생각을 해보자.
클라이언트 측에서 booting() , display() 메서드를 알고 있어야 할까?(public 접근 지시자가 맞을까??)
사용자 입장에선 process() 메서드만 알고 있어도 핸드폰이 작동하는 것이 좋을 것 같다.
이 처럼 내부에서는 재정의를 열어두고 외부 클라이언트에선 호출을 금하고 싶을 때 protected 접근자를 이용하자.
만약 재정의를 하지 않는다면 private static 메서드로(자바 9 이후)으로 인터페이스 내 에서 구현을 하자.
인터페이스에선 핵심 메서드만 정의
public interface Phone {
void process();
}
추상 클래스(추상 골격 구현 클래스)
public abstract class AbstractPhone implements Phone{
protected void booting() {
System.out.println("booting");
}
protected void display() {
System.out.println("turn on display");
}
protected abstract void greeting(); // 해당 부분만 구현체가 구현하게 한다..
@Override
public void process() {
booting();
greeting();
display();
}
}
구현체
public class IPhone extends AbstractPhone{
@Override
protected void greeting() {
System.out.println("hello IPhone");
}
}
public class GalaxyPhone extends AbstractPhone {
@Override
protected void greeting() {
System.out.println("hello Galaxy");
}
}
실행코드.
@Test
void process(){
Phone iPhone = new IPhone();
Phone galaxyPhone = new GalaxyPhone();
iPhone.process();
galaxyPhone.process();
}
결론
- 일반적으로 다중 구현용 타입으로는 인터페이스가 가장 적합하다.
- 복잡한 인터페이스라면 구현하는 수고를 덜어주는 골격 구현을 함께 제공하는 방법을 고려하자.
- 골격 구현은 가능한 인터페이스의 default 메서드로 제공하여 그 인터페이스를 구현한 모든 곳에서 활용하자.
'Book Review > effective-java' 카테고리의 다른 글
[ITEM22] 인터페이스는 타입을 정의하는 용도로만 사용하라 (0) | 2021.02.20 |
---|---|
[ITEM21] 인터페이스는 구현하는 쪽을 생각해 설계하라 (0) | 2021.02.18 |
[ITEM18] 상속보다는 컴포지션을 사용하라 (0) | 2021.02.14 |
[ITEM17]변경 가능성을 최소화하라 (0) | 2021.02.14 |
[ITEM16] public 클래스에서는 public 필드가 아닌 접근자 메서드를 사용하라 (0) | 2021.02.12 |