Programming/기타

코드 품질올리기, 코드 설계 - 2 분기중접 줄이기 (2/3)

armyost 2023. 8. 28. 17:47
728x90

조건 분기 중복과 중첩

인터페이스는 switch조건문의 중복을 제거할 수 있을 뿐만아니라 다중 중첩된 복잡한 분기를 제거하는데 활용할 수 있습니다. 

 

잘못된 예

/** 
* @return 골드 회원이라면 true
* @param history 구매이력
*/

boolean isGoldCustomer (PurchaseHistory history) {
	if (1000000 <= history.totalAmount) {
    	if (10 <= history.purchaseFrequencyPermonth) {
        	if (history.returnRate <= 0.001) {
            	return true;
            }
        }
    }
	return false;
}

/** 
* @return 실버 회원이라면 true
* @param history 구매 이력
*/

boolean isSilverCustomer (PurchaseHistory history) {
	if (10 <= history.purchaseFrequencyPerMonth) {
		if (history.returnRate <= 0.001) {
        	return true;
    	}
    }
	return false;
}
}

판정조건이 골드회원과 실버회원이 거의 같습니다. 만약 같은 판정로직을 재사용하려면 어떻게 해야할까요?

 

1) 정책 패턴으로 조건 집약하기

 

올바른 예

interface ExcellentCustomerRule {
	/**
    * @param history 구매이력
    * @return 조건을 만족하는 경우 true
    */
    boolean ok(final PurchaseHistory history);
}

골드회원이 되려면 3개의 조건을 만족해야 합니다.

class GoldCustomerPurchaseAmountRule implements ExcellentCustomerRule {
	public boolean ok(final PurchaseHistory history) {
    	return 1000000 <= history.totalAmount;
    }
}
class PurchaseFrequencyRule implements ExcellentCustomerRule {
	public boolean ok(final PurchaseHistory history) {
    	return 10 <= history.purchaseFrequencyPerMonth;
    }
}
class ReturnRateRule implements ExcellentCustomerRule {
	public boolean ok (final PurchaseHsitory history) {
    	return history.returnRate <= 0.001;
    }
}

이어서 정책 Class를 만듭니다.  add 메서드로 규칙을 집약하고, complyWithAll 메서드 내부에서 규칙을 모두 만족하는지 판정합니다.

class ExcellentCustomerPolicy {
	private final Set <ExcellentCustomerRule> rules;
    ExcellentCustomerPolicy(){
    	rule = new HashSet();
    }

	/** 
    *규칙추가
    *@param rule 규칙
    */
    void add(final ExcellentCustormerRule rule){
    	rules.add(rule);
    }
    
    /**
    * @param history 구매 이력
    * @return 규칙을 모두 만족하는 경우 true
    */
    boolean complyWithAll (final PurchaseHistory history) {
    	for (ExcellentCustomerRule each : rules) {
        	if (!each.ok(hisotry)) return false;
        }
        return true;
    }
}

Rule과 Policy를 사용해서 골드회원 판정로직을 개선했습니다.

class GoldCustomerPolicy {
	private final ExcellentCustomerPolicy policy;
	
    GoldCustomerPoilcy() {
        ExcellentCustomerPolicy goldCustomerPolicy = new ExcellentCustomerPolicy();
        goldCustomerPolicy.add(new GoldCustomerPurchaseAmountRule());
        goldCustomerPolicy.add(new PurchaseFrequencyRule());
        goldCustomerPolicy.add(new ReturnRateRule());
	}
    
    /**
    * @param history 구매 이력
    * @return 규칙을 모두 만족하는 경우 true
    */
    boolean complyWithAll(final PurchaseHistory history) {
    	return policy.complyWithAll(purchaseHistory);
    }

골드회원 조건이 집약된 클래스 구조입니다. 이후 골드회원 조건이 달라질경우 이 GoldCustomerPolicy만 변경하면 됩니다. 예를 들어 실버회원은 다음과 같이 만듭니다.

class SilverCustomerPoilcy {
	private final ExcellentCustomerPolicy policy;
    
    SilverCustomerPolicy() {
    	policy = new ExcellentCustomerPolicy();
        policy.add(new PurchaseFrequencyRule());
        policy.add(new ReturnRateRule());
    }
    
    /**
    * @param history 구매 이력
    * @return 규칙을 모두 만족하는 경우 true
    */
    boolean complyWithAll(final PurchaseHistory history){
    	return policy.complyWithAll(history);
    }
}

 

자료형을 확인하는 조건분기를 위해 사용하는 instanceof는 리스코프 치환 원칙이라는 소프트웨어 원칙을 위반합니다. 

※ 리스코프 치환 원칙 : 부모객체를 호출하는 동작에서 자식객체가 부모객체를 완전히 대체할 수 있다. 

 

잘못된 예

Money busySeasonFee;
if(hotelRates instanceof RegularRates) {
    busySeasonFee = hotelRates.fee().add(new Money(30000));
}
else if (hotelRates instanceof PremiumRates) {
    busySeasonFee = hotelRates.fee().add(new Money(50000));
}

 

이처럼 리스코프 치환 원칙을 위반하면 자료형 판정을 위한 조건 분기 코드가 점점 많아져서, 유지 보수하기 어려운 코드가 되어 버립니다. 인터페이스의 의미를 충분히 이해하지 못하고 사용하면 이와 같은 로직이 자주 만들어 집니다. 

 

올바른 예

interface HotelRates {
	Money fee();
    Money busySeasonFee();
}
class RegularRates implements HotelRates {
	public Money fee() {
    	return new Money(70000);
    }
    
    public Money busySeasonFee() {
    	return fee().add(new Money(30000));
    }
}
class PremiumRates implements HotelRates {
	public Money fee() {
    	return new Money(120000);
    }
    
    public Money busySeasonFee() {
    	return fee().add(new Money(50000));
    }
}

 

이제 요금 자료형 판정 로직이 필요가 없어졌습니다. 그냥 인터페이스 자료형으로 사용하면 됩니다.

Money busySeasonFee = hotelRates.busySeasonFee();