본문 바로가기
개발관련 서적 정리/Effective Java

아이템 20. 추상클래스보다 인터페이스를 우선하라

by Backchus 2023. 2. 5.

1. 자바 8부터 인터페이스도 디폴트 메서드를 제공할 수 있고 기존 클래스도 손쉽게 새로운 인터페이스를 구현해 넣을 수 있다.

import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public interface TimeClient {

    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year, int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
}

위와 같은 TimeClient인터페이스도 언젠가 시간이 지나다보면 변경을 하고싶어지는 경우가 있을 수 있습니다. 예를 들어 새로운 메서드를 추가하고싶지만 인터페이스에 메서드를 추가하게되면 TimeClient의 구현체들은 모두 TimeClient의 인터페이스에 새로 추가한 메서드를 모두 구현해줘야하는데 현실적으로 불가능합니다. 그래서 자바8 이후로 인터페이스에 default메서드를 구현하면 이러한 문제를 방지할 수 있습니다.

추가된 default 메서드
static ZoneId getZonedId(String zoneString) {
    try {
        return ZoneId.of(zoneString);
       } catch (DateTimeException e) {
        System.err.println("Invalid time zone: " + zoneString + "; using default time zone instead.");
        return ZoneId.systemDefault();
    } 
}

default ZonedDateTime getZonedDateTime(String zoneString) {
    return ZonedDateTime.of(getLocalDateTime(), getZonedId(zoneString));
}

default메서드를 추가해도 TimeClient의 구현체들은 이 default메서드를 재정의하지 않아도 됩니다.

2. 인터페이스는 믹스인(mixtin) 정의에 안성맞춤이다. (선택적인 기능 추가)

package me.whiteship.chapter04.item20.defaultmethod;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public class SimpleTimeClient implements TimeClient {

    private LocalDateTime dateAndTime;

    public SimpleTimeClient() {
        dateAndTime = LocalDateTime.now();
    }

    public void setTime(int hour, int minute, int second) {
        LocalDate currentDate = LocalDate.from(dateAndTime);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(currentDate, timeToSet);
    }

    public void setDate(int day, int month, int year) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime currentTime = LocalTime.from(dateAndTime);
        dateAndTime = LocalDateTime.of(dateToSet, currentTime);
    }

    public void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
    }

    public LocalDateTime getLocalDateTime() {
        return dateAndTime;
    }

    public String toString() {
        return dateAndTime.toString();
    }
}

예를들어 위와같이 TimeClient인터페이스를 구현하는 SimpleTimeClient클래스가 추가적으로 다른 인터페이스를 추가해서 구현을 할 수 있다.

여러개의 인터페이스를 구현가능

public class SimpleTimeClient implements TimeClient, AutoCliseable {
        // 코드 생략
}

3. 계층구조가 없는 타입 프레임워크를 만들 수 있다.

타입간에 계층구조가 명확하지 않은 경우가 있는데 그때 여러 인터페이스를 조합해서 하나의 타입으로 만들 수 있다.

Singer

public interface Singer {

    AudioClip sing(Song song);
}

Songwriter

public interface Songwriter {

    Song compose(int shartPosition);
}

SingerSongwriter

public interface SingerSongwriter extends Singer, Songwriter{

    AudioClip strum();
    void actSensitive();
}

가수와 작사의 관계는 계층관계가 아닙니다. 하지만 가수와 작사를 합쳐서 싱어송라이터라는 새로운 타입을 선언할 수 있습니다.

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

import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

// 코드 18-3 재사용할 수 있는 전달 클래스 (118쪽)
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }

    public void clear()               { s.clear();            }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty()          { return s.isEmpty();   }
    public int size()                 { return s.size();      }
    public Iterator<E> iterator()     { return s.iterator();  }
    public boolean add(E e)           { return s.add(e);      }
    public boolean remove(Object o)   { return s.remove(o);   }
    public boolean containsAll(Collection<?> c)
                                   { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
                                   { return s.addAll(c);      }
    public boolean removeAll(Collection<?> c)
                                   { return s.removeAll(c);   }
    public boolean retainAll(Collection<?> c)
                                   { return s.retainAll(c);   }
    public Object[] toArray()          { return s.toArray();  }
    public <T> T[] toArray(T[] a)      { return s.toArray(a); }
    @Override public boolean equals(Object o)
                                       { return s.equals(o);  }
    @Override public int hashCode()    { return s.hashCode(); }
    @Override public String toString() { return s.toString(); }
}

아이템 18에서 살펴본것과 같이 컴포지션을 사용하여 Set인터페이스를 구현하는 ForwardingSet은 캡슐화가 되어있기때문에 안전하게 사용가능합니다.

5. 구현이 명백한 것은 인터페이스의 디폴트 메서드를 사용해 프로그래머의 일감을 덜어 줄 수 있다.

import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;

public interface TimeClient {

    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year, int hour, int minute, int second);
    LocalDateTime getLocalDateTime();

    static ZoneId getZonedId(String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString + "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    default ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZonedId(zoneString));
    }

}