본문 바로가기

Dev Book Review/Effective Java

[Effective Java] Item20. 추상 클래스 보다는 인터페이스를 우선하라

자바의 다중 구현 메커니즘 : 둘다 인스턴스 메서드를 구현 형태로 제공할 수 있다 (default method)

  • 인터페이스 : 다중 상속, 같은 타입 취급
  • 추상클래스 : 단일 상속, 하위 클래스 (상하 관계)

 

1. 인터페이스의 장점

a. 기존 클래스에도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.

  • 인터페이스 : 요구하는 메서드를 추가하고, 클래스 선언에 implements 구문만 추가하면 끝이다.
  • 추상클래스 : 계층 구조상 확장시킨 클래스의 공통 조상이 되어, 클래스 계층 구조를 생각해야한다.
 

b. 믹스인 정의에 안성맞춤이다.

믹스인 : 클래스가 구현할 수 있는 타입.
믹스인을 구현한 클래스에 원래의 '주된 타입'외에도 선택적 기능을 '혼합'하여 사용한다.

추상클래스 : 기존 클래스에 덧씌울 수 없다. 클래스가 두 부모를 섬길 수 없는 단일 상속의 특징이 있다.

Comparable, Clonable, Serializable

 

c. 인터페이스로는 계층 구조가 없는 타입 프레임 워크를 만들 수 있다.

public interface SingerSongWriter extends Singer, SongWriter {
    void strum();
    void actSensitive();
}
public abstract class Singer {
    abstract void sing(String s);
}

public abstract class SongWriter {
    abstract void compose(int chartPosition);
}

public abstract class SingerSongWriter {
    abstract void strum();
    abstract void actSensitive();
    abstract void Compose(int chartPosition);
    abstract void sing(String s);
}

추상 클래스로 만들면 다중상속이 불가능해, 새로운 추상클래스를 만들어서 클래스 계층을 표현할 수 밖에 없다.

따라서 이 계층구조를 만들기 위해 많은 조합이 필요하고, 결국엔 고도비만 계층 구조가 만들어진다. (조합 폭발)

 

d. 래퍼 클래스 관용구와 함께 사용하면 인터페이스는 기능을 향상시키는 안전하고 강력한 수단이된다.

타입을 추상클래스로 정의했을 때 : 기능 추가 방법은 상속뿐이다. -> 활용도가 떨어지고 쉽게 깨진다

래퍼클래스의 활용도가 더 높다 : 아이템 18

 

2. 인터페이스의 디폴트 메서드 제약

  • 디폴트 메서드를 제공할 때는 @implSpec 을 붙여 문서화한다.
  • equals와 hashCode는 디폴트 메서드로 정의하면 안된다.
  • 인터페이스는 인스턴스 필드를 가질 수 없다.
  • public이 아닌 정적 멤버도 가질 수 없다.
  • 우리가 만들지 않은 인터페이스에는 디폴트 메서드를 추가할 수 없다.

 

3. 인터페이스와 추상골격 구현 클래스

a. 개념

인터페이스 : 타입 + 디폴트 메서드

골격구현 클래스 : 나머지 메서드들까지 구현

인터페이스 구현에 필요한 대부분의 일이 완료된다 -> 템플릿 메서드 패턴

네이밍 관례 : Abstract[Interface명] : AbstractCollection, AbstractSet, AbstractList, AbstractMap

 

b. 장점

추상 클래스처럼 구현을 도와주는 동시에, 추상클래스로 타입을 정의할 때 따라오는 심각한 제약에서 자유롭다.

 

4. 시뮬레이트한 다중 상속(simulated multiple inheritance)

골격 구현 클래스를 우회적으로 이용하는 방식

인터페이스를 구현한 클래스에서, 골격 구현을 확장한 private 내부 클래스를 정의하고, 각 메서드 호출을 내부 클래스의 인스턴스에 전달한다.

public interface Vending {
    void start();
    void chooseProduct();
    void stop();
    void process();
}

public abstract class AbstractVending implements Vending {
    @Override
    public void start() {
        System.out.println("vending start");
    }

    @Override
    public void stop() {
        System.out.println("stop vending");
    }

    @Override
    public void process() {
        start();
        chooseProduct();
        stop();
    }
}

public class VendingManufacturer {
    public void printManufacturerName() {
        System.out.println("Made By JavaBom");
    }
}

public class SnackVending extends VendingManufacturer implements Vending {
    InnerAbstractVending innerAbstractVending = new InnerAbstractVending();

    @Override
    public void start() {
        innerAbstractVending.start();
    }

    @Override
    public void chooseProduct() {
        innerAbstractVending.chooseProduct();
    }

    @Override
    public void stop() {
        innerAbstractVending.stop();
    }

    @Override
    public void process() {
        printManufacturerName();
        innerAbstractVending.process();
    }

    private class InnerAbstractVending extends AbstractVending {

        @Override
        public void chooseProduct() {
            System.out.println("choose product");
            System.out.println("chocolate");
            System.out.println("cracker");
        }
    }
}

 

5. 골격 구현 작성 방법

a. 인터페이스를 잘 살펴 다른 메서드들의 구현에 사용되는 기반 메서드를 선정

b. 기반 메서드들을 사용해 직접 구현할 수 있는 메서드들을 모두 디폴트 메서드로 제공

c. 기반 메서드나 디폴트 메서드로 만들지 못한 메서드가 남아있다면, 이 인터페이스를 구현하는 골격 구현 클래스를 하나 만들어서 작성한다.

d. 골격 구현은 기본적으로 상속이므로, 설계 및 문서화 지침을 모두 따라야한다.

 

6. 단순 구현

  • 골격 구현의 작은 변종
  • 골격 구현처럼 상속을 위해 인터페이스를 구현했으나 추상클래스가 아니다.
  • AbstractMap.SimpleEntry