[Effective Java] 20. 태그 형태의 클래스 구현 대신 상속을 사용

1 분 소요

Class가 특정 상태에 따라서 다르게 동작하는 식으로 구현되는 경우가 있다.
이런 특정 상태(태그)에 따라서 내부 구현이 달라진다면 상속으로 구분하는 것이 좋다.

아래는 이러한 구현의 예로 shape 라는 태그 내용에 따라서 클래스가 사용하는 멤버가 정해진다.

public class Figure {
	enum Shape {
		RECTANGLE, CIRCLE;
	}
	
	// 모양을 나타내는 태그
	final Shape shape;
	
	// RECTANGLE일때만 사용하는 필드
	double length, width;
	
	// CIRCLE일때만 사용하는 필드
	double radius;
	
	Figure(double radius) {
		share = Shape.CIRCLE;
		this.radius = radius;
	}
	
	Figure(double length, double width) {
		shape = Shape.RECTANGLE;
		this.length = length;
		this.width = width;
	}
	
	double area() {
		switch(shape) {
			case RECTANBLE:
				return length * width;
			case CIRCLE:
				return Math.PI * (radius * radius)
			default:
				throw new AssertionError();
		}
	}
}

1. 태그 형태 구현의 문제점

  1. enum 선언, 태그 필드, switch 문과 같은 상투적(boilerplate) 코드가 많아진다.

  2. 서로 다른 기능을 위한 코드가 한 class에 있어 가독성이 떨어진다.

  3. 필드를 잘못 사용하면 runtimeException이 발생할 위험이 있다.

  4. 새로운 기능을 추가하고자 할때 원본 class의 수정이 불가피하고, 수정할때는 모든 switch문에 case를 추가해야 한다.

  5. 클래스의 객체만 보고는 무슨 역할을 하는지 알수가 없다.

정리하면.. 난잡하고 버그 위험이 높으며 효율적이지도 않다.

2. 상속을 이용한 구현

태그에 따라서 다르게 동작하는 메서드를 abstract method로 분리하여 이를 abstract class로 만든다.

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, width;
	
	Rectangle(double length, double width) {
		this.length = length;
		this.width = width;
	}
	
	@Override
	double area() {
		return length * width;
	}
}

위와 같이 리팩토링 한 경우
단순 명료하고, boilerplate 코드도 없으며, 각각의 기능은 명확하게 클래스로 구분되어 있다. 또한 모든 필드는 final로 선언되어 있고 생성자는 이들을 적절하게 초기화 한다.

만약 새로운 기능을 추가하고자 할 경우 기존 class의 수정 없이 하위 클래스를 만들어 확장할 수도 있다.

댓글남기기