class Figure {
enum Shape { RECTANGLE, CIRCLE };
// 태그 필드 - 현재 모양을 나타냄
final Shape shape;
// 다음 필드들은 모양이 사각형(RECTANGLE)일 때만 쓰임
double length;
double width;
// 다음 필드는 모양이 원(CIRCLE)일 때만 쓰임
double radius;
// 원용 생성자
Figure(double radius) {
shape = Shape.CIRCLE;
this.radius = radius;
}
// 사각형용 생성자
Figure(double length, double width) {
shape = Shape.RECTANGLE;
this.length = length;
this.width = width;
}
double area() {
switch(shape) {
case RECTANGLE:
return length * width;
case CIRCLE:
return Math.PI * (radius * radius);
default:
throw new AssertionError(shape);
}
}
}
위와 같은 태그 달린 클래스는 열거 타입 선언, 태그 필드, switch문 등 쓸데없는 코드가 많음
여러 구현이 섞여서 가독성도 나쁘고 메모리도 많이 사용함
엉뚱한 필드를 초기화해도 런타임에서 문제가 나오고 또다른 의미를 추가하려면 코드를 수정해야함
한마디로 태그 달린 클래스는 장황하고, 오류를 내기 쉽고, 비효율적임
태그 달린 클래스는 클래스 계층구조를 어설프게 흉내낸 아류일 뿐임
위의 예시를 활용하면, 가장 먼저 계층구조의 루트(root)가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언함
그런 다음 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가함
모든 하위 클래스에서 공통으로 사용하는 데이터 필드들도 전부 루트 클래스로 올림
즉 Figure
클래스에는 태그 값에 상관없는 메서드가 하나도 없고, 모든 하위 클래스에서 사용하는 공통 데이터 필드도 없음, 그 결과 루트 클래스에는 추상 메서드인 area
하나만 남음
그 다음 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의함
Figure
에서 원 클래스와 사각형 클래스를 만들면 됨
각 하위 클래스에는 각자의 의미에 해당하는 데이터 필드들을 넣음, 그런 다음 루트 클래스가 정의한 추상 메서드를 각자의 의미에 맞게 구현함
abstract class Figure {
abstract double area();
}
class Circle extends Figure {
final double radius;
Circle(double radius) { this.radius = radius; }
@Override double area() { return Math.PI * (radius * radius); }
}
class Rectangle extends Figure {
final double length;
final double width;
Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override double area() { return length * width; }
}
위처럼 루트 클래스의 코드를 건드리지 않고도 독립적으로 계층구조를 확장하고 함께 사용할 수 있음
타입 사이의 자연스러운 계층 관계를 반영할 수 있어서 유연성은 물론 컴파일타임 타입 검사 능력도 높여줌
만약 정사각형을 지원하도록 하려면 아래와 같이 간단히 반영할 수 있음
class Square extends Rectangle {
Square(double side) {
super(side, side);
}
}