public class Call { // 개별 통화 기간을 저장
private LocalDateTime from;
private LocalDateTime to;
public Call(LocalDateTime from, LocalDateTime to) {
this.from = from;
this.to = to;
}
public Duration getDuration() {
return Duration.between(from, to);
}
public LocalDateTime getFrom() {
return from;
}
}
public class Phone { // Call의 목록을 관리하며 통화 요금을 계산
private Money amount; // 단위 요금
private Duration seconds; // 단위 시간
private List<Call> calls = new ArrayList<>();
public Phone(Money amount, Duration seconds) {
this.amount = amount;
this.seconds = seconds;
}
public void call(Call call) {
calls.add(call);
}
public List<Call> getCalls() {
return calls;
}
public Money getAmount() {
return amount;
}
public Duration getSeconds() {
return seconds;
}
// seconds 당 amount씩 부과되는 요금제에서 통화 요금을 계산
// 통화 시간 / 단위 시간 * 단위 요금
public Money calculateFee() {
Money result = Money.ZERO;
for(Call call : calls) {
result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
}
return result;
}
}
NightlyDiscountPhone
이라는 새 클래스를 만드는 것이다.public class NightlyDiscountPhone {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount; // 10시 이후 적용할 통화 요금
private Money regularAmount; // 10시 이전 적용할 통화 요금
private Duration seconds;
private List<Call> calls = new ArrayList<>();
public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds) {
this.nightlyAmount = nightlyAmount;
this.regularAmount = regularAmount;
this.seconds = seconds;
}
public Money calculateFee() {
Money result = Money.ZERO;
for(Call call : calls) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
result = result.plus(nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
} else {
result = result.plus(regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
}
}
return result;
}
}
Phone
과 NightlyDiscountPhone
양쪽 모두 수정해야 한다.public class Phone {
// ...
private double taxRate;
public Phone(Money amount, Duration seconds, double taxRate) {
// ...
this.taxRate = taxRate;
}
// ...
public Money calculateFee() {
Money result = Money.ZERO;
for(Call call : calls) {
result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
}
return result.plus(result.times(taxRate));
}
}
public class NightlyDiscountPhone {
// ...
private double taxRate;
public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds, double taxRate) {
// ...
this.taxRate = taxRate;
}
public Money calculateFee() {
Money result = Money.ZERO;
for(Call call : calls) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
result = result.plus(nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
} else {
result = result.plus(regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
}
}
return result.minus(result.times(taxRate));
}
}
Phone
에서는 plus
메서드로 세금을 더했지만 NightlyDiscountPhone
에선 minus
를 호출하고 있다.public class Phone {
private static final int LATE_NIGHT_HOUR = 22;
enum PhoneType { REGULAR, NIGHTLY }
private PhoneType type;
// ...
public Money calculateFee() {
Money result = Money.ZERO;
for(Call call : calls) {
if (type == PhoneType.REGULAR) {
result = result.plus(amount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
} else {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
result = result.plus(nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
} else {
result = result.plus(regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
}
}
}
return result;
}
}
public class NightlyDiscountPhone extends Phone {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
public NightlyDiscountPhone(Money nightlyAmount, Money regularAmount, Duration seconds) {
super(regularAmount, seconds);
this.nightlyAmount = nightlyAmount;
}
@Override
public Money calculateFee() {
Money result = super.calculateFee();
Money nightlyFee = Money.ZERO;
for(Call call : getCalls()) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
nightlyFee = nightlyFee.plus(
getAmount().minus(nightlyAmount).times(
call.getDuration().getSeconds() / getSeconds().getSeconds()));
}
}
return result.minus(nightlyFee);
}
}
NightlyDiscountPhone
이 Phone
을 상속하면 중복을 제거할 수 있다.calculateFee
메서드
caculateFee
로 계산하고 10시 이후 통화 요금을 빼주고 있다.Phone
에서 처리하기로 결정했기 때문에 10시 이전 요금 계산 시 필요한 regularAmount
와 seconds
를 Phone
에게 전달한 것public class Phone {
// ...
public Money calculateFee() {
// ...
return result.plus(result.times(taxRate));
}
}
public class NightlyDiscountPhone extends Phone {
// ...
@Override
public Money calculateFee() {
// ...
return result.minus(nightlyFee.plus(nightlyFee.times(getTaxRate())));
}
}
NightlyDiscountPhone
이 Phone
의 구현에 너무 강하게 결합되어 있기 때문에 발생하는 문제다.상속을 위한 경고 1 - 자식 클래스 메서드 안에서 super 참조를 이용해 부모를 직접 호출할 경우 두 클래스는 강하게 결합된다. super 호출을 제거할 수 있는 방법을 찾아 결합도를 제거하라.
java.util.Properties
java.util.Stack
Stack
은 스택 자료 구조를 구현한 클래스
Vector
리스트 자료 구조를 상속Vector
는 임의의 위치에 요수를 삽입, 추출할 수 있기에 퍼블릭 인터페이스로 add
와 remove
가 존재한다.Vector
를 상속한 Stack
에도 당연히 add
와 remove
가 노출되어 있어 스택의 규칙을 위반할 수 있게 된다.stack.add(0, “4th”)
상속을 위한 경고2 - 상속받은 부모 클래스 메서드가 자식 클래스 내부 구조에 대한 규칙을 깨트릴 수 있다.
HashSet
구현에 강하게 결합된 InstrumentHashSet
클래스를 소개한다.
상속을 위한 경고 3 - 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 경우 부모 클래스가 자신의 메서드를 사용하는 방법에 자식 클래스가 결합될 수 있다.
상속을 위한 경고 4 - 클래스를 상속하면 결합도로 인해 자식 클래스와 부모 클래스 구현을 영원히 변경하지 않거나 자식 클래스와 부모 클래스를 동시에 변경하거나 둘 중 하나를 선택할 수밖에 없다.
Phone
과 NightlyDiscountPhone
의 경우
Call
을 순회하며 결과를 집계하는 로직은 같다.public class Phone {
// ...
public Money calculateFee() {
Money result = Money.ZERO;
for(Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return result;
}
private Money calculateCallFee(Call call) {
return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
public class NightlyDiscountPhone {
// ...
public Money calculateFee() {
Money result = Money.ZERO;
for(Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return result;
}
private Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return result.plus(nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
} else {
return result.plus(regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds()));
}
}
}
public abstract class AbstractPhone {
private List<Call> calls = new ArrayList<>();
public Money calculateFee() {
Money result = Money.ZERO;
for(Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return result;
}
abstract protected Money calculateCallFee(Call call);
}
public class Phone extends AbstractPhone {
private Money amount;
private Duration seconds;
// ...
@Override
protected Money calculateCallFee(Call call) {
return amount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
public class NightlyDiscountPhone extends AbstractPhone {
private static final int LATE_NIGHT_HOUR = 22;
private Money nightlyAmount;
private Money regularAmount;
private Duration seconds;
// ...
@Override
protected Money calculateCallFee(Call call) {
if (call.getFrom().getHour() >= LATE_NIGHT_HOUR) {
return nightlyAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
} else {
return regularAmount.times(call.getDuration().getSeconds() / seconds.getSeconds());
}
}
}
AbstractPhone
: 전체 통화 목록을 계산하는 방법이 바뀔 경우Phone
: 일반 요금제의 통화 한 건을 계산하는 방식이 바뀔 경우NightlyDiscountPhone
: 심야 할인 요금제의 통화 한 건을 계산하는 방식이 바뀔 경우calculateCallFee
Phone
은 일반 요금제와 관련된 내용이라는 것을 명시적으로 전달하지 못한다.
NightlyDiscountPhone
은 심야 요금제라는 것이 명확하다.Phone
을 RegularPhone
으로 변경하는 것이 적절하다.AbstactPhone
이 Phone
이 되면 명확해진다.Phone
을 수정하면 자식 클래스가 공유할 수 있을 것이다.public abstract class Phone {
private double taxRate;
private List<Call> calls = new ArrayList<>();
public Phone(double taxRate) {
this.taxRate = taxRate;
}
public Money calculateFee() {
Money result = Money.ZERO;
for(Call call : calls) {
result = result.plus(calculateCallFee(call));
}
return result.plus(result.times(taxRate));
}
protected abstract Money calculateCallFee(Call call);
}
Phone
에 taxRate
새로운 변수를 추가했기에 자식 클래스 생성자 역시 수정해야 한다.
taxRate
를 추가적으로 받고 super
를 호출해야 한다.