본문 바로가기
Language/Effective java

클래스와 인터페이스

by y.j 2022. 6. 17.
728x90

인터페이스는 구현하는 쪽을 생각해 설계하라.

 자바 8이후부터는 인터페이스도 디폴트 메서드를 구현 할 수 있게 되었지만 생각 할 수 있는 모든 상황에서 불변식을 해치지 않는 디폴트 메서드를 작성하기란 어렵다. removeIf의 예제를 보면 Predicate에서 true를 반환하는 것들을 제거한다.

default boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    boolean result = false;
    for(Iterator<E> it = iterator(); it.hasNext(); ) {
        if(filter.test(it.next)) {
            it.remove();
            result = true;
        }
    }
    return result;
}

이 코드보다 더 범용적으로 구현하는 것도 어렵고 현존하는 모든 Collection 구현체와 잘 어울러지지 않는다. 아파치의 SynchronizedCollection클래스에서 default removeIf를 사용할 때 동기화를 해주지 못한다.

디폴트 메서드는 컴파일이 성공하더라도 기존 구현체에 런타임 오류를 일으킬 수 있다. 기존에 디폴트 메서드를 추가하는 것은 기존 구현체들과 충돌하지 않을지 심사수고해야 한다. 하지만 새로운 인터페이스를 설계하는데는 강력한 수단이다.

 

인터페이스는 타입의 정의하는 용도로만 사용하라.

인터페이스는 자신을 구현한 클래스의 인스턴스를 참조 할 수 있는 타입 역할을 한다. 클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에 얘기해주는 것이다. 인터페이스는 오직 이용도로만 사용해야 한다.

 이 지침에 맞지 않는 예로 소위 상수 인터페이스라는 것이 있다. 상수 인터페이스란 메서드 없이 상수를 뜻하는 static final 필드로만 가득 찬 인터페이스를 말한다.

public interface PhysicalConstants<E> {
    
    static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    static final double ELECTRON_MASS = 9.109_383_56-31;
    
}

 위의 코드는 잘못된 예이다. 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아니라 내부 구현에 해당한다. 상수 인터페이스는 내부 구현을 노출하는 행위가 된다. 또, 더이상 쓰지 않더라도 호환성을 위해 여전히 구현하고 있어야 되며 final이 아닐경우 인터페이스에서 정의한 상수가 오염되어 버린다.

 상수를 공개할 목적으로 사용한다면 몇 가지 선택지가 있다. Interger나 Double의 MIN_VALUE나 MAX_VALUE가 그런 예이다. 열거 타입으로 내기 적합한 상수라면 열가 타입으로 만들어 공개하면 된다. 또, 인스턴스화 할 수 없는 유틸리티 클래스에 담아 공개해도 된다.

public class PhysicalConstants<E> {
    
    private PhysicalConstants() {} // 인스턴스화 방지
    
    static final double AVOGADROS_NUMBER = 6.022_140_857e23;
    
    static final double BOLTZMANN_CONSTANT = 1.380_648_52e-23;
    
    static final double ELECTRON_MASS = 9.109_383_56-31;
    
}

 

태그 달린 클래스보다는 클래스 계층구조를 잘 활용하라.

태그 달린 클래스

public class Figure {
    enum Shape { RECTANGLE, CIRCLE }
    
    final Shape shape;
    
    double length;
    double width;
    
    double radius;
    
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }
    
    double area() {
        switch (shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * ( radius * radius );
            default:
                throw new AssertionError(shape);
        }
    }
}

태그 달린 클래스의 단점

1. 열거 타입 선언, 태그 필드, switch문 등 쓸데없는 코드가 많다. 따라서 가독성이 떨어지고 메모리도 많이 사용한다. final필드를 쓰려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다.

2. 새로운 의미를 추가할 때마다 switch문을 찾아 새 의미를 처리하는 코드를 추가해야 한다.

3. 태그 달린 클래스는 클래스 계층 구조를 어설프게 흉내낸 아류이다.

 

위 코드를 계층구조로 바꿔보자!

Figure 클래스에 area()를 만들고 상속받아 재정의 하도록 한다.

 

Figure

abstract class Figure {
    abstract double area();
}

Rectangle

class Rectangle extends Figure {

    private final double length;
    private final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }


    @Override
    double area() {
        return length * width;
    }
}

Circle

public class Circle extends Figure {

    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }


    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}

 

태그 달린 클래스의 모든 단점을 날려 버린다. 가독성이 좋아지고 데이터와 관련 없는 필드들이 모두 제거되었다. case문으로 인해 오류가 발생할 일도 없다.

728x90

'Language > Effective java' 카테고리의 다른 글

제네릭  (0) 2022.06.18
클래스와 인터페이스  (0) 2022.06.17
클래스와 인터페이스  (0) 2022.06.14
클래스와 인터페이스  (0) 2022.06.14
클래스와 인터페이스  (0) 2022.06.12

댓글