본문 바로가기
독서에서 한걸음

Replace Conditional with Polymorphism

by oncerun 2022. 12. 21.
반응형

 

조건부 로직을 다형성으로 변경해 보자.

 

복잡한 조건식을 상속과 다형성을 사용해 코드를 보다 명확하게 분리할 수 있다.

 

switch문을 사용해서 타입에 따라 각기 다른 로직을 사용하는 코드

 

기본 동작과 타입에 따른 특수한 기능이 섞여있는 경우에 상속 구조를 만들어서 기본 동작을 상위클래스에 두고 특수한 기능을 하위 클래스로 옮겨서 각 타입에 따른 차이점을 강조할 수 있다.

 

다만 단순한 조건문은 그대로 두어도 좋다. 오직 복잡한 조건문을 다형서울 활용해 좀 더 나은 코드로 만들 수 있는 경우에만 적용하자!

 

public class Employee {

    private String type;

    private List<String> availableProjects;

    public Employee(String type, List<String> availableProjects) {
        this.type = type;
        this.availableProjects = availableProjects;
    }

    public int vacationHours() {
        return switch (type) {
            case "full-time" -> 120;
            case "part-time" -> 80;
            case "temporal" -> 32;
            default -> 0;
        };
    }

    public boolean canAccessTo(String project) {
        return switch (type) {
            case "full-time" -> true;
            case "part-time", "temporal" -> this.availableProjects.contains(project);
            default -> false;
        };
    }
}

 

switch문 인경우이다.

 

public boolean canAccessTo(String project) {
    return switch (type) {
        case "full-time" -> true;
        case "part-time", "temporal" -> this.availableProjects.contains(project);
        default -> false;
    };
}

각각의 type마다 실행하는 로직이 달라지거나 특수한 상황이 추가된다면 이를 다형성을 활용할 수 있다.

 

이를 위해 부모 클래스를 미리 변경해 놓자.

public class Employee {

    protected String type;

    protected List<String> availableProjects;

    public Employee(String type, List<String> availableProjects) {
        this.type = type;
        this.availableProjects = availableProjects;
    }

    public Employee() {
    }

    public Employee(List<String> availableProjects) {
        this.availableProjects = availableProjects;
    }

    public int vacationHours() {
        return switch (type) {
            case "full-time" -> 120;
            case "part-time" -> 80;
            case "temporal" -> 32;
            default -> 0;
        };
    }

    public boolean canAccessTo(String project) {
        return switch (type) {
            case "full-time" -> true;
            case "part-time", "temporal" -> this.availableProjects.contains(project);
            default -> false;
        };
    }
}

 

자식 클래스인 part-time, temporal은 availdableProjects라는 필드가 필요할 것이다. 

다만 full-time인 경우 모든 프로젝트의 접근이 가능하기 때문에 별도의 프로젝트 리스트로 검증할 필요가 없다.

 

이에 따라 생성되는 자식 클래스들은 다음과 같다.

 

public class FullTimeEmployee extends Employee{

    @Override
    public int vacationHours() {
        return 120;
    }

    @Override
    public boolean canAccessTo(String project) {
        return true;
    }
}

public class PartTimeEmployee extends Employee{

    public PartTimeEmployee(List<String> availableProjects) {
        super(availableProjects);
    }

    @Override
    public int vacationHours() {
        return 80;
    }

}

public class TemporalEmployee extends Employee{

    public TemporalEmployee(List<String> availableProjects) {
        super(availableProjects);
    }

    @Override
    public int vacationHours() {
        return 32;
    }

}

 

이제 부모 클래스는 default 메서드는 기본 값을 반환하도록 하고 기존 type 필드를 제거해 주자.

public abstract class Employee {

    protected List<String> availableProjects;

    public Employee() {
    }

    public Employee(List<String> availableProjects) {
        this.availableProjects = availableProjects;
    }

    public abstract int vacationHours();

    public boolean canAccessTo(String project) {
        return this.availableProjects.contains(project);
    }
}

 

 

다음에는 조금 복잡한 예시이다.

 

public class VoyageRating {

    private Voyage voyage;

    private List<VoyageHistory> history;

