[리팩토링] 악취 19 : 상속 (2)

 

✍️ 악취 19 : 상속

객체지향에서 상속은 코드 재사용과 기능 확장이라는 장점이 있지만 상속이 적절하지 않은 케이도 분명 있다.
서브 클래스는 슈퍼 클래스의 모든 기능을 지원해야 한다.(List를 상속받는 Stack이 적절한가?)
서브 클래스는 슈퍼 클래스가 가진 기능적 규약을 위반해선 안 된다. (리스코프 치환 원칙)
서브 클래스는 슈퍼 클래스의 변경에 취약한 다르게 말하면 둘의 결합도가 매우 높다.
무엇보다 다중 상속이 불가능하다.

 

상속은 매우 유용한 기능이지만 적절하지 못한 구조라고 판단하면 위임의 형태로 변경하는 것이 유용할 때가 있다.

 

여기 상속 악취를 해결하기 위한 두 가지 리팩토링 기법이 있다.

1. "슈퍼 클래스를 위임으로 변경하기"

2. "서브 클래스를 위임으로 변경하기"

 

본 포스팅에서 관심있게 살펴볼 리팩토링은 "서브 클래스를 위임으로 변경하기"이다.

 

🍊 서브 클래스를 위임으로 변경하기

객체가 특정 상황에 예외적인 로직을 수행한다면 보통은 상속을 사용해서 일반적인 로직은 슈퍼 클래스에, 예외적인 로직은 서브 클래스에 작성한다. 하지만 상속은 반드시 한 클래스로부터만 받을 수 있기에 시간이 지남에 따라 슈퍼 클래스가 다른 클래스로 대체될 수 있다. 이처럼 상속 구조에서 상속이 아닌 구조로 변경해야할 위임을 사용할 수 있다. 

 

Customer 클래스와 이를 상속받아 기능을 확장한 VipCustomer가 있다.

@AllArgsConstructor
@Getter
public class Customer {

    protected int id;

    /**
     * 장기 고객
     */
    protected boolean isLongTermCustomer;

    /**
     * 멤버십 보유 여부
     */
    protected boolean hasMembership;

    public boolean canUseRoomService(){
        return isLongTermCustomer && hasMembership;
    }

    public double discountRate() {
        double discount = 0.1;
        if (hasMembership)
            discount += 0.1;

        return discount;
    }
}

public class VipCustomer extends Customer {

    private VipBenefit benefit;

    public VipCustomer(int id, boolean isLongTermCustomer, boolean hasMembership, VipBenefit benefit) {
        super(id, isLongTermCustomer, hasMembership);

        this.benefit = benefit;
    }

    @Override
    public boolean canUseRoomService() {
        return isLongTermCustomer || hasMembership;
    }

    @Override
    public double discountRate() {
        return super.discountRate() + benefit.getExtraDiscount();
    }

    public boolean canUsePrivateRoom() {
        return benefit.getAccessibleSpace().contains("PrivateRoom");
    }
}

@AllArgsConstructor
@Getter
public class VipBenefit {

    private double extraDiscount;

    private List<String> accessibleSpace;
}

 

만약 VipCustomer가 Customer 상속을 포기해야한다면 위임(Delegate)으로 대체할 수 있다. 기존 VipCustomer의 로직을 실행할 대리인 VipCustomerDelegate를 생성한다. 

@AllArgsConstructor
@Getter
public class VipCustomerDelegate {

    private VipBenefit benefit;

    public boolean canAccessLounge(boolean isLongTermCustomer, boolean hasMembership) {
        return isLongTermCustomer || hasMembership;
    }

    public double discountRate(double baseRate) {
        return baseRate + benefit.getExtraDiscount();
    }

    public boolean canUsePrivateRoom() {
        return benefit.getAccessibleSpace().contains("PrivateRoom");
    }
}

 

대리인 VipCustomerDelegate를 Customer에 추가해서 delegate가 존재하면 작업을 위임하고 없다면 본 로직을 수행한다.

@AllArgsConstructor
@Getter
public class Customer {

    private int id;

    private boolean isLongTermCustomer;

    private boolean hasMembership;

    /**
     * 서브 클래스를 위임으로
     */
    private VipCustomerDelegate delegate;

    public static Customer createCustomer(int id, boolean isLongTermCustomer, boolean hasMembership) {
        return new Customer(id, isLongTermCustomer, hasMembership);
    }

    public static Customer createCustomer(int id, boolean isLongTermCustomer, boolean hasMembership, VipBenefit benefit) {
        Customer customer = new Customer(id, isLongTermCustomer, hasMembership);
        customer.delegate = new VipCustomerDelegate(benefit);

        return customer;
    }

    public Customer(int id, boolean isLongTermCustomer, boolean hasMembership) {
        this.id = id;
        this.isLongTermCustomer = isLongTermCustomer;
        this.hasMembership = hasMembership;
    }

    public boolean canAccessLounge() {
        return delegate != null ? delegate.canAccessLounge(isLongTermCustomer, hasMembership)
                : isLongTermCustomer && hasMembership;
    }

    public double discountRate() {
        double discount = 0.1;
        if (hasMembership)
            discount += 0.1;

        return delegate != null ? delegate.discountRate(discount) : discount;
    }

    public boolean canUsePrivateRoom() {
        return delegate != null ? delegate.canUsePrivateRoom() : false;
    }
}