본문 바로가기
Study/Refactoring

리팩터링 챕터 10

by irerin07 2023. 8. 28.
728x90

10장

조건부 로직 간소화

10.1 조건문 분해하기

배경

  • 조건부 로직이 복잡하면 프로그램을 복잡하게 만든다.
  • 코드를 부위별로 분해하고 각 덩어리의 의도를 살린 이름의 함수 호출로 바꿔 전체적인 의도를 살린다.

절차

  1. 조건식과 그 조건식에 딸린 조건절 각각을 함수로 추출한다.

예시

if (!aDate.isBefore(plan.getSummerStart()) && !aDate.isAfter(plan.getSummerEnd())) {
    charge = quantity * plan.getSummerRate();
  } else {
    charge = quantity * plan.getRegularRate() + plan.getRegularServiceCharge();
  }
1. 조건 부분을 별도 함수로 추출하기
if (summer(LocalDate.now(), plan)) {
  charge = quantity * plan.getSummerRate();
} else {
  charge = quantity * plan.getRegularRate() + plan.getRegularServiceCharge();
}

public boolean summer(LocalDate aDate, Plan plan) {
  return !aDate.isBefore(plan.getSummerStart()) && !aDate.isAfter(plan.getSummerEnd());
}
조건이 만족했을 때의 로직도 또 다른 함수로 추출
if (summer(LocalDate.now(), plan)) {
  charge = summerCharge(quantity, plan.getSummerRate());
} else {
  charge = quantity * plan.getRegularRate() + plan.getRegularServiceCharge();
}

public boolean summer(LocalDate aDate, Plan plan) {
  return !aDate.isBefore(plan.getSummerStart()) && !aDate.isAfter(plan.getSummerEnd());
}

public int summerCharge(int quantity, int summerRate) {
  return quantity * summerRate;
}
if (summer(LocalDate.now(), plan)) {
  charge = summerCharge(quantity, plan.getSummerRate());
} else {
  charge = regularCharge(quantity, plan.getRegularRate(), plan.getRegularServiceCharge());
}

public boolean summer(LocalDate aDate, Plan plan) {
  return !aDate.isBefore(plan.getSummerStart()) && !aDate.isAfter(plan.getSummerEnd());
}

public int summerCharge(int quantity, int summerRate) {
  return quantity * summerRate;
}

public int regularCharge(int quantity, int regularRate, int regularServiceCharge) {
  return quantity * regularRate + regularServiceCharge;
}
취향에 따라 삼항연산자 사용 가능
charge = summer(LocalDate.now(), plan) ? summerCharge(quantity, plan.getSummerRate()) : regularCharge(quantity, plan.getRegularRate(), plan.getRegularServiceCharge());

10.2 조건식 통합하기

배경

  • 어차피 같은 일을 할 거라면 조건 검사도 하나로 통합하는데 낫다.
  • 여러 조각으로 나뉜 일을 하나로 통합하여 의도를 더 명확하게 한다.
  • 조건식 통합하기는 함수 추출하기까지 이어질 수 있다. 복잡한 조건식을 함수로 추출하면 코드의 의도가 훨씬 분명하게 드러나는 경우가 많다.
  • 하나의 검사가 아닌, 진짜로 독립된 검사들이라면 조건식 통합하기를 적용해선 안된다.

절차

  1. 해당 조건식들 모두에 부수효과가 없는지 확인한다.
    1. 부수 효과가 있는 조건식들에는 질의 함수와 변경 함수 분리하기를 먼저 적용한다.
  2. 조건문 두 개를 선택하여 두 조건문의 조건식들을 논리 연산자로 결합니다.
    1. 순차적으로 이뤄지는(레벨이 같은) 조건문은 or 로 결합하고, 중첩된 조건문은 and로 결합한다.
  3. 테스트한다.
  4. 조건이 하나만 남을 때까지 2~3을 반복한다.
  5. 하나로 합쳐진 조건식을 함수로 추출할지 고려해본다.

예시

if (anEmployee.getSeniority() > 2) {
  return 0;
}
if (anEmployee.getMonthisDisabled() > 12) {
  return 0;
}
if (anEmployee.isPartTime() > 2) {
  return 0;
}
(2단계) 결과로 행하는 동작이 같으므로 조건들을 하나의 식으로 통합한다.
if (anEmployee.getSeniority() > 2 || anEmployee.getMonthisDisabled() > 12) {
  return 0;
}
if (anEmployee.isPartTime() > 2) {
  return 0;
}
3단계 테스트한 후 4단계 진행하여 다음 조건에서 적용한다. 
if (anEmployee.getSeniority() > 2 || anEmployee.getMonthisDisabled() > 12 || anEmployee.isPartTime() > 2) {
  return 0;
}
5단계 모든 조건을 통합했다면 최종 조건식을 함수로 추출을 고려한다.
if (isNotEligibleForDisability(anEmployee)) {
  return 0;
}