    public VoyageRating(Voyage voyage, List<VoyageHistory> history) {
        this.voyage = voyage;
        this.history = history;
    }

    public char value() {
        final int vpf = this.voyageProfitFactor();
        final int vr = this.voyageRisk();
        final int chr = this.captainHistoryRisk();
        return (vpf * 3 > (vr + chr * 2)) ? 'A' : 'B';
    }

    private int captainHistoryRisk() {
        int result = 1;
        if (this.history.size() < 5) result += 4;
        result += this.history.stream().filter(v -> v.profit() < 0).count();
        if (this.voyage.zone().equals("china") && this.hasChinaHistory()) result -= 2;
        return Math.max(result, 0);
    }

    private int voyageRisk() {
        int result = 1;
        if (this.voyage.length() > 4) result += 2;
        if (this.voyage.length() > 8) result += this.voyage.length() - 8;
        if (List.of("china", "east-indies").contains(this.voyage.zone())) result += 4;
        return Math.max(result, 0);
    }

    private int voyageProfitFactor() {
        int result = 2;

        if (this.voyage.zone().equals("china")) result += 1;
        if (this.voyage.zone().equals("east-indies")) result +=1 ;
        if (this.voyage.zone().equals("china") && this.hasChinaHistory()) {
            result += 3;
            if (this.history.size() > 10) result += 1;
            if (this.voyage.length() > 12) result += 1;
            if (this.voyage.length() > 18) result -= 1;
        } else {
            if (this.history.size() > 8) result +=1 ;
            if (this.voyage.length() > 14) result -= 1;
        }

        return result;
    }

    private boolean hasChinaHistory() {
        return this.history.stream().anyMatch(v -> v.zone().equals("china"));
    }


}

 

변종을 찾는다. this.voyage.zone(). equals()에 따라 분기되거나 특수한 처리가 되는 것을 찾았다.

 

china인 경우 많은 특수처리가 된다. 이경우 부모 클래스를 사용할 것인지 새로 생성된 chinaVoyage를 쓸 건지 선택할 팩토리 클래스를 하나 더 만든다.

 

public class VoyageRatingFactory {

    public static VoyageRating createRating(Voyage voyage, List<VoyageHistory> history) {
        if (voyage.zone().equals("china") && hasChinaHistory(history)) {
            return new ChinaExperiencedVoyageRating(voyage, history);
        } else {
            return new VoyageRating(voyage, history);
        }
    }

    private static boolean hasChinaHistory(List<VoyageHistory> history) {
        return history.stream().anyMatch(v -> v.zone().equals("china"));
    }

}

 

이를 토대로 기존 클래스에서 함수를 옮겨야 한다.

 

TDD에서는 리팩토링 경우마다 테스트를 진행한다. 테스트를 진행할 때 우선순위는 가장 예외적인 상황과 가장 쉬운 상황이다. 이 경우 간단한 메서드부터 옮기기 시작한다.

 

protected int captainHistoryRisk() {
    int result = 1;
    if (this.history.size() < 5) result += 4;
    result += this.history.stream().filter(v -> v.profit() < 0).count();
    //제거 대상
    if (this.voyage.zone().equals("china") && this.hasChinaHistory()) result -= 2;
    return Math.max(result, 0);
}

 

china인 경우 result값에서 -2를 빼야 한다.

@Override
protected int captainHistoryRisk() {
    int result = super.captainHistoryRisk() - 2;
    return Math.max(result, 0);
}

 

if 문 한 줄을 제거했다.

 

private int voyageProfitFactor() {
    int result = 2;

    if (this.voyage.zone().equals("china")) result += 1;
    if (this.voyage.zone().equals("east-indies")) result +=1 ;
    if (this.voyage.zone().equals("china") && this.hasChinaHistory()) {
        result += 3;
        if (this.history.size() > 10) result += 1;
        if (this.voyage.length() > 12) result += 1;
        if (this.voyage.length() > 18) result -= 1;
    } else {
        if (this.history.size() > 8) result +=1 ;
        if (this.voyage.length() > 14) result -= 1;
    }

    return result;
}

 

복잡하다. 크기와 길이를 보고 있다. 이를 메서드로 추출하자.

