Programming/기타

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

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

가. 이해하기 어렵게 만드는 분기중첩

조건 분기는 조건에 따라 처리 방식을 다르게 하는데 사용되는 프로그래밍 언어의 기본 제어 구조입니다. 그런데 조건 분기를 어설프게 사용하면 악마가 되어 개발자를 괴롭힙니다.

 

잘못된 예

if ( 0 < member.hitPoint ) { 	//살아 있는가?
	if (member.canAct()) { 		//움직일 수 있는가?
    	if(magic.costMagicPoint <= member.magicPoint) {	//매직포인트가 있는가?
        	member.consumeMagicPoint(magic.costMagicPoint);
            member.chant(magic);
        }
    }
}

위와 같이 중첩이 많을수록 가독성이 나빠집니다.

 

1) 우선 중첩을 걷어내는 데 방법중 하나는 조기 리턴이 있습니다.

if (member.hitPoint <= 0) return;
if (!member.canAct()) return;
if (member.magicPoint < magic.costMagicPoint) return;

member.consumeMagicPoint(magic.costMagicPoint);
member.chant(magic);

위와 같이 코딩하면 조건 추가가 심플해집니다. 또는 실행 로직 추가가 간단해집니다.

 

다른 예를 보시지요

flaot hitPointRate = member.hitPoint / member.maxHitPoint;

if(hitPointRate == 0){
	return HealthCondition.dead;
}
else if (hitPointRate < 0.3){
	return HealthCondition.danger;
}
else if (hitPointRate < 0.5){
	return HealthCondition.caution;
}
else {
	return HealthCondition.fine;
}

아래와 같이 바로 리턴으로 풀면 else if가 필요가 없습니다.

float hitPointRate = member.hitPoint / member.maxHitPoint;

if (hitPointRate == 0) return HealthCondition.dead;
if (hitPointRate < 0.3) return HealthCondition.danger;
if (hitPointRate < 0.5) return HealthCondition.caution;

return HealthCondition.fine;

 

 

switch 조건문을 사용해서 코드 작성하는 경우에는 다음과 같은 이슈가 발생합니다.

 

잘못된 예

enum MagicType {
        fire,
        lighting
}
class MagicManager {
	String getName(MagicType magicType){
    	String Name = "";
        
        switch (magicType) {
        	case fire:
            	name = "파이어";
                break;
            case lightning:
            ..
        }
        return name;
    }
}
int costMagicPoint(MagicType magicType, Member member){
	int magicPoint = 0;
    
    switch (magicType){
    	case fire:
        	magicPoint = 2;
            break;
        case lightening:
        	magicPoint = 5 + (int)(member.level * 0.2);
            break;
    }
    return magicPoint;
}
int attackPower(MagicType magicType, Member member){
	int attackPower = 0;
    
    switch (maticType) {
    	case fire:
        	attackPower = 20 + (int)(member.level * 0.5);
            break;
        case lightening:
        	attackPower = 50 + (int)(member.level * 1.5);
            break;
    }
    return attackPower;
}

만약 이런 코드에서 새로운 마법 '헬파이어'가 추가된다면 개발자는 여러군데 case를 추가해야 하는 번거로움이 있습니다.

 

이때는 우선 조건 분기를 모읍니다. 단일 책임 선택의 원칙 을 적용합니다.

한번에 switch 구문을 작성하는 것입니다.

class Magic {
	final String name;
    final int costMagicPoint;
    final int attackPower;
    final int costTechnicalPoint;
    
    Magic(final MagicType magicType, final Member member){
    	switch (magicType) {
        	case fire:
                name = "파이어";
                costMagicPoint = 2;
                attackPower = 20 + (int)(member.level * 0.5);
                costTechinicalPoint = 0;
                break;
            case lightening: ...
            
            default:
            	throw new IllegalArgumentException();
        }
    }
}

2) 이렇게도 좋지만, 더 유지관리가 쉽도록 간결하게 바꾸어 봅시다. 여기서는 인터페이스를 쓰겠습니다. 인터페이스는 하위 상속 클래스들을  같은 자료형으로 사용할 수 있으므로, 굳이 자료형을 판정하지 않아도 됩니다.

 

올바른 예

interface Magic {
	String name();
	MagicPoint costMagicPoint();
	AttackPower attackPower();
	TechnicalPoint costTechnicalPoint();
}

위아 같이 인터페이스화 하여 규격화하면 Magic의 구현체에서 메서드를 빠뜨리는 실수를 방지할 수 있습니다.

그리고 추가적으로 값객체화하여 자료형의 고민도 없도록 만들었습니다.

class Fire implements Magic {
	private final Member member;
    
    Fire(final Member member){
    	this.member = member;
    }
    
    public String name(){
    	return "파이어";
    }
    
    public MagicPoint costMagicPoint(){
    	return new MagicPoint(2);
    }
    
    public AttackPower attackPower() {
    	final int value = 20 + (int)(member.level * 0.5);
    	return new AttackPower(value);
    }
    
    public TechnicalPoint costTechnicalPoint() {
    	return new TechnicalPoint(0);
    }
}

이런식으로 라이트닝, 헬파이어 Class를 작성한다...

그리고 이제 switch 조건문이 아니라 Map으로 변경한다.

final Map<MagicType, Magic> magics = new HashMap<>();
final Fire fire = new Fire(member);
final Lightning lightning = new Lightning(member);
final HellFire hellFire = new HellFire(member);

magics.put(MagicType.fire, fire);
magics.put(MagicType.lightning, lightning);
magics.put(MagicType.hellFire, hellFire);
final Map<MagicType, Magic> magics = new HashMap<>();
final Fire fire = new Fire(member);
final Lightning lightning = new Lightning(member);
final HellFire hellFire = new HellFire(member);

magics.put(MagicType.fire, fire);
magics.put(MagicType.lightning, lightning);
magics.put(MagicType.hellFire, hellFire);

void magicAttack(final MagicType magicType) {
	final Magic usingMagic = magics.get(magicType);
    
    showMagicName(usingMagic);
    consumeMagicPoint(usingMagic);
    consuemTechnicalPoint(usingMagic);
    magicDamage(usingMagic);
}

void showMagicName(final Magic magic){
	final String mame = magic.name();
}

void consumeMagicPoint(final Magic magic) {
	final int costMagicPoint = magic.costMagicPoint();
}

void consumeTechnicalPoint(final Magic magic) {
	final int costTechnicalPoint = magic.costTechnicalPoint();
}

void magicDamage(final Magic magic) {
	final int attackPower = magic.attackPower();
}