public boolean isNotEligibleForDisability(Employee anEmployee) {
  return (anEmployee.getSeniority() > 2 || anEmployee.getMonthisDisabled() > 12 || anEmployee.isPartTime() > 2);
}
if (anEmployee.onVacation()) {
  if (anEmpolyee.getSeniority() > 10) {
    return 1;
  }
  return 0.5;
}
if (anEmployee.onVacation() && anEmployee.getSeniority() > 10) {
  return 1;
}
return 0.5;

10.3 중첩 조건문을 보호 구문으로 바꾸기

배경

  • 조건문은 참인 경로와 거짓인 경로로 쓰이며 두 형태는 의도하는 바가 다르기 때문에 그 의도가 코드에 드러나야 한다.
  • 두 경로 중 한쪽만 정상인 경우 비정상 조건을 if에서 검사하여 참인 경우 함수에서 빠져나온다. 이를 보호구문이라 한다.

절차

  1. 교체해야 할 조건 중 바깥 것을 선택하여 보호 구문으로 바꾼다.
  2. 테스트한다.
  3. 1~2과정을 필요한 만큼 반복한다.
  4. 모든 보호 구문이 같은 결과를 반환한다면 보호 구문들의 조건식을 통합한다.

예시

최상위 조건 보호 구문으로 변경
public Result payAmount(Employee employee) {
    Result result = null;

    if (employee.isSeparated()) {
      result = new Result(0, "SEP");
    } else {
      if (employee.isRetired()) {
        result = new Result(0, "RET");
      } else {
        // 급여 계산 로직;
        result = someFinalMethod();
      }
    }

    return result;
  }
테스트 한 뒤 다음 조건을 보호 구문으로 변경
public Result payAmount(Employee employee) {
    Result result = null;

    if (employee.isSeparated()) {
      return new Result(0, "SEP");
    } else {
      if (employee.isRetired()) {
        result = new Result(0, "RET");
      } else {
        // 급여 계산 로직;
        result = someFinalMethod();
      }
    }

    return result;
  }
public Result payAmount(Employee employee) {
    Result result = null;

    if (employee.isSeparated()) {
      return new Result(0, "SEP");
    }

    if (employee.isRetired()) {
      return new Result(0, "RET");
    } else {
      // 급여 계산 로직;
            result = someFinalMethod();
    }

    return result;
  }
.result 변수는 더이상 필요하지 않다.
public Result payAmount(Employee employee) {

    if (employee.isSeparated()) {
      return new Result(0, "SEP");
    }

    if (employee.isRetired()) {
      return new Result(0, "RET");
    }

    // 급여 계산 로직;
    return someFinalMethod();
  }
조건을 반대로 만들어 적용하는 경우
public int adjustedCapital(Instrument anInstrument) {
    int result = 0;

    if (anInstrument.capital > 0) {
      if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
        result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
      }
    }

    return result;
  }
한 번에 하나씩 보호 구문을 추가하며 조건을 역으로 바꾼다.
public int adjustedCapital(Instrument anInstrument) {
    int result = 0;

    if (anInstrument.capital <= 0) {
      return result;
    }

    if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
      result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
    }

    return result;
  }
조금 더 복잡한 조건은 두 단계로 나눠 진행한다. 우선 not 연산자를 추가한다
public int adjustedCapital(Instrument anInstrument) {
    int result = 0;

    if (anInstrument.capital <= 0) {
      return result;
    }

    if (!(anInstrument.interestRate > 0 && anInstrument.duration > 0)) {
      result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
    }

    return result;
  }
조건식 안에 있는 not 연산자를 간소화한다.
public int adjustedCapital(Instrument anInstrument) {
    int result = 0;

    if (anInstrument.capital <= 0) {
      return result;
    }

    if (anInstrument.interestRate <= 0 || anInstrument.duration <= 0) {
      return result;
    }
    result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;

    return result;
  }
두 if문 모두 같은 결과를 내는 조건을 포함하니 조건식을 통합한다.
public int adjustedCapital(Instrument anInstrument) {
    int result = 0;

    if (anInstrument.capital <= 0 || anInstrument.interestRate <= 0 || anInstrument.duration <= 0) {
      return result;
    }

    result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;

    return result;
  }
public int adjustedCapital(Instrument anInstrument) {
    int result = 0;

    if (anInstrument.capital <= 0 || anInstrument.interestRate <= 0 || anInstrument.duration <= 0) {
      return result;
    }

    return (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
  }

10.4 조건부 로직을 다형성으로 바꾸기

배경

  • 조건부 로직을 직관적으로 구조화하는 방법으로 클래스와 다형성을 이용해 더 확실하게 분리할 수 있다.
  • 다만 남용해서는 안된다.

절차

  • 다형적 동작을 표현하는 클래스들이 아직 없다면 만들어준다. 이왕이면 적합한 인스턴스를 알아서 만들어 반환하는 팩터리 함수도 함께 만든다.
  • 호출하는 코드에서 팩터리 함수를 사용하게 한다.
  • 조건부 로직 함수를 슈퍼클래스로 옮긴다.
    • 조건부 로직이 온전한 함수로 분리되어 있지 않다면 먼저 함수로 추추한다.
  • 서브클래스 중 하나를 선택한다. 서브클래스에서 슈퍼클래스의 조건부 로직 메서드를 오버라이드한다. 조건부 문장 중 선택된 서브클래스에 해당하는 조건절을 서브클래스 메서드로 복사한 다음 적절히 수정한다.
  • 같은 방식으로 각 조건절을 해당 서브클래스에서 메서드로 구현한다.
  • 슈퍼클래스 메서드에는 기본 동작 부분만 남긴다. 혹은 슈퍼클래스가 추상 클래스여야 한다면, 이 메서드를 추상으로 선언하거나 서브클래스에서 처리해야 함을 알리는 에러를 던진다.

예시

public static Map<String, String> plumages(List<Bird> birds) {
    Map<String, String> plumageMap = new HashMap<>();
    for (Bird b : birds) {
      plumageMap.put(b.name, plumage(b));
    }
    return plumageMap;
  }

  public static Map<String, Integer> speeds(List<Bird> birds) {
    Map<String, Integer> speedMap = new HashMap<>();
    for (Bird b : birds) {
      speedMap.put(b.name, airSpeedVelocity(b));
    }
    return speedMap;
  }

  public static String plumage(Bird bird) {
    switch (bird.type) {
      case "유럽 제비":
        return "보통이다";
      case "아프리카 제비":
        return (bird.numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
      case "노르웨이 파랑 앵무":
        return (bird.voltage > 100) ? "그을렸다" : "예쁘다";
      default:
        return "알 수 없다";
    }
  }

  public static Integer airSpeedVelocity(Bird bird) {
    switch (bird.type) {
      case "유럽 제비":
        return 35;
      case "아프리카 제비":
        return 40 * 2 * bird.numberOfCoconuts;
      case "노르웨이 파랑 앵무":
        return (bird.isNailed) ? 0 : 10 + bird.voltage / 10;
      default:
        return null;
    }
  }
3 가장 먼저 airSppedVelocity()와 plumage()를 Bird라는 클래스로 묶는다.(여러 함수 클래스로 묶기)
public static String plumage(Bird bird) {
    return bird.getPlumage();
  }

public static int airSpeedVelocity(Bird bird) {
    return bird.getAirSpeedVelocity();
  }

class Bird {
  private String name;
  private String type;
  private int numberOfCoconuts;
  private int voltage;
  private boolean isNailed;

  public Bird(String name, String type, int numberOfCoconuts, int voltage, boolean isNailed) {
    this.name = name;
    this.type = type;
    this.numberOfCoconuts = numberOfCoconuts;
    this.voltage = voltage;
    this.isNailed = isNailed;
  }

  public String getPlumage() {
    switch (type) {
      case "유럽 제비":
        return "보통이다";
      case "아프리카 제비":
        return (numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
      case "노르웨이 파랑 앵무":
        return (voltage > 100) ? "그을렸다" : "예쁘다";
      default:
        return "알 수 없다";
    }
  }

  public Integer getAirSpeedVelocity() {
    switch (type) {
      case "유럽 제비":
        return 35;
      case "아프리카 제비":
        return 40 * 2 * numberOfCoconuts;
      case "노르웨이 파랑 앵무":
        return (isNailed) ? 0 : 10 + voltage / 10;
      default:
        return null;
    }
  }

}
1 이제 종별 서브클래스를 만든다. 적합한 서브 클래스의 인스턴스를 만들어줄 팩터리 함수도 잊지 말자
2 그러고 나서 객체를 얻을 때 팩터리 함수를 사용하도록 수정한다.

public static String plumage(String name, String type) {
    return createBird(name, type).getPlumage();
  }

  public static int airSpeedVelocity(String name, String type) {
    return createBird(name, type).getAirSpeedVelocity();
  }

  public static Bird createBird(String name, String type) {
    switch (type) {
      case "EuropeanSwallow":
        return new EuropeanSwallow(name, type);
      case "AfricanSwallow":
        return new AfricanSwallow(name, type);
      case "NorwegianBlueParrot":
        return new NorwegianBlueParrot(name, type);
      default:
        return new Bird(name, type);
    }
  }

class EuropeanSwallow extends Bird {
  public EuropeanSwallow(String name, String type) {
    super(name, type);
  }
}

class AfricanSwallow extends Bird {
  public AfricanSwallow(String name, String type) {
    super(name, type);
  }
}

class NorwegianBlueParrot extends Bird {
  public NorwegianBlueParrot(String name, String type) {
    super(name, type);
  }
}
두 조건부 메서드를 처리할 차례다.
plumage()부터 시작
4 switch 문의 절 하나를 선택해 해당 서브클래스에서 오버라이드한다.

class EuropeanSwallow extends Bird {
  public EuropeanSwallow(String name, String type) {
    super(name, type);
  }

  @Override
  public String getPlumage() {
    return "보통이다";
  }
}

public String getPlumage() {
    switch (type) {
      case "유럽 제비":
        throw new IllegalArgumentException("오류 발생");
      case "아프리카 제비":
        return (numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
      case "노르웨이 파랑 앵무":
        return (voltage > 100) ? "그을렸다" : "예쁘다";
      default:
        return "알 수 없다";
    }
  }
다음 조건절을 처리, 아프리카 제비
class AfricanSwallow extends Bird {
  public AfricanSwallow(String name, String type) {
    super(name, type);
  }

  @Override
  public String getPlumage() {
    return (this.getNumberOfCoconuts() > 2) ? "지쳤다" : "보통이다";
  }
}
노르웨이 파랑 앵무 처리
class NorwegianBlueParrot extends Bird {
  public NorwegianBlueParrot(String name, String type) {
    super(name, type);
  }

  @Override
  public String getPlumage() {
    return (this.getVoltage() > 100) ? "그을렸다" : "예쁘다";
  }
}
슈퍼클래스의 메서드는 기본 동작용으로 남겨놓는다.
public String getPlumage() {
    return "알 수 없다";
  }
똑같은 과정을 airSpedVelocity()에도 수행한다.

변형 동작을 다형성으로 표현하기

public static String rating(Voyage voyage, List<VoyageHistory> history) {
      double vpf = voyageProfitFactor(voyage, history);
      int vr = voyageRisk(voyage);
      int chr = captainHistoryRisk(voyage, history);
      if (vpf * 3 > (vr + chr * 2)) {
        return "A";
      } else {
        return "B";
      }
    }

    public static int voyageRisk(Voyage voyage) {
      int result = 1;
      if (voyage.getLength() > 4) result += 2;
      if (voyage.getLength() > 8) result += (voyage.getLength() - 8);
      if (List.of("china", "eastindies").contains(voyage.getZone())) result += 4;
      return Math.max(result, 0);
    }

    public static int captainHistoryRisk(Voyage voyage, List<VoyageHistory> history) {
      int result = 1;
      if (history.size() < 5) result += 4;
      result += history.stream().filter(v -> v.getProfit() < 0).count();
      if ("china".equals(voyage.getZone()) && hasChina(history)) {
        result -= 2;
      }
      return Math.max(result, 0);
    }

    public static boolean hasChina(List<VoyageHistory> history) {
      return history.stream().anyMatch(v -> "china".equals(v.getZone()));
    }

    public static double voyageProfitFactor(Voyage voyage, List<VoyageHistory> history) {
      double result = 2;
      if ("china".equals(voyage.getZone())) result += 1;
      if ("eastindies".equals(voyage.getZone())) result += 1;
      if ("china".equals(voyage.getZone()) && hasChina(history)) {
        result += 3;
        if (history.size() > 10) result += 1;
        if (voyage.getLength() > 12) result += 1;
        if (voyage.getLength() > 18) result -= 1;
      } else {
        if (history.size() > 8) result += 1;
        if (voyage.getLength() > 14) result -= 1;
      }
      return result;
    }

중국까지 항해해본 선장이 중국을 경유해 항해할 때를 다루는 조건부 로직 처럼 특수한 상황을 다루는 로직들을 기본 동작에서 분리하기 위해 상속과 다형성을 이용한다.

이 특수 상황을 검사하는 로직이 반복 되면서 기본 동작을 이해하는데 방해가 된다.

세부 계산을 수행하는 함수들을 먼저 처리해보자.

우선 다형성 적용을 위해 클래스를 만들어야 하니 여러 함수를 클래스로 묶기 부터 적용한다.

class Rating { //함수들을 Rating 클래스로 묶는다.
    private Voyage voyage;
    private List<VoyageHistory> history;

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

    public String getValue() {
        double vpf = voyageProfitFactor();
        int vr = voyageRisk();
        int chr = captainHistoryRisk();
        if (vpf * 3 > (vr + chr * 2)) {
            return "A";
        } else {
            return "B";
        }
    }

    private int voyageRisk() {
        int result = 1;
        if (voyage.getLength() > 4) result += 2;
        if (voyage.getLength() > 8) result += (voyage.getLength() - 8);
        if (List.of("china", "eastindies").contains(voyage.getZone())) result += 4;
        return Math.max(result, 0);
    }

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

    private double voyageProfitFactor() {
        double result = 2;
        if ("china".equals(voyage.getZone())) result += 1;
        if ("eastindies".equals(voyage.getZone())) result += 1;
        if ("china".equals(voyage.getZone()) && hasChinaHistory()) {
            result += 3;
            if (history.size() > 10) result += 1;
            if (voyage.getLength() > 12) result += 1;
            if (voyage.getLength() > 18) result -= 1;
        } else {
            if (history.size() > 8) result += 1;
            if (voyage.getLength() > 14) result -= 1;
        }
        return result;
    }

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

public static String rating(Voyage voyage, List<VoyageHistory> history) {
    return new Rating(voyage, history).getValue();
}

기본 동작을 담당할 클래스가 만들어졌으니 다음은 변형 동작을 담을 빈 서브클래스를 만든다.

class ExperiencedChinaRating extends Rating {

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

}

그런 다음 적절한 변형 클래스를 반환해줄 팩터리 함수를 만든다.

public static Rating createRating(Voyage voyage, List<VoyageHistory> history) {
        if ("china".equals(voyage.getZone()) && history.stream().anyMatch(v -> "china".equals(v.getZone()))) {
            return new ExperiencedChinaRating(voyage, history);
        } else {
            return new Rating(voyage, history);
        }
    }

생성자를 호출하는 코드를 모두 위의 팩터리 함수를 사용하도록 수정한다.

public static String rating(Voyage voyage, List<VoyageHistory> history) {
    return createRating(voyage, history).getValue();
}

서브클래스로 옮길 동작은 총 두 가지로 captainHistoryRist()안의 로직부터 시작하자.

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

서브클래스에서 메서드를 오버라이드

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

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

voyageProfitFactor()에서 변형 동작을 분리하는 작업은 조금 더 복잡하다. 해당 함수에는 다른 경로가 존재하기 때문에 단순히 변형 동작을 제거하고 슈퍼클래스의 메서드를 호출하는 방식은 사용할 수 없다.

또한 슈퍼클래스의 메서드를 통쨰로 서브클래스로 복사하고 싶지도 않다.

private double voyageProfitFactor() {
    double result = 2;
    if ("china".equals(voyage.getZone())) result += 1;
    if ("eastindies".equals(voyage.getZone())) result += 1;
    if ("china".equals(voyage.getZone()) && hasChinaHistory()) {
        result += 3;
        if (history.size() > 10) result += 1;
        if (voyage.getLength() > 12) result += 1;
        if (voyage.getLength() > 18) result -= 1;
    } else {
        if (history.size() > 8) result += 1;
        if (voyage.getLength() > 14) result -= 1;
    }
    return result;
}

먼저 해당 조건부 블록 전체를 함수로 추출한다.

private double voyageProfitFactor() {
    double result = 2;
    if ("china".equals(voyage.getZone())) result += 1;
    if ("eastindies".equals(voyage.getZone())) result += 1;
    result += voyageAndHistoryLengthFactor();
    return result;
}

private double voyageAndHistoryLengthFactor() {
    double result = 0;
    if ("china".equals(voyage.getZone()) && hasChinaHistory()) {
        result += 3;
        if (history.size() > 10) result += 1;
        if (voyage.getLength() > 12) result += 1;
        if (voyage.getLength() > 18) result -= 1;
    } else {
        if (history.size() > 8) result += 1;
        if (voyage.getLength() > 14) result -= 1;
    }
    return result;
}
private double voyageAndHistoryLengthFactor() {
    double result = 0;
    if (history.size() > 8) result += 1;
    if (voyage.getLength() > 14) result -= 1;

    return result;
}
private double voyageAndHistoryLengthFactor() {
    double result = 0;
    result += 3;
    if (history.size() > 10) result += 1;
    if (voyage.getLength() > 12) result += 1;
    if (voyage.getLength() > 18) result -= 1;

    return result;
}

더 가다듬기

변형 동작을 서브 클래스로 뽑아냈으니 공식적으로는 끝이지만 악취를 풍기는 매서드가 새로 생겼다.

메서드 이름의 AND는 이 메서드가 두 가지 독립된 일을 수행한다고 알려준다.

그러니 이들을 분리해야 한다.

이력 길이를 수정하는 부분을 함수로 추출 하면 해결 되는데, 슈퍼 클래스와 서브 클래스 모두에 적용해야 한다.

Rating 클래스

private double voyageAndHistoryLengthFactor() {
    double result = 0;
    result += historyLengthFactor();
    if (voyage.getLength() > 14) result = 1;
    return result;
}

private double historyLengthFactor() {
    return (history.size() > 8) ? 1 : 0;
}

같은 작업을 서브클래스에도 적용한다.

private double voyageAndHistoryLengthFactor() {
    double result = 0;
    result += 3;
    result += historyLengthFactor();
    if (voyage.getLength() > 12) result += 1;
    if (voyage.getLength() > 18) result = 1;
    return result;
}

private double historyLengthFactor() {
    return (history.size() > 10) ? 1 : 0;
}

슈퍼클래스에서는 문장을 호출한 곳으로 옮기기를 적용할 수 있다

private double voyageProfitFactor() {
    double result = 2;
    if ("china".equals(voyage.getZone())) result += 1;
    if ("eastindies".equals(voyage.getZone())) result += 1;
    result += historyLengthFactor();
    result += voyageAndHistoryLengthFactor();
    return result;
}

private double voyageAndHistoryLengthFactor() {
    double result = 0;
    ~~result += historyLengthFactor();~~
    if (voyage.getLength() > 14) result -= 1;

    return result;
}
private double voyageAndHistoryLengthFactor() {
    double result = 0;
    result += 3;
    ~~result += historyLengthFactor();~~
    if (voyage.getLength() > 12) result += 1;
    if (voyage.getLength() > 18) result -= 1;

    return result;
}

함수 이름을 바꾼다.

private double voyageProfitFactor() {
    double result = 2;
    if ("china".equals(voyage.getZone())) result += 1;
    if ("eastindies".equals(voyage.getZone())) result += 1;
    result += historyLengthFactor();
    result += voyageLengthFactor();

    return result;
}

//3항 연산자를 써 voyageLengthFacrtor()를 간소화
private double voyageLengthFactor() {
    return (voyage.getLength() > 14) ? -1 : 0;
}

항해 거리 요인 계산시 3점을 더하고 있는데, 이 로직은 전체 결과를 계산하는 쪽으로 옮기는 것이 좋아 보인다.

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

private double voyageLengthFactor() {
    double result = 0;
    ~~result += 3;~~
    if (voyage.getLength() > 12) result += 1;
    if (voyage.getLength() > 18) result -= 1;

    return result;
}

10.5 특이 케이스 추가하기

배경

  • 특정 값에 대해 똑같이 반응하는 코드가 여러 곳에 있다면 그 코드들을 한 데로 모으는 게 효율적이다.
  • 특이 케이스 패턴을 사용하면 특이 케이스를 확인하는 코드 대부분을 단순한 함수 호출로 바꿀 수 있다.
  • Null 객체는 특이 케이스의 특수한 예이다.

절차

  • 리팩터링의 대상이 될 속성을 담을 데이터 구조, 컨테이너에서 시작한다.
  • 이를 사용하는 코드에서는 해당 속성이 특이한 값인지를 검사한다.
  • 이 대상이 가질 수 있는 값 중 특별하게 다뤄야 할 값을 특이 케이스 클래스(혹은 데이터 구조)로 대체하고자 한다.
  1. 컨테이너에 특이 케이스인지를 검사하는 속성을 추가하고, false를 반환하게 만든다.
  2. 특이 케이스 객체를 만든다. 이 객체는 특이 케이스인지를 검사하는 속성만 포함하며, 이 속성은 true를 반환하게 한다.
  3. 클라이언트에서 특이 케이스인지를 검사하는 코드를 함수로 추출한다. 모든 클라이언트가 값을 직접 비교하는 대신 방금 추출한 함수를 사용하도록 고친다.
  4. 코드에 새로운 특이 케이스 대상을 추가한다. 함수의 반환 값으로 받거나 변환 함수를 적용하면 된다.
  5. 특이 케이스를 검사하는 함수 본문을 수정하여 특이 케이스 객체의 속성을 사용하도록 한다.
  6. 테스트한다.
  7. 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 적용하여 특이 케이스를 처리하는 공통 동작을 새로운 요소로 옮긴다.
    1. 특이 케이스 클래스는 간단한 요청에는 항상 같은 값을 반환하는 게 보통이므로, 해당 특이 케이스의 리터럴 레코드를 만들어 활용할 수 있을 것이다.
  8. 아직도 특이 케이스 검사 함수를 이용하는 곳이 남아 있다면 검사 함수를 인라인 한다.

예시

public class Site{
    private Customer customer;

    public Customer getCustomer() {
        return customer;
    }

}
public class Customer {
    private String name;
    private BillingPlan billingPlan;
    private Map<String, BillingPlan> billingPlans;

    public String getName() {
        return name;
    }

    public BillingPlan getBillingPlan() {
        return billingPlan;
    }

    public BillingPlan getBillingPlan(String arg) {
        return billingPlans.get(arg);
    }
}
// Site 클래스를 사용하는 클라이언트 코드들은 알려지지 않은 미확인 고객도 처리할 수 있어야 한다.
Customer aCustomer = site.getCustomer();
//많은 코드
String customerName;
if (aCustomer == null || aCustomer.equals("미확인 코드")) {
    customerName = "거주자";
} else {
    customerName = aCustomer.getName();
}
BillingPlan plan;
if (aCustomer == null || aCustomer.equals("미확인 고객")) {
    plan = registry.getBillingPlans().getBasicPlan();
} else {
    plan = aCustomer.getBillingPlan();
}
if (aCustomer != null && !aCustomer.equals("미확인 고객")) {
    aCustomer.setBillingPlan(new BillingPlan());
}
int weeksDelinquent;
if (aCustomer == null || aCustomer.equals("미확인 고객")) {
    weeksDelinquent = 0;
} else {
    weeksDelinquent = aCustomer.getPaymentHistory().getWeeksDelinquentInLastYear();
}
  • 대부분의 클라이언트가 고객 이름으로는 “거주자”를 사용하고 기본 요금제를 청구, 연체 기간은 0주로 분류했다.
  • 많은 곳에서 이뤄지는 이 특이 케이스 검사와 공통된 반응이 우리에게 특이 케이스 객체를 도입할 때임을 말해준다.
1 먼저 미확인 고객인지를 나타내는 메서드를 고객 클래스에 추가한다.
public class Customer {
    private String name;
    private BillingPlan billingPlan;
    private Map<String, BillingPlan> billingPlans;

    public String getName() {
        return name;
    }

    public BillingPlan getBillingPlan() {
        return billingPlan;
    }

    public BillingPlan getBillingPlan(String arg) {
        return billingPlans.get(arg);
    }

        public boolean isUnknown() {
            return false;
        }
}
2 미확인 고객 전용 클래스를 만든다.
public class UnknownCustomer extends Customer{

    public boolean isUnknown() {
        return true;
    }
}
  • 3 “미확인 고객”을 기대하는 곳 모두에 새로 만든 특이 케이스 객체 UnknownCustomer를 반환하도록 하고, 역시 값이 “미확인 고객”인지를 검사하는 곳 모두에서 새로운 inUnknown()메서드를 사용하도록 고쳐야 한다.
  • 여러 곳에서 똑같이 수정해야만 하는 코드를 별도 함수로 추출하여 한데로 모으자.
특이 케이스인지 확인하는 코드가 추출 대상이다 (어느 클래스에 만들어야 함...? 새로 클래스를 만드나...?)
public static boolean isUnknown(Object arg) {
        if (!((arg instanceof Customer) || (arg instanceof UnknownCustomer))) {
            throw new IllegalArgumentException("잘못된 값과 비교: <" + arg + ">");
        }
        return arg instanceof UnknownCustomer;
    }
  • 이 변경을 한 번에 하나씩만 적용하고 테스트한다.
String customerName;
if (UnknownCustomerChecker.isUnknown(aCustomer)) {
    customerName = "거주자";
} else {
    customerName = aCustomer.getName();
}
BillingPlan plan = UnknownCustomerChecker.isUnknown(aCustomer)
        ? registry.getBillingPlans().getBasicPlan()
        : aCustomer.getBillingPlan();
if (!UnknownCustomerChecker.isUnknown(aCustomer)) {
    aCustomer.setBillingPlan(new BillingPlan());
}
int weeksDelinquent = UnknownCustomerChecker.isUnknown(aCustomer)
        ? 0
        : aCustomer.getPaymentHistory().getWeeksDelinquentInLastYear();
  • 호출하는 모든 곳에서 isUnknown() 함수를 사용하도록 변경했다면 4특이 케이스일 때 Site클래스가 UnknownCustomer객체를 반환하도록 수정한다.
public Customer getCustomer() {

    if (this.customer instanceof UnknownCustomer) {
      return new UnknownCustomer();
    }

    return this.customer;
  }
  • 5 isUnknown() 함수를 수정하여 고객 객체의 속성을 사용하도록 하면 “미확인 고객”문자열을 사용하던 코드는 사라진다.
public static boolean isUnknown(Object c) {
    if (!((c instanceof Customer) || (c instanceof UnknownCustomer))) {
      throw new IllegalArgumentException("investigate bad value: <" + c + ">");
    }

    return ((Customer) c).isUnknown();
  }
  • 6 모든 기능이 잘 동작하는지 테스트한다.
  • 7 각 클라이언트에서 수행하는 특이 케이스 검사를 일반적인 기본값으로 대체할 수 있다면 이 검사 코드에 여러 함수를 클래스로 묶기를 적용할 수 있다.
String customerName;
if (UnknownCustomerChecker.isUnknown(aCustomer)) {
    customerName = "거주자";
} else {
    customerName = aCustomer.getName();
}
  • 적절한 메서드를 UnknownCustomer클래스에 추가하고
//UnknownCustomer 클래스
public String getName() {
    return "거주자";
}
  • 조건부 코드는 지워도 된다.
~~String customerName;
if (UnknownCustomerChecker.isUnknown(aCustomer)) {
    customerName = "거주자";
} else {
    customerName = aCustomer.getName();
}~~

String customerName = aCustomer.getName();
  • 추가적으로 이 변수는 인라인 한다.

요금제 속성

BillingPlan plan = UnknownCustomerChecker.isUnknown(aCustomer)
        ? registry.getBillingPlans().getBasisPlan();
        : aCustomer.getBillingPlan();
if (!UnknownCustomerChecker.isUnknown(aCustomer)) {
    aCustomer.setBillingPlan(new BillingPlan());
}

겉보기 동작을 똑같게 만들어야 하므로 특이 케이스에서도 세터가 호출되도록 하되, 세터에서는 아무런 일도 하지 않는다.

public class Customer {
    private BillingPlan billingPlan;

    public BillingPlan getBillingPlan() {
        return registry.getBillingPlans().getBasicPlan();
    }

    public void setBillingPlan(BillingPlan arg) {
        // Ignore the setter logic as specified
    }
}
BillingPlan plan = aCustomer.getBillingPlan();
aCustomer.setBillingPlan(new BillingPlan());

특이 케이스 객체는 값 객체이며 항상 불변이어야 한다. 대체하려는 갑싱 가변이라도 마찬가지다.

마지막 상황은 좀 더 복잡한데, 특이 케이스가 자신만의 속성을 갖는 또 다른 객체(지불 이력)를 반환해야 한다.

int weeksDelinquent = UnknownCustomerChecker.isUnknown(aCustomer)
        ? 0
        : aCustomer.getPaymentHistory().getWeeksDelinquentInLastYear();

특이 케이스 객체가 다른 객체를 반환해야 한다면 그 객체 역시 특이 케이스여야 하는 것이 일반적이다. 그래서 NullPaymentHistory를 만든다.

10.6 어서션 추가하기

배경

  • 절차
  1. 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션을 추가한다.

예시

10.7 제어 플래그를 탈출문으로 바꾸기

배경

  • 코드의 동작을 변경하는 데 사용되는 변수는 악취로 취급된다.
  • 함수에서 할 일을 다 마쳤다면 그 사실을 return문으로 명확히 하는것이 낫다.

절차

  1. 제어 플래스를 사용하는 코드를 함수로 추출할지 고려한다.
  2. 제어 플래스를 갱신하는 코드 각각을 적절한 제어문으로 바꾼다. 하나 바꿀 때마다 테스트한다.
    1. 제어문으로는 주로 return, break, continue가 쓰인다
  3. 모두 수정했다면 제어 플래그를 제거한다.

예시

728x90

'Study > Refactoring' 카테고리의 다른 글

리팩터링 12  (0) 2023.08.30
리팩터링 11  (0) 2023.08.29
리팩터링 챕터 9  (0) 2023.08.27
리팩토링 챕터 8  (0) 2023.07.25
리팩터링 챕터7  (0) 2023.07.17