태그 클래스란 두 가지 이상의 의미를 표현할 때, 그 중 현재 표현하는 의미를 태그(플래그)값으로 알려주는 클래스다.
📌 살면서 처음 본 태그 달린 클래스
책에 나온 원과 사각형을 표현할 수 있는 클래스다. (이런 식으로 코드를 작성하는 개발자가 있다고?)
class Figure {
enum Shape { RECTANGLE, CIRCLE };
// 태그 필드
final Shape shape;
// shape가 RECTANGLE일때만 사용
double length;
double width;
// shape이 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문 등)가 너무 많아서 가독성이 안 좋다.
- 다른 의미를 위한 코드가 언제하 함께 있어서 메모리도 많이 사용한다.
- 필드들을 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화 해야 한다.
- 생성자가 태그 필드를 설정하고 해당 의미에 쓰이는 데이터 필드를 초기화하는 데 컴파일러가 도와줄 수 있는 건 별로 없다. (엉뚱한 필드를 초기화해도 런타임에야 문제가 드러난다)
- 인스턴스 타입만으로는 현재 나타내는 의미를 알 수 없다. (SRP인 단일 책임 원칙을 위배하고 있다.)
장황하고, 오류를 내기 쉽고, 비효율적인 쓰레기같은 코드다.
아니, 애초에 객체 지향 처음 공부할 때 SOLID 원칙 정도는 공부하지 않나...????
이런 거에 이름씩이나 붙여주네.
📌 클래스 계층 구조
타입 하나로 다양한 의미의 객체를 표현하는 훨씬 나은 수단이 있다.
클래스 계층 구조를 활용하는 서브타이핑(subtyping)이다.
public abstract class Figure {
public abstract double area();
}
public class Circle extends Figure {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * (radius * radius);
}
}
public class Rectangle extends Figure{
private final double length;
private final double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public double area() {
return length * width;
}
}
태그 달린 클래스 → 클래스 계층 구조
- 계층 구조의 root가 될 추상 클래스 정의
- 태그 값에 따라 동작이 달라지는 메서드들을 루트 클래스의 추상 메서드로 선언
- Figure 클래스의 area가 이런 메서드에 해당한다.
- 태그 값에 상관없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가
- 하위 클래스에서 공통으로 사용하는 데이터 필드도 전부 루트 클래스로 올리기
- 루트 클래스를 확장한 구체 클래스를 의미별로 하나씩 정의
장점
- 독립된 의미를 가지는 상태값 필드들이 제거되어 각 구현 클래스가 간결해진다.
- 살아남은 field는 모두 final이므로 정의할 수 있다.
- 실수로 빼먹은 case 문으로 인한 런타임 에러를 방지할 수 있다.
- 최상위 클래스를 수정하지 않고 타입을 확장할 수 있다.
- 타입 사이의 자연스러운 계층 관계를 반영할 수 있어 유연성은 물론 컴파일 타임 타입 검사 능력을 높여준다.
유연성
정사각형도 지원하도록 간단하게 수정 가능
public class Square extends Figure{
private final double side;
public Square(double side) {
this.side = side;
}
@Override
public double area() {
return side * side;
}
}