protected int voyageAndHistoryLength(int result) {
    if (this.voyage.zone().equals("china") && this.hasChinaHistory()) {
        result += 3;
        if (this.history.size() > 10) result += 1;
        if (this.voyage.length() > 12) result += 1;
        if (this.voyage.length() > 18) result -= 1;
    } else {
        if (this.history.size() > 8) result +=1 ;
        if (this.voyage.length() > 14) result -= 1;
    }
    return result;
}

 

이제 하위 클래스에서는 this.voyage.zone(). equals("china") && this.hasChinaHistory() 조건을 가져가고

default로는 다음 코드를 남긴다.

if (this.history.size() > 8) result +=1 ;
if (this.voyage.length() > 14) result -= 1;
public class ChinaExperiencedVoyageRating extends VoyageRating{

    public ChinaExperiencedVoyageRating(Voyage voyage, List<VoyageHistory> history) {
        super(voyage, history);
    }

    @Override
    protected int captainHistoryRisk() {
        int result = super.captainHistoryRisk() - 2;
        return Math.max(result, 0);
    }

    @Override
    protected int voyageProfitFactor() {
        return super.voyageProfitFactor() + 3;
    }

    @Override
    protected int voyageLengthFactor() {
        int result = 0;
        if (this.voyage.length() > 12) result += 1;
        if (this.voyage.length() > 18) result -= 1;

        return result;
    }

    @Override
    protected int historyLengthFactor() {
        return this.history.size() > 10 ? 1 : 0;
    }
}
public class VoyageRating {

    protected Voyage voyage;

    protected List<VoyageHistory> history;

    public VoyageRating(Voyage voyage, List<VoyageHistory> history) {
        this.voyage = voyage;
        this.history = history;
    }

    public char value() {
        final int vpf = this.voyageProfitFactor();
        final int vr = this.voyageRisk();
        final int chr = this.captainHistoryRisk();
        return (vpf * 3 > (vr + chr * 2)) ? 'A' : 'B';
    }

    protected int captainHistoryRisk() {
        int result = 1;
        if (this.history.size() < 5) result += 4;
        result += this.history.stream().filter(v -> v.profit() < 0).count();
        return Math.max(result, 0);
    }

    private int voyageRisk() {
        int result = 1;
        if (this.voyage.length() > 4) result += 2;
        if (this.voyage.length() > 8) result += this.voyage.length() - 8;
        if (List.of("china", "east-indies").contains(this.voyage.zone())) result += 4;
        return Math.max(result, 0);
    }

    protected int voyageProfitFactor() {
        int result = 2;

        if (this.voyage.zone().equals("china")) result += 1;
        if (this.voyage.zone().equals("east-indies")) result +=1 ;

        result += historyLengthFactor();
        result += voyageLengthFactor();

        return result;
    }

    protected int voyageLengthFactor() {
        return this.voyage.length() > 14 ? -1 : 0;
    }

    protected int historyLengthFactor() {
        return this.history.size() > 8 ? 1 : 0;
    }

}

 

 

이제 특정 기능에 대한 책임은 자식에서 처리한다. 이에 대한 테스트 코드는 다음과 같다.

 


@Test
void westIndies() {
    VoyageRating voyageRating = VoyageRatingFactory.createRating(new Voyage("west-inides", 10),
            List.of(new VoyageHistory("east-indies", 5),
                    new VoyageHistory("west-indies", 15),
                    new VoyageHistory("china", -2),
                    new VoyageHistory("west-africa", 7)));
    assertEquals('B', voyageRating.value());
}

@Test
void china() {
    VoyageRating voyageRating = VoyageRatingFactory.createRating(new Voyage("china", 10),
            List.of(new VoyageHistory("east-indies", 5),
                    new VoyageHistory("west-indies", 15),
                    new VoyageHistory("china", -2),
                    new VoyageHistory("west-africa", 7)));
    assertEquals('A', voyageRating.value());
}

 

반응형

'독서에서 한걸음' 카테고리의 다른 글

Lazy Element  (0) 2022.12.24
Repeated Switches  (0) 2022.12.22
타입 코드를 서브클래스로 변경하기  (0) 2022.12.19
Primitive Obsession  (0) 2022.12.19
Shotgun Surgery  (0) 2022.12.18

댓글