11장
API 리팩터링
11.1 질의 함수와 변경 함수 분리하기
배경
- 우리는 외부에서 관찰할 수 있는 겉보기 부수효과가 전혀 없이 값을 반환해주는 함수를 추구해야 한다.
- 이를 위해 “질의 함수(읽기 함수)는 모두 부수효과가 없어야 한다”는 규칙을 따르는 것이다. 물론 이 규칙이 절대적인것은 아니지만 분명 유용하다.
- 만약 값을 반환하면서 부수효과도 가지는 함수가 있다면 상태를 변경하는 부분과 질의하는 부분의 분리를 시도해본다.
절차
- 대상 함수를 복제하고 질의 목적에 충실한 이름을 짓는다.
- 함수 내부를 살펴 무엇을 반환하는지 찾는다. 어떤 변수의 값을 반환한다면 그 변수 이름이 훌륭한 단초가 될 것이다.
- 새 질의 함수에서 부수효과를 모두 제거한다.
- 정적 검사를 수행한다.
- 원래 함수(변경 함수)를 호출하는 곳을 모두 찾아낸다. 호출하는 곳에서 반환 값을 사용한다면 질의 함수를 호출하도록 바꾸고, 원래 함수를 호출하는 코드를 바로 아래 줄에 새로 추가한다. 하나 수정할때마다 테스트한다.
- 원래 함수에서 질의 관련 코드를 제거한다.
- 테스트한다.
이 리팩터링을 마친 후에는 새로 만든 질의 함수와 원래 함수에 (정리해야 할) 중복이 남아 있을 수 있다.
예시
public static String alertForMiscreant(String[] people) {
for (String p : people) {
if (p.equals("Don")) {
setOffAlarms();
return "Don";
}
if (p.equals("John")) {
setOffAlarms();
return "John";
}
}
return "";
}
1. 함수를 복제하고 질의 목적에 맞는 이름짓기
public static String findMiscreant(String[] people) {
for (String p : people) {
if (p.equals("Don")) {
setOffAlarms();
return "Don";
}
if (p.equals("John")) {
setOffAlarms();
return "John";
}
}
return "";
}
2. 새 질의 함수에서 부수효과를 낳는 부분 제거
public static String findMiscreant(String[] people) {
for (String p : people) {
if (p.equals("Don")) {
~~setOffAlarms();~~
return "Don";
}
if (p.equals("John")) {
~~setOffAlarms();~~
return "John";
}
}
return "";
}
4. 원래 함수를 호출하는 곳을 모두 찾아서 새로운 질의 함수로 호출하도록 바꾸고, 이어서 원래의 변경 함수를 호출하는 코드를 바로 아래에 삽입한다.
String found = alertForMiscreant(people); // 이 코드를
String found = findMiscreant(people); //이렇게 바꾼다.
alertForMiscreant(people);
5. 원래의 변경 함수에서 질의 관련 코드를 없앤다.
public static void alertForMiscreant(String[] people) {
for (String p : people) {
if (p.equals("Don")) {
setOffAlarms();
return;
}
if (p.equals("John")) {
setOffAlarms();
return;
}
}
}
더 가다듬기.
변경 함수와 새 질의 함수에 중복코드가 많다. 이번 경우엔 변경 함수에서 질의 함수를 사용하도록 고치면 해결된다. (알고리즘 교체하기 적용)
public static void alertForMiscreant(String[] people) {
if (!findMiscreant(people).isEmpty()) {
setOffAlarms();
}
}
11.2 함수 매개변수화하기
배경
- 로직이 아주 비슷하고 단지 리터럴 값만 다르다면, 그 다른 값만 매개변수로 받아 처리하는 함수 하나로 합쳐 중복을 없앨 수 있다.
절차
- 비슷한 함수 중 하나를 선택한다.
- 함수 선언 바꾸기로 리터럴들을 매개변수로 추가한다.
- 이 함수를 호출하는 곳 모두에 적절한 리터럴 값을 추가한다.
- 테스트한다.
- 매개변수로 받은 값을 사용하도록 함수 본문을 수정한다. 하나 수정할 때마다 테스트한다.
- 비슷한 다른 함수를 호출하는 코드를 찾아 매개변수화된 함수를 호출하도록 하나씩 수정한다. 하나 수정할 때마다 테스트한다.
- 매개변수화된 함수가 대체할 비슷한 함수와 다르게 동작한다면, 그 비슷한 함수의 동작도 처리할 수 있도록 본문 코드를 적절히 수정한 후 진행한다.
예시
public static void tenPercentRaise(Person aPerson) {
BigDecimal newSalary = aPerson.getSalary().multiply(new BigDecimal("1.10"));
aPerson.setSalary(newSalary);
}
public static void fivePercentRaise(Person aPerson) {
BigDecimal newSalary = aPerson.getSalary().multiply(new BigDecimal("1.05"));
aPerson.setSalary(newSalary);
}
위의 두 함수는 다음 함수로 대체할 수 있다.
public static void raise(Person aPerson, double factor) {
BigDecimal newSalary = aPerson.getSalary().multiply(BigDecimal.valueOf(1 + factor));
aPerson.setSalary(newSalary);
}
public static double baseCharge(int usage) {
if (usage < 0) return 0.0;
double amount = bottomBand(usage) * 0.03
+ middleBand(usage) * 0.05
+ topBand(usage) * 0.07;
return amount;
}
public static int bottomBand(int usage) {
return Math.min(usage, 100);
}
public static int middleBand(int usage) {
return usage > 100 ? Math.min(usage, 200) - 100 : 0;
}
public static int topBand(int usage) {
return usage > 200 ? usage - 200 : 0;
}
대역을 다루는 세 함수의 로직이 상당히 비슷한 건 사실이지만, 과연 매개변수화 함수로 통합할 수 있을 만큼 비슷한가? 그렇다. 하지만 앞의 간단한 예보다는 덜 직관적이다.
1. 비슷한 함수들을 매개변수화 하여 통합할때는 우선 대상 함수 중 하나를 골라 매개변수를 추가한다. 우선 middleBand()부터 시작한다.
2. middleBand() 는 리터럴 두 개 (100, 200) 사용하며, 그 각각은 중간 대역의 하한과 상한을 뜻한다. 함수 선언 바꾸기를 적용한다.
3. 이 리터럴들을 호출 시점에 입력하도록 바꿔보자. 이 과정에서 함수 이름도 매개변수화된 기능에 어울리게 수정한다.
public static double withinBand(int usage, int bottom, int top) {
return usage > 100 ? Math.min(usage, 200) - 100 : 0;
}
public static double baseCharge(int usage) {
if (usage < 0) return 0.0;
double amount = bottomBand(usage) * 0.03
+ withinBand(usage, 100, 200) * 0.05
+ topBand(usage) * 0.07;
return amount;
}
5. 함수에서 사용하던 리터럴들을 적절한 매개변수로 대체한다.
public static double withinBand(int usage, int bottom, int top) {
return usage > bottom ? Math.min(usage, top) - bottom : 0;
}
나머지 매개변수도 대체한다.
public static double withinBand(int usage, int bottom, int top) {
return usage > bottom ? Math.min(usage, top) - bottom : 0;
}
6. 대역의 하한을 호출하는 부분도 새로 만든 매개변수화 함수를 호출하도록 바꾼다
public static double baseCharge(int usage) {
if (usage < 0) return 0.0;
double amount = withinBand(usage, 0, 100) * 0.03
+ withinBand(usage, 100, 200) * 0.05
+ topBand(usage) * 0.07;
return amount;
}
~~public static int bottomBand(int usage) {
return Math.min(usage, 100);
}~~
대역의 상한 호출을 대체할 때는 무한대를 뜻하는 Infinity를 이용했다.
public static double baseCharge(int usage) {
if (usage < 0) return 0.0;
double amount = withinBand(usage, 0, 100) * 0.03
+ withinBand(usage, 100, 200) * 0.05
+ withinBand(usage, 200, Integer.MAX_VALUE) * 0.07;
return amount;
}
~~public static int topBand(int usage) {
return usage > 200 ? usage - 200 : 0;
}~~
11.3 플래그 인수 제거하기
배경
- 플래그 인수란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수다.
- 플래그 함수를 사용하게 되면 호출할 수 있는 함수들이 무엇인지, 어떻게 호출해야 하는지 이해하기 난해해진다.
- 이들은 함수들의 기능 차이를 잘 드러내지 않게 만든다.
- 플래그 인수가 되려면 호출하는 쪽에서 불리언 값으로 (프로그램에서 사용되는 데이터가 아닌) 리터럴 값을 건네야 하고, 호출되는 함수는 그 인수를 (다른 함수에 전달하는 데이터가 아닌) 제어 흐름을 결정하는 데 사용해야 한다.
- 함수 하나에 플래그 인수를 두 개 이상 사용한다면 이는 함수 하나가 너무 많은 일을 처리하고 있다는 신호일 수 있다.
절차
- 매개변수로 주어질 수 있는 값 각각에 대응하는 명시적 함수들을 생성한다.
- 주가 되는 함수에 깔끔한 분배 조건문이 포함되어 있다면 조건문 분해하기로 명시적 함수들을 생성하자. 그렇지 않다면 래핑 함수 형태로 만든다.
- 원래 함수를 호출하는 코드들을 모두 찾아서 각 리터럴 값에 대응되는 명시적 함수를 호출하도록 수정한다.
예시
public static LocalDate deliveryDate(Order anOrder, boolean isRush) {
if (isRush) {
int deliveryTime;
if (Arrays.asList("MA", "CT").contains(anOrder.deliveryState)) {
deliveryTime = 1;
} else if (Arrays.asList("NY", "NH").contains(anOrder.deliveryState)) {
deliveryTime = 2;
} else {
deliveryTime = 3;
}
return anOrder.placedOn.plusDays(1 + deliveryTime);
} else {
int deliveryTime;
if (Arrays.asList("MA", "CT", "NY").contains(anOrder.deliveryState)) {
deliveryTime = 2;
} else if (Arrays.asList("ME", "NH").contains(anOrder.deliveryState)) {
deliveryTime = 3;
} else {
deliveryTime = 4;
}
return anOrder.placedOn.plusDays(2 + deliveryTime);
}
}
aShipment.deliveryDate = deliveryDate(anOrder, true);
aShipment.deliveryDate = deliveryDate(anOrder, false);
호출하는 쪽에서는 이 불리언 리터럴 값을 이용해 어느 코드를 실행할지 정한다. → 플래그 인수
1. 이 예에서라면 조건문 분해하기를 적용한다.
public static LocalDate deliveryDate(Order anOrder, boolean isRush) {
if (isRush) {
return rushDeliveryDate(anOrder);
} else {
return regularDeliveryDate(anOrder);
}
}
public static LocalDate rushDeliveryDate(Order anOrder) {
int deliveryTime;
if (Arrays.asList("MA", "CT").contains(anOrder.deliveryState)) {
deliveryTime = 1;
} else if (Arrays.asList("NY", "NH").contains(anOrder.deliveryState)) {
deliveryTime = 2;
} else {
deliveryTime = 3;
}
return anOrder.placedOn.plusDays(1 + deliveryTime);
}
public static LocalDate regularDeliveryDate(Order anOrder) {
int deliveryTime;
if (Arrays.asList("MA", "CT", "NY").contains(anOrder.deliveryState)) {
deliveryTime = 2;
} else if (Arrays.asList("ME", "NH").contains(anOrder.deliveryState)) {
deliveryTime = 3;
} else {
deliveryTime = 4;
}
return anOrder.placedOn.plusDays(2 + deliveryTime);
}
2. 다음 호출은
aShipment.deliveryDate = deliveryDate(anOrder, true);
이렇게 대체할 수 있다.
aShipment.deliveryDate = rushDeliveryDate(anOrder);
모든 호출을 대체했다면 deliveryDate()를 제거한다.
매개변수를 까다로운 방식으로 사용할 때
public static LocalDate deliveryDate(Order anOrder, boolean isRush) {
LocalDate result;
int deliveryTime;
if (anOrder.deliveryState.equals("MA") || anOrder.deliveryState.equals("CT")) {
deliveryTime = isRush ? 1 : 2;
} else if (anOrder.deliveryState.equals("NY") || anOrder.deliveryState.equals("NH")) {
deliveryTime = 2;
if (anOrder.deliveryState.equals("NH") && !isRush) {
deliveryTime = 3;
}
} else if (isRush) {
deliveryTime = 3;
} else if (anOrder.deliveryState.equals("ME")) {
deliveryTime = 3;
} else {
deliveryTime = 4;
}
result = anOrder.placedOn.plusDays(2 + deliveryTime);
if (isRush) {
result = result.minusDays(1);
}
return result;
}
위 코드에서 isRuch를 최상위 분배 조건으로 뽑아내려면 생각보다 일이 커질 수 있다.
그렇다면 deliveryDate()를 감싸는 래핑 함수를 생각해볼 수 있다.
public static LocalDate rushDeliveryDate(Order anOrder) {
return deliveryDate(anOrder, true);
}
public static LocalDate regularDeliveryDate(Order anOrder) {
return deliveryDate(anOrder, false);
}
이 두 함수를 추가했다면 호출하는 코드들을 앞에서 조건문을 쪼갰을 떄와 똑같은 방식으로 대체할 수 있다.
11.4 객체 통째로 넘기기
배경
- 레코드를 통째로 넘기면 변화에 대응하기 쉽다.
- 다만 함수가 레코드 자체에 의존하기를 원하지 않는 경우 해당 리팩터링을 수행하지 않는다.
절차
- 매개변수들을 원하는 형태로 받는 빈 함수를 만든다
- 마지막 단계에서 이 함수의 이름을 변경해야 하니 검색하기 쉬운 이름으로 지어준다.
- 새 함수의 본문에서는 원래 함수를 호출하도록 하며, 새 매개변수와 원래 함수의 매개변수를 매핑한다.
- 정적 검사를 수행한다.
- 모든 호출자가 새 함수를 사용하게 수정한다. 하나씩 수정하며 테스트한다.
- 수정 후에는 원래의 매개변수를 만들어내는 코드 일부가 필요 없어질 수 있다. 따라서 죽은 코드 제거하기로 없앨 수 있을 것이다.
- 호출자를 모두 수정했다면 원래 함수를 인라인 한다.
- 새 함수의 이름을 적절히 수정하고 모든 호출자에 반영한다.
예시
int low = aRoom.daysTempRange.low;
int high = aRoom.daysTempRange.high;
if (!aPlan.withinRange(low, high)) {
alerts.add("room temperature went outside range");
}
boolean withinRange(int bottom, int top) {
return (bottom >= this._temperatureRange.low) && (top <= this._temperatureRange.high);
}
1. 가장 먼저 원하는 인터페이스를 갖춘 빈 메서드를 만든다.
public boolean xxNEWwithinRange(NumberRange aNumberRange) {
// Method body will be added here
}
2. 그런 다음 새 메서드의 본문은 기존 withinRange()를 호출하는 코드로 채운다. 자연스럽게 새 매개변수를 기존 매개변수와 매핑하는 로직이 만들어진다.
public boolean xxNEWwithinRange(NumberRange aNumberRange) {
return this.withinRange(aNumberRange.low, aNumberRange.high);
}
4. 기존 함수를 호출하는 코드를 찾아서 새 함수를 호출하게 수정한다. 수정한 뒤 기존 코드 중 더는 필요 없는 부분은 죽은 코드이니 제거한다.
~~int low = aRoom.daysTempRange.low;
int high = aRoom.daysTempRange.high;~~
if (!aPlan.xxNEWwithinRange(aRoom.daysTempRange)) {
alerts.add("room temperature went outside range");
}
5. 모두 새 함수로 대체했다면 원래 함수를 인라인 해준다.
public boolean xxNEWwithinRange(NumberRange aNumberRange) {
return (aNumberRange.low >= this._temperatureRange.low) && (aNumberRange.high <= this._temperatureRange.high);
}
6. 마지막으로 새 함수에서 보기 흉한 접두어를 제거하고 호출자들에도 모두 반영한다. 접두어를 활용하면 이름 변경 기능을 제공하지 않는 코드 편집기를 사용하더라도 전체 바꾸기를 간단히 수행할 수 있다.
public boolean withinRange(NumberRange aNumberRange) {
return (aNumberRange.low >= this._temperatureRange.low) && (aNumberRange.high <= this._temperatureRange.high);
}
if (!aPlan.withinRange(aRoom.daysTempRange)) {
alerts.add("room temperature went outside range");
}
예시 : 새 함수를 다른 방식으로 만들기
- 코드 작성 없이 순전히 다른 리팩터링들을 연달아 수행하여 새 메서드를 만들어내는 방법
int low = aRoom.daysTempRange.low;
int high = aRoom.daysTempRange.high;
if (!aPlan.withinRange(low, high)) {
alerts.add("room temperature went outside range");
}
코드를 재정렬해서 기존 코드 일부를 메서드로 추출하는 방식으로 새 메서드를 만들려한다. 지금의 호출자 코드는 이에 적합하지 않지만 변수 추출하기를 몇 번 적용하면 원하는 모습으로 둔갑한다. 먼저, 조건문에서 기존 메서드를 호출하는 코드들을 해방시켜보자.
int low = aRoom.daysTempRange.low;
int high = aRoom.daysTempRange.high;
boolean isWithinRange = aPlan.withinRange(low, high);
if (!isWithinRange ) {
alerts.add("room temperature went outside range");
}
그럼 다음 입력 매개변수를 추출한다.
TempRange tempRange = aRoom.days.getTempRange();
int low = tempRange.getLow();
int high = tempRange.getHigh();
boolean isWithinRange = aPlan.withinRange(low, high);
if (!isWithinRange ) {
alerts.add("room temperature went outside range");
}
함수 추출하기로 새 메서드를 만들 수 있다.
TempRange tempRange = aRoom.days.getTempRange();
boolean isWithinRange = xxNEWwithinRange(aPlan, tempRange);
if (!isWithinRange ) {
alerts.add("room temperature went outside range");
}
public boolean xxNEWwithinRange(HeantingPlan plan, TempRange tempRange) {
int low = tempRange.getLow();
int high = tempRange.getHigh();
boolean isWithinRange = aPlan.withinRange(low, high);
return isWithinRange;
}
원래 메서드는 다른 컨텍스트(HeantingPlan 클래스 안)에 있으니 함수 옮기기를 수행해야 한다.
TempRange tempRange = aRoom.days.getTempRange();
boolean isWithinRange = aPlan.xxNEWwithinRange(tempRange);
if (!isWithinRange ) {
alerts.add("room temperature went outside range");
}
HeatingPlan 클래스
public boolean xxNEWwithinRange(NumberRange tempRange) {
int low = tempRange.getLow();
int high = tempRange.getHigh();
boolean isWithinRange = this.withinRange(low, high);
return isWithinRange;
}
11.5 매개변수를 질의 함수로 바꾸기
- 반대 리팩터링 : 질의 함수를 매개변수로 바꾸기
배경
- 피호출 함수가 스스로 쉽게 결정할 수 있는 값을 매개변수로 건네는 것 역시 중복이다. (굳이 매개변수로 건네받지 않아도 접근할 수 있는 값이 아닐까?)
- 해당 매개변수를 제거하면 값을 결정하는 책임 주체가 달라진다
- 매개변수가 있다면 결정 주체가 호출자가 된다.
- 매개변수가 없다면 피호출 함수가 결정 주체가 된다.
- 보통 호출하는 쪽을 간소하게 만들기 위해 책임 소재를 피호출 함수로 옮긴다. 물론 피호출 함수가 그 역할을 수행하기에 적합할 때만!
- 만약 매개변수를 제거했을때 피호출 함수에 원치 않는 의존성이 생기는 경우 매개변수를 질의 함수로 바꾸지 말아야 한다.
- 제거하려는 매개변수의 값을 다른 매개변수에서 질의해서 얻을 수 있다면 질의 함수로 바꿀 수 있다.
- 주의사항으로는 대상 함수가 참조 투명해야 한다는 것이다.
- 함수에 똑같은 값을 건네 호출하면 항상 똑같이 동작해야 한다는 것
- 이런 특성은 테스트시 유용하므로 유지하도록 해야한다. 따라서 매개변수를 없애는 대신 가변 전역 변수를 이용하는 일은 하면 안된다.
절차
- 필요하다면 대상 매개변수의 값을 계산하는 코드를 변로 함수로 추출 해놓는다.
- 함수 본문에서 대상 매개변수로의 참조를 모두 찾아서 그 매개변수의 값을 만들어주는 표현식을 참조하도록 바꾼다. 하나 수정할 때마다 테스트한다.
- 함수 선언 바꾸기로 대상 매개변수를 없앤다.
예시
public class Order {
private int quantity;
private double itemPrice;
public Order(int quantity, double itemPrice) {
this.quantity = quantity;
this.itemPrice = itemPrice;
}
public double getFinalPrice() {
double basePrice = quantity * itemPrice;
int discountLevel;
if (quantity > 100) {
discountLevel = 2;
} else {
discountLevel = 1;
}
return discountedPrice(basePrice, discountLevel);
}
private double discountedPrice(double basePrice, int discountLevel) {
switch (discountLevel) {
case 1:
return basePrice * 0.95;
case 2:
return basePrice * 0.9;
default:
return basePrice;
}
}
}
임시 변수를 질의 함수로 바꾸기
를 finalPrice() 함수에 적용한다.
public class Order {
private int quantity;
private double itemPrice;
public Order(int quantity, double itemPrice) {
this.quantity = quantity;
this.itemPrice = itemPrice;
}
**public double getFinalPrice() {
double basePrice = quantity * itemPrice;
return discountedPrice(basePrice, discountLevel());
}**
**private int discountLevel() {
return quantity > 100 ? 2 : 1;
}**
private double discountedPrice(double basePrice, int discountLevel) {
switch (discountLevel) {
case 1:
return basePrice * 0.95;
case 2:
return basePrice * 0.9;
default:
return basePrice;
}
}
}
이제 discountedPrice() 함수에 discountLevel()의 반환 값을 건넬 이유가 사라졌다.
// 매개 변수를 참조하는 코드를 모두 함수 호출로 바꾼다.
public class Order {
private int quantity;
private double itemPrice;
public Order(int quantity, double itemPrice) {
this.quantity = quantity;
this.itemPrice = itemPrice;
}
public double getFinalPrice() {
double basePrice = quantity * itemPrice;
return discountedPrice(basePrice, discountLevel());
}
private int discountLevel() {
return quantity > 100 ? 2 : 1;
}
private double discountedPrice(double basePrice, int discountLevel) {
**switch (discountLevel()) {**
case 1:
return basePrice * 0.95;
case 2:
return basePrice * 0.9;
default:
return basePrice;
}
}
}
함수 선언 바꾸기로 매개변수를 없앨 수 있다.
public class Order {
private int quantity;
private double itemPrice;
public Order(int quantity, double itemPrice) {
this.quantity = quantity;
this.itemPrice = itemPrice;
}
public double getFinalPrice() {
double basePrice = quantity * itemPrice;
return discountedPrice(basePrice~~, discountLevel()~~);
}
private int discountLevel() {
return quantity > 100 ? 2 : 1;
}
private double discountedPrice(double basePrice~~, int discountLevel~~) {
switch (discountLevel()) {
case 1:
return basePrice * 0.95;
case 2:
return basePrice * 0.9;
default:
return basePrice;
}
}
}
11.6 질의 함수를 매개변수로 바꾸기
배경
- 전역 변수를 참조하거나 제거하길 원하는 원소를 참조하는 경우 해당 참조를 매개변수로 바꿔 해결할 수 있다. 참조를 풀어내는 책임을 호출자로 옮기는 것
- 이런 문제는 코드의 의존 관계를 바꾸려 할 때 벌어진다.
- 똑같은 값을 건네면 매번 똑같은 결과를 내는 함수는 참조 투명성이라는 성질을 가지는데 참조 투명하지 않은 원소에 접근하는 모든 함수는 참조 투명성을 잃는다. 이는 해당 원소를 매개변수로 바꾸어 해결한다.
- 모듈 개발시 순수 함수들을 따로 구분하고, 프로그램의 입출력과 기타 가변 원소들을 다루는 로직으로 순수 함수들의 겉을 감싸는 패턴을 많이 활용한다.
- 이 리팩터링을 사용하여 프로그램의 일부를 순수 함수로 바꿀 수 있다.
- 단점으로는 질의 함수를 매개변수로 바꾸면 어떤 값을 제공할지를 호출자가 알아내야 한다.
절차
- 변수 추출하기로 질의 코드를 함수 본문의 나머지 코드와 분리한다.
- 함수 본문 중 해당 질의를 호출하지 않는 코드들을 별도 함수로 추출한다.
- 이 함수의 이름은 나중에 수정해야 하니 검색하기 쉬운 이름으로 짓는다.
- 방금 만든 변수를 인라인하여 제거한다.
- 원래 함수도 인라인한다.
- 새 함수의 이름을 원래 함수의 이름으로 고쳐준다.
예시
public class HeatingPlan {
private double maxTemperature;
private double minTemperature;
public HeatingPlan(double maxTemperature, double minTemperature) {
this.maxTemperature = maxTemperature;
this.minTemperature = minTemperature;
}
public double targetTemperature(Thermostat thermostat) {
if (thermostat.getSelectedTemperature() > maxTemperature) {
return maxTemperature;
} else if (thermostat.getSelectedTemperature() < minTemperature){
return minTemperature;
}
return thermostat.getSelectedTemperature();
}
}
public class Thermostat {
private double selectedTemperature;
private double currentTemperature; // Added currentTemperature field
public Thermostat(double selectedTemperature) {
this.selectedTemperature = selectedTemperature;
}
public double getSelectedTemperature() {
return selectedTemperature;
}
public double getCurrentTemperature() {
return currentTemperature;
}
public void setCurrentTemperature(double currentTemperature) {
this.currentTemperature = currentTemperature;
}
public void setToHeat() {
// Code to set the thermostat to heating mode
}
public void setToCool() {
// Code to set the thermostat to cooling mode
}
public void setOff() {
// Code to turn off the thermostat
}
}
public static void main(String[] args) {
Thermostat thermostat = new Thermostat(72.0);
HeatingPlan heatingPlan = new HeatingPlan(80.0, 20.0);
double targetTemperature = heatingPlan.targetTemperature(thermostat);
if (targetTemperature > thermostat.getCurrentTemperature()) {
thermostat.setToHeat();
} else if (targetTemperature < thermostat.getCurrentTemperature()) {
thermostat.setToCool();
} else {
thermostat.setOff();
}
}
targetTemperature() 메서드가 전역 객체인 thermostat에 의존한다는 사실이 신경쓰인다. 이 전역 객체에 건네는 질의 메소드를 매개변수로 옮겨 의존성을 끊자.
// 변수 추출하기를 이용해여 이 메서드에서 사용할 매개변수를 준비한다.
public class HeatingPlan {
private double maxTemperature;
private double minTemperature;
public HeatingPlan(double maxTemperature, double minTemperature) {
this.maxTemperature = maxTemperature;
this.minTemperature = minTemperature;
}
**public double targetTemperature(Thermostat thermostat) {
double selectedTemperature = thermostat.getSelectedTemperature();
if (selectedTemperature > maxTemperature) {
return maxTemperature;
} else if (selectedTemperature < minTemperature){
return minTemperature;
}
return selectedTemperature;
}**
}
// 매개변수의 값을 구하는 코드를 제외한 나머지를 메서드로 추출하기가 한결 수월해진다.
public class HeatingPlan {
private double maxTemperature;
private double minTemperature;
public HeatingPlan(double maxTemperature, double minTemperature) {
this.maxTemperature = maxTemperature;
this.minTemperature = minTemperature;
}
public double targetTemperature(Thermostat thermostat) {
if (thermostat.getSelectedTemperature() > maxTemperature) {
return maxTemperature;
} else if (thermostat.getSelectedTemperature() < minTemperature){
return minTemperature;
}
return thermostat.getSelectedTemperature();
}
**public double xxNEWtargetTemperature(double selectedTemperature) {
if (selectedTemperature > maxTemperature) {
return maxTemperature;
} else if (selectedTemperature < minTemperature) {
return minTemperature
}
return selectedTemperature;
}**
}
// 다음으로 방금 추출한 변수를 인라인하면 원래 메서드에는 단순한 호출만 남는다.
public class HeatingPlan {
private double maxTemperature;
private double minTemperature;
public HeatingPlan(double maxTemperature, double minTemperature) {
this.maxTemperature = maxTemperature;
this.minTemperature = minTemperature;
}
**public double targetTemperature(Thermostat thermostat) {
return xxNEWtargetTemperature(thermostat.getSelectedTemperature());
}**
public double xxNEWtargetTemperature(double selectedTemperature) {
if (selectedTemperature > maxTemperature) {
return maxTemperature;
} else if (selectedTemperature < minTemperature) {
return minTemperature;
}
return selectedTemperature;
}
}
// 이어서 이 메서드까지 인라인 한다
public static void main(String[] args) {
Thermostat thermostat = new Thermostat(72.0);
HeatingPlan heatingPlan = new HeatingPlan(80.0, 20.0);
**if (heatingPlan.xxNEWtargetTemperature(thermostat.getSelectedTemperature()) > thermostat.getCurrentTemperature()) {
thermostat.setToHeat();
} else if (heatingPlan.xxNEWtargetTemperature(thermostat.getSelectedTemperature()) < thermostat.getCurrentTemperature()) {
thermostat.setToCool();
} else {
thermostat.setOff();
}**
}
// 새 메서드의 이름을 원래 메서드의 이름으로 바꿀 차례다. 앞서 이 메서드의 이름을 검색하기 쉽게 만들어놓은 덕에 쉽게 바꿀 수 있다.
public static void main(String[] args) {
Thermostat thermostat = new Thermostat(72.0);
HeatingPlan heatingPlan = new HeatingPlan(80.0, 20.0);
**if (heatingPlan.targetTemperature(thermostat.getSelectedTemperature()) > thermostat.getCurrentTemperature()) {
thermostat.setToHeat();
} else if (heatingPlan.targetTemperature(thermostat.getSelectedTemperature()) < thermostat.getCurrentTemperature()) {
thermostat.setToCool();
} else {
thermostat.setOff();**
}
}
public class HeatingPlan {
private double maxTemperature;
private double minTemperature;
public HeatingPlan(double maxTemperature, double minTemperature) {
this.maxTemperature = maxTemperature;
this.minTemperature = minTemperature;
}
~~public double targetTemperature(Thermostat thermostat) {
return xxNEWtargetTemperature(thermostat.getSelectedTemperature());
}~~
**public double targetTemperature(double selectedTemperature) {
if (selectedTemperature > maxTemperature) {
return maxTemperature;
} else if (selectedTemperature < minTemperature) {
return minTemperature;
}
return selectedTemperature;
}**
}
11.7 세터 제거하기
배경
- 세터 제거하기가 필요한 상황은 주로 두가지가 있다.
- 사람들이 무조건 접근자 메서드를 통해서만 필드를 다루려 할 때, 생성자를 제거해 객체가 생성된 이후에는 변경이 되서는 안된다는 것을 분명히 한다.
- 클라이언트에서 생성 스크립트를 사용해 객체를 생성할 때. 스크립트가 완료된 뒤로는 그 객체의 필드는 변경되지 않으리라 생각하기 때문에 이 의도를 명확하게 하기 위해 세터를 제거한다.
절차
- 설정해야 할 값을 생성자에서 받지 않는다면 그 값을 받을 매개변수를 생성자에 추가한다. 그런 다음 생성자 안에서 적절한 세터를 호출한다.
- 세터 여러 개를 제거하려면 해당 값 모두를 한꺼번에 생성자에 추가한다. 그러면 이후 과정이 간소해진다.
- 생성자 밖에서 세터를 호출하는 곳을 찾아 제거하고, 대신 새로운 생성자를 사용하도록 한다. 하나 수정할 때마다 테스트한다.
- 갱신하려는 대상이 공유 참조 객체라서 새로운 객체를 생성하는 방식으로는 세터 호출을 대체할 수 없다면 이 리팩터링을 취소한다.
- 세터 메소드를 인라인한다. 가능하다면 해당 필드를 불변으로 만든다.
- 테스트한다.
예시
public class Person {
private String name;
private String id;
public Person() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
public static void main(String[] args) {
Person martin = new Person();
martin.setName("martin");
martin.setId("1234");
}
이름은 추후에 변경 될 가능성이 있지만 id는 변경되어서는 안된다. 이 의도를 명확히 하기 위해 setter를 제거한다.
// 최초 한 번은 ID를 설정할 수 있어야 하니 함수 선언 바꾸기로 생성자에서 ID를 받도록 한다.
public class Person {
private String name;
private String id;
public Person() {
}
**public Person(String id) {
this.id = id;
}**
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
// 그런 다음 생성 스크립트가 이 생성자를 통해 ID를 설정하게끔 수정한다.
public static void main(String[] args) {
**Person martin = new Person("1234");**
martin.setName("martin");
~~martin.setId("1234");~~
}
// 모두 수정했다면 세터 메서드를 인라인 한다.
public class Person {
private String name;
private String id;
public Person() {
}
public Person(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getId() {
return id;
}
~~public void setId(String id) {
this.id = id;
}~~
}
11.8 생성자를 팩터리 함수로 바꾸기
배경
- 생성자와는 다르게 팩터리 함수에는 제약이 별로 없다.
- 팩터리 함수를 구현하는 과정에서 생성자를 호출할 수는 있지만, 원한다면 다른 무언가로 대체할 수 있다.
절차
- 팩터리 함수를 만든다. 팩터리 함수의 본문에서는 원래의 생성자를 호출한다.
- 생성자를 호출하던 코드를 팩터리 함수 호출로 바꾼다.
- 하나씩 수정할 떄마다 테스트한다.
- 생성자의 가시 범위가 최소가 되도록 제한한다.
예시
public class Employee {
private String name;
private int typeCode;
public Employee(String name, int typeCode) {
this.name = name;
this.typeCode = typeCode;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getTypeCode() {
return typeCode;
}
public void setTypeCode(int typeCode) {
this.typeCode = typeCode;
}
public String getType() {
return legalTypeCodes.get(typeCode);
}
public static Map<String, String> legalTypeCodes = Map.of(
"E", "Engineer",
"M", "Manager",
"S", "Salesman"
);
}
public static void main(String[] args) {
Employee candidate = new Employee(document.getName(), document.getEmpType());
Employee leadEngineer = new Employee(document.getName(), "E");
}
우선 팩터리 함수를 만들어야 한다. 팩터리 본문은 단순히 생성자에 위임하는 방식으로 구현한다. (여기서 말하는 최상위는 어디…?)
public Employee createEmployee(String name, String typeCode) {
return new Employee(name, typeCode);
}
그런 다음 생성자를 호출하는 곳을 찾아 수정한다. 생성자 대신 팩터리 함수를 사용하게 바꾼다.
Employee candidate = createEmployee(document.getName(), document.getEmpType());
Employee leadEngineer = createEmployee(document.getName(), "E");
이보다는 직원 유형을 팩터리 함수의 이름에 녹이는 방식을 권한다
Employee leadEngineer = createEngineer(document.getName());
public Employee createEngineer(String name) {
return new Employee(name, "E");
}
11.9 함수를 명령으로 바꾸기
- 반대 리팩터링 : 명령을 함수로 바꾸기
배경
- 프로그래밍의 기본적인 빌딩 블록 중 하나인 함수를 해당 함수만을 위한 객체 안으로 캡슐화 하면 더 유용해진다. 이런 객체를 명령 객체라 한다.
- 명령은 훨씬 유연하게 함수를 제어하고 표현할 수 있다.
- 되돌리기 같은 보조 연산을 제공할 수 있다
- 수명주기를 더 정밀하게 제어하는 데 필요한 매개변수를 만들어주는 메서드를 제공할 수 있다.
- 상속과 훅을 이용해 사용자 맞춤형으로 만들 수 있다.
- 객체는 지원하지만 일급 함수를 지원하지 않는 프로그래밍 언어를 사용할 때는 명령을 이용해 일급 함수의 기능 대부분을 흉내 낼 수 있다.
- 중첩 함수를 지원하지 않는 언어에서도 메서드와 필드를 이용해 복잡한 함수를 잘게 쪼갤 수 있고, 이렇게 쪼갠 메서드들을 테스트와 디버깅에 직접 이용할 수 있다.
- 유연성은 복잡성을 키우고 얻는 대가이기에 일급 함수와 명령 중 선택해야 한다면 가능한 일급 함수를 사용하자.
- 명령보다 더 간단한 방식으로는 얻을 수 없는 기능이 필요할 때 명령을 사용한다.
절차
- 대항 함수의 기능을 옮긴 빈 클래스를 만든다. 클래스 이름은 함수 이름에 기초해 짓는다.
- 방금 생선한 빈 클래스로 함수를 옮긴다.
- 리팩터링이 끝날 때까지는 원래 함수를 전달 함수 역할로 남겨두자
- 명령 관련 이름은 사용하는 프로그래밍 언어의 명명규칙을 따른다.
- 함수의 인수들 각각은 명령의 필드로 만들어 생성자를 통해 설정할지 고민해본다.
예시
public class Score {
public static int score(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
int result = 0;
int healthLevel = 0;
boolean highMedicalRiskFlag = false;
if (medicalExam.isSmoker()) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
String certificationGrade = "regular";
if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
certificationGrade = "low";
result -= 5;
}
// lots more code like this
result -= Math.max(healthLevel - 5, 0);
return result;
}
}
우선 빈 클래스를 만들고 해당하는 함수를 빈 클래스로 옮기는 일부터 진행한다.
public class Score {
**public int score(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
return new Scorer().execute(candidate, medicalExam, scoringGuide);
}**
}
**public class Scorer {
public int execute(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
int result = 0;
int healthLevel = 0;
boolean highMedicalRiskFlag = false;
if (medicalExam.isSmoker()) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
String certificationGrade = "regular";
if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
certificationGrade = "low";
result -= 5;
}
// lots more code like this
result = Math.max(healthLevel - 5, 0);
return result;
}
}**
매개변수 옮기기는 한 번에 하나씩 수행한다.
public class Score {
public int score(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
return new Scorer(**candidate**).execute(~~candidate~~, medicalExam, scoringGuide);
}
}
public class Scorer {
**private Candidate candidate;
public Scorer(Candidate candidate) {
this.candidate = candidate;
}**
public int execute(~~Candidate candidate~~, MedicalExam medicalExam, ScoringGuide scoringGuide) {
int result = 0;
int healthLevel = 0;
boolean highMedicalRiskFlag = false;
if (medicalExam.isSmoker()) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
String certificationGrade = "regular";
if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
certificationGrade = "low";
result -= 5;
}
// lots more code like this
result = Math.max(healthLevel - 5, 0);
return result;
}
}
다른 매개변수도 옮긴다.
public class Score {
public int score(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
return new Scorer(candidate, **medicalExam, scoringGuide**).execute(~~medicalExam, scoringGuide~~);
}
}
public class Scorer {
**private Candidate candidate;
private MedicalExam medicalExam;
private ScoringGuide scoringGuide;
public Scorer(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
this.candidate = candidate;
this.medicalExam = medicalExam;
this.scoringGuide = scoringGuide;
}**
public int execute(~~MedicalExam medicalExam, ScoringGuide scoringGuide~~) {
int result = 0;
int healthLevel = 0;
boolean highMedicalRiskFlag = false;
if (medicalExam.isSmoker()) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
String certificationGrade = "regular";
if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
certificationGrade = "low";
result -= 5;
}
// lots more code like this
result = Math.max(healthLevel - 5, 0);
return result;
}
}
더 가다듬기
이 리팩터링의 본래 목적은 복잡한 함수를 잘게 나누는 것이다.
먼저 모든 지역 변수를 필드로 바꿔야 한다.
public class Scorer {
private Candidate candidate;
private MedicalExam medicalExam;
private ScoringGuide scoringGuide;
**private int result;**
public Scorer(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
this.candidate = candidate;
this.medicalExam = medicalExam;
this.scoringGuide = scoringGuide;
}
public int execute() {
**result = 0;**
int healthLevel = 0;
boolean highMedicalRiskFlag = false;
if (medicalExam.isSmoker()) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
String certificationGrade = "regular";
if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
certificationGrade = "low";
result -= 5;
}
// lots more code like this
result = Math.max(healthLevel - 5, 0);
return result;
}
}
다른 지역 변수도 동일하게 바꿔준다.
public class Scorer {
private Candidate candidate;
private MedicalExam medicalExam;
private ScoringGuide scoringGuide;
private int result;
**private int healthLevel;
private boolean highMedicalRiskFlag;**
public Scorer(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
this.candidate = candidate;
this.medicalExam = medicalExam;
this.scoringGuide = scoringGuide;
}
public int execute() {
result = 0;
**healthLevel = 0;
highMedicalRiskFlag = false;**
if (medicalExam.isSmoker()) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
String certificationGrade = "regular";
if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
certificationGrade = "low";
result -= 5;
}
// lots more code like this
result = Math.max(healthLevel - 5, 0);
return result;
}
}
이제 함수의 상태가 모두 명령 객체로 옮겨졌다. 따라서 함수가 사용하던 변수나 그 유효범위에 구애받지 않고 함수 추출하기 같은 리팩터링을 적용할 수 있다.
public class Scorer {
private Candidate candidate;
private MedicalExam medicalExam;
private ScoringGuide scoringGuide;
private int result;
private int healthLevel;
private boolean highMedicalRiskFlag;
public Scorer(Candidate candidate, MedicalExam medicalExam, ScoringGuide scoringGuide) {
this.candidate = candidate;
this.medicalExam = medicalExam;
this.scoringGuide = scoringGuide;
}
public int execute() {
result = 0;
healthLevel = 0;
highMedicalRiskFlag = false;
scoreSmoking();
String certificationGrade = "regular";
if (scoringGuide.stateWithLowCertification(candidate.getOriginState())) {
certificationGrade = "low";
result -= 5;
}
// lots more code like this
result = Math.max(healthLevel - 5, 0);
return result;
}
public void scoreSmoking() {
if (medicalExam.isSmoker()) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
}
}
이제 명령을 중첩 함수처럼 다룰 수 있다.
11.10 명령을 함수로 바꾸기
배경
- 명령은 그저 함수를 하나 호출해 정해진 일을 수행하는 용도로 주로 쓰인다. 이런 상황이고 로직이 크게 복잡하지 않다면 명령 객체는 장점보다 단점이 더 크니 평범한 함수로 바꿔준다.
절차
- 명령을 생성하는 코드와 명령의 실행 메서드를 호출하는 코드를 함께 함수로 추출한다.
- 이 함수가 바로 명령을 대체할 함수다
- 명령의 실행 함수가 호출하는 보조 메서드들 각각을 인라인 한다.
- 보조 메서드가 값을 반환한다면 함수 인라인에 앞서 변수 추출하기를 적용한다.
- 함수 선언 바꾸기를 적용하여 생성자의 매개변수 모두를 명령의 실행 메서드로 옮긴다.
- 명령의 실행 메서드에서 참조하는 필드들 대신 대응하는 매개변수를 사용하게끔 바꾼다. 하나씩 수정할 때마다 테스트한다.
- 생성자 호출과 명령의 실행 메서드 호출을 호출자(대체 함수)안으로 인라인한다.
- 테스트한다.
- 죽은 코드 제거하기로 명령 클래스를 없앤다.
예시
public class ChargeCalculator {
private int usage;
private Customer customer;
private Provider provider;
public ChargeCalculator(int usage, Customer customer, Provider provider) {
this.usage = usage;
this.customer = customer;
this.provider = provider;
}
public int getBaseCharge() {
return this.customer.getBaseRate() * usage;
}
public int getCharge() {
return getBaseCharge() + provider.getConnectionCharge();
}
}
public static void main(String[] args) {
ChargeCalculator chargeCalculator = new ChargeCalculator(new Customer(), 10, new Provider());
int charge = chargeCalculator.getCharge();
}
이 명령 클래스는 간단한 편이므로 함수로 대체하는 게 나아 보인다.
// 첫 번째로, 이 클래스를 생성하고 호출하는 코드를 함께 함수로 추출한다.
public static void main(String[] args) {
int monthCharge = charge(new Customer(), 10, new Provider());
}
public static int charge(Customer customer, int usage, Provider provider) {
return new ChargeCalculator(customer, usage, provider).getCharge();
}
// 이때 보조 메서드들을 어떻게 다룰지 정해야 하는데, baseCharge()가 이러한 보조 메서드에 속한다. 값을 반환하는 메서드라면 먼저 반환할 값을 변수로 추출한다.
public class ChargeCalculator {
private Customer customer;
private int usage;
private Provider provider;
public ChargeCalculator(Customer customer, int usage, Provider provider) {
this.customer = customer;
this.usage = usage;
this.provider = provider;
}
public int getBaseCharge() {
return this.customer.getBaseRate() * usage;
}
public int getCharge() {
**int baseCharge = getBaseCharge();
return baseCharge + provider.getConnectionCharge();**
}
}
// 그럼 다음 보조 메서드를 인라인한다.
public class ChargeCalculator {
private Customer customer;
private int usage;
private Provider provider;
public ChargeCalculator(Customer customer, int usage, Provider provider) {
this.customer = customer;
this.usage = usage;
this.provider = provider;
}
~~public int getBaseCharge() {
return customer.getBaseRate() * usage;
}~~
public int getCharge() {
**int baseCharge = this.customer.getBaseRate() * usage;**
return baseCharge + provider.getConnectionCharge();
}
}
이제 로직 전체가 한 메서드에서 이뤄지므로, 다음으로는 생성자에 전달되는 모든 데이터를 주 메서드로 옮겨야 한다.
먼저 생성자가 받던 모든 매개변수를 charge()메서드로 옮기기 위해 함수 선언 바꾸기를 적용한다.
public class ChargeCalculator {
private Customer customer;
private int usage;
private Provider provider;
public ChargeCalculator(Customer customer, int usage, Provider provider) {
this.customer = customer;
this.usage = usage;
this.provider = provider;
}
**public int getCharge(Customer customer, int usage, Provider provider) {**
int baseCharge = this.customer.getBaseRate() * this.usage;
return baseCharge + this.provider.getConnectionCharge();
}
}
public static void main(String[] args) {
int monthCharge = charge(new Customer(), 10, new Provider());
}
public static int charge(Customer customer, int usage, Provider provider) {
**return new ChargeCalculator(customer, usage, provider).getCharge(customer, usage, provider);**
}
이제 charge()의 본문에서 필드 대신 건네받은 매개변수를 사용하도록 수정한다.
public class ChargeCalculator {
private Customer customer;
private int usage;
private Provider provider;
public ChargeCalculator(Customer customer, int usage, Provider provider) {
~~this.customer = customer;~~
this.usage = usage;
this.provider = provider;
}
public int getCharge(Customer customer, int usage, Provider provider) {
**int baseCharge = customer.getBaseRate() * usage;**
return baseCharge + provider.getConnectionCharge();
}
}
나머지 매개변수들도 똑같이 바꿔준다.
public class ChargeCalculator {
~~private Customer customer;
private int usage;
private Provider provider;~~
~~public ChargeCalculator(Customer customer, int usage, Provider provider) {
this.customer = customer;
this.usage = usage;
this.provider = provider;
}~~
public ChargeCalculator() {
}
public int getCharge(Customer customer, int usage, Provider provider) {
int baseCharge = customer.getBaseRate() * usage;
return baseCharge + provider.getConnectionCharge();
}
}
다 됐다면 최상위 charge() 함수로 인라인할 수 있다. 이는 생성자와 메서드 호출을 함께 인라인하는 특별한 형태의 함수 인라인하기다.
public static void main(String[] args) {
int monthCharge = charge(new Customer(), 10, new Provider());
}
public static int charge(Customer customer, int usage, Provider provider) {
**int baseCharge = customer.getBaseRate() * usage;
return baseCharge + provider.getConnectionCharge();**
}
명령 클래스는 이제 죽은 코드가 되었으니 죽은 코드 제거하기로 제거한다.
~~public class ChargeCalculator {
public ChargeCalculator() {
}
public int getCharge(Customer customer, int usage, Provider provider) {
int baseCharge = customer.getBaseRate() * usage;
return baseCharge + provider.getConnectionCharge();
}
}~~
11.11 수정된 값 반환하기
배경
- 데이터가 수정된 경우 변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담아두도록 한다. 이를 통해 호출자 코드를 읽을 때 변수가 갱신될 것임을 분명히 인지할 수 있다.
- 값 하나를 계산한다는 목적이 있는 함수들에 효과적이고, 값 여러 개를 갱신하는 함수에는 효과적이지 못하다.
- 함수 옮기기의 준비 작업으로 적용하기 좋은 리팩터링이다.
절차
- 함수가 수정된 값을 반환하게 하여 호출자가 그 값을 자신의 변수에 저장하게 한다.
- 테스트한다.
- 피호출 함수 안에 반환활 값을 가리키는 새로운 변수를 선언한다.
- 이 작업이 의도대로 이뤄졌는지 검사하고 싶다면 호출자에서 초깃값을 수정해보자. 제대로 처리했다면 수정된 값이 무시된다.
- 테스트한다.
- 계산이 선언과 동시에 이뤄지도록 통합한다.
- 테스트한다.
- 피호출 함수의 변수 이름을 새 역할에 어울리도록 바꿔준다.
- 테스트한다.
예시
public class GPSCalculator {
private Point[] points;
public void calculateByGPSLocation() {
int totalAscent = 0;
int totalTime = 0;
int totalDistance = 0;
calculateAscent();
calculateTime();
calculateDistance();
int pace = totalTime / 60 / totalDistance;
}
private void calculateDistance() {
int totalAscent = 0;
for (int i = 1; i < points.length; i++) {
int verticalChange = points[i].getElevation() - points[i-1].getElevation();
totalAscent += (verticalChange > 0) ? verticalChange : 0;
}
}
private void calculateTime() {
}
private void calculateAscent() {
}
}
현재는 calculateAscent() 안에서 totalAscent가 갱신된다는 사실이 드러나지 않으므로 calculateAscent()와 외부 환경이 어떻게 연결돼 있는지가 숨겨진다. 갱신 사실을 밖으로 알려보자
// 우선 totalAscent 값을 반환하고, 호출한 곳에서 변수에 대입하게 고친다.
public class GPSCalculator {
private Point[] points;
public void calculateByGPSLocation() {
int totalAscent = 0;
int totalTime = 0;
int totalDistance = 0;
**totalAscent = calculateAscent();**
calculateTime();
calculateDistance();
int pace = totalTime / 60 / totalDistance;
}
private int calculateAscent() {
int totalAscent = 0;
for (int i = 1; i < points.length; i++) {
int verticalChange = points[i].getElevation() - points[i-1].getElevation();
totalAscent += (verticalChange > 0) ? verticalChange : 0;
}
**return totalAscent;**
}
private void calculateTime() {
}
private void calculateDistance() {
}
}
그런 다음 calcuateAscent() 안에 반환할 값을 담을 변수인 totalAscent를 선언한다. 그런데 이 결과로 부모 코드에 있는 똑같은 이름의 변수가 가려진다.
이를 피하기 위해 명명규칙에 맞게 수정한다.
public class GPSCalculator {
private Point[] points;
public void calculateByGPSLocation() {
int totalAscent = 0;
int totalTime = 0;
int totalDistance = 0;
totalAscent = calculateAscent();
calculateTime();
calculateDistance();
int pace = totalTime / 60 / totalDistance;
}
private int calculateAscent() {
**int result = 0;**
for (int i = 1; i < points.length; i++) {
int verticalChange = points[i].getElevation() - points[i-1].getElevation();
**result += (verticalChange > 0) ? verticalChange : 0;**
}
**return result;**
}
private void calculateTime() {
}
private void calculateDistance() {
}
}
그런 다음 이 계산이 변수 선언과 동시에 수해오디도록 하고, 불변으로 만든다. (왜 불변으로 만들어야 하지? 값이 단 한 번만 정해지면 된다는 것을 보여주기 위해?)
public class GPSCalculator {
private Point[] points;
public void calculateByGPSLocation() {
**final int totalAscent = calculateAscent();**
int totalTime = 0;
int totalDistance = 0;
calculateTime();
calculateDistance();
int pace = totalTime / 60 / totalDistance;
}
private int calculateAscent() {
int result = 0;
for (int i = 1; i < points.length; i++) {
int verticalChange = points[i].getElevation() - points[i-1].getElevation();
result += (verticalChange > 0) ? verticalChange : 0;
}
return result;
}
private void calculateTime() {
}
private void calculateDistance() {
}
}
11.12 오류 코드를 예외로 바꾸기
배경
- 예외는 다른 정교한 메커니즘과 함께 정확하게 사용할 때만 최고의 효과를 낸다. 정확히 예상 밖의 동작일 때만 쓰여야 한다.
- 예외를 던지는 코드를 프로그램 종료 코드로 바꿔도 프로그램이 여전히 정상 동작할지를 따져서 정상 동작하지 않을 것 같다면 예외를 사용하지 말라는 신호다.
- 이런 경우에는 예외 대신 오류를 검출하여 프로그램을 정상 흐름으로 되돌리게끔 처리해야 한다.
절차
- 콜스택 상위에 해당 예외를 처리할 예외 핸들러를 작성한다.
- 이 핸들러는 처음에는 모든 예외를 다시 던지게 해둔다.
- 적절한 처리를 해주는 핸들러가 이미 있다면 지금의 콜스택도 처리할 수 있도록 확장한다.
- 테스트한다.
- 해당 오류 코드를 대체할 예외와 그 밖의 예외를 구분할 식별 방법을 찾는다.
- 사용하는 프로그래밍 언어에 맞게 선택하면 된다. 대부분 언어에서는 서브클래스를 사용하면 될것이다.
- 정적 검사를 수행한다.
- catch절을 수정하여 직접 처리할 수 있는 예외는 적절히 대처하고 그렇지 않은 예외는 다시 던진다.
- 테스트한다.
- 오류 코드를 반환하는 곳 모두에서 예외를 던지도록 수정한다. 하나씩 수정할 때마다 테스트한다.
- 모두 수정했다면 그 오류 코드를 콜스택 위로 전달하는 코드를 모두 제거한다. 하나씩 수정할 때마다 테스트한다.
- 먼저 오류 코드를 검사하는 부분을 함정으로 바꾼 다음, 함정에 걸려들지 않는지 테스트한 수 제거하는 전략을 권한다. 함정에 걸려드는 곳이 있다면 오류 코드를 검사하는 코드가 아직 남아있다는 뜻이다. 함정을 무사히 피했다면 안심하고 본문을 정리하자
예시
public int localShippingRules(String country) {
ShippingRulesData data = countryData.get(country);
if (data != null) {
return new ShippingRules(data).getShippingCost();
} else {
return -23;
}
}
위 코드는 국가 정보(country)가 유효한지를 이 함수 호출 전에 다 검증했다고 가정한다. 그렇기에 이 함수에서 오류가 난다면 무언가 잘못되었다는 뜻이다.
// 다음과 같이 호출한 곳에서는 반환된 오류 코드를 검사하여 오류가 발견되면 위로 전파한다.
public int calculateShippingCosts(Order anOrder) {
// Not related codes
int shippingRules = localShippingRules(anOrder.getCountry());
if (shippingRules < 0) {
return shippingRules; //오류 전파
}
// Not related codes
}
더 윗단 함수는 오류를 낸 주문을 오류 목록(errorList)에 넣는다.
int status = calculator.calculateShippingCosts(order);
if (status < 0) {
errorList.add(new ErrorEntry(order, status));
}
여기서 가장 먼저 고려할 것은 이 오류가 ‘예상된 것이냐’다
만약 예상할 수 있는 정상 동작 범주에 든다면 오류 코드를 예외로 바꾸는 리팩터링을 적용할 준비가 된 것이다.
// 가장 먼저 최상위에 예외 핸들러를 갖춘다. 오류 처리 로직을 포함하고 싶지않다. 하지만 status를 확인하는 코드를 try catch 블록 바깥으로 위치하면 status의 유효범위가 try 블록으로 국한되어 검사할 수가 없다.
try {
int status = calculator.calculateShippingCosts(order);
if (status < 0) {
errorList.add(new ErrorEntry(order, status));
}
} catch (Exception e) {
// Exception handling
}
status 선언과 초기화를 분리해야 한다. 지금은 어쩔 수 없이 진행하지만 평소라면 쓰지 않았을 것이다.
int status = 0;
status = calculateShippingCosts(orderData);
if (status <0) {
errorList.add(new ErrorEntry(order, status));
}
이제 함수 호출을 try catch 블록으로 감싼다.
int status = 0;
try {
status = calculateShippingCosts(orderData);
} catch (Exception e) {
//exception handling
}
if (status <0) {
errorList.add(new ErrorEntry(order, status));
}
이번 리팩터링으로 추가된 예외만을 처리하고자 한다면 다른 예외와 구별할 방법이 필요하다.
int status = 0;
try {
status = calculateShippingCosts(orderData);
} catch (NullPointerException npe) {
//exception handling
} catch (Exception e) {
//exception handling
}
if (status <0) {
errorList.add(new ErrorEntry(order, status));
}
public int localShippingRules(String country) {
ShippingRulesData data = countryData.get(country);
if (data != null) {
return new ShippingRules(data).getShippingCost();
} else {
throw new OrderProcessingException(-23);
}
}
코드를 다 작성했고 테스트도 통과했다면 오류 코드를 전파하는 임시 코드를 제거할 수 있지만 우선 함정을 추가한 후 테스트를 해본다.
함정에 걸려들지 않는다면 줄 전체를 제거해도 안전하다.
int status = calculator.calculateShippingCosts(order);
~~if (status < 0) {
errorList.add(new ErrorEntry(order, status));
}~~
int status = 0;
try {
status = calculateShippingCosts(orderData);
} catch (NullPointerException npe) {
//exception handling
} catch (Exception e) {
//exception handling
}
~~if (status <0) {
errorList.add(new ErrorEntry(order, status));
}~~
try {
~~status =~~ calculateShippingCosts(orderData);
} catch (NullPointerException npe) {
//different exception handling
} catch (Exception e) {
//exception handling
}
11.13 예외를 사전확인으로 바꾸기
배경
- 예외는 말 그래도 예외적으로 동작할 때만 쓰여야 한다.
- 함수 수행 시 문제가 될 수 있는 조건을 함수 호출 전에 검사할 수 있다면, 예외를 던지는 대신 호출하는 곳에서 조건을 검사하도록 한다.
절차
- 예외를 유발하는 상황을 검사할 수 있는 조건문을 추가한다. catch 블록의 코드를 조건문의 조건절중 하나로 옮기고, 남은 try블록의 코드를 다른 조건절로 옮긴다.
- catch 블록에 어서션을 추가하고 테스트한다.
- try문과 catch 블록을 제거한다.
- 테스트한다.
예시
public class ResourcePool {
public Resource get() {
Resource result;
try {
result = available.pop();
allocated.add(result);
} catch (NoSuchElementException e) {
result = Resource.create();
allocated.add(result);
}
return result;
}
private Deque<Resource> available;
private List<Resource> allocated;
}
풀에서 자원이 고갈되는 건 예상치 못한 조건이 아니므로 예외 처리로 대응하는 건 옳지 않다.
사용하기 전에 allocated 컬렉션의 상태를 확인하여 예상 범주에 있는 동작임을 뚜렷하게 드러내준다.
// 조건 검사 코드를 추가하고, catch 블록의 코드를 조건문의 조건절로 옮기고, 남은 try블록 코드를 다른 조건절로 옮긴다.
public class ResourcePool {
public Resource get() {
Resource result;
**if (available.isEmpty()) {
result = Resource.create();
allocated.add(result);
} else {
try {
result = available.pop();
allocated.add(result);
} catch (NoSuchElementException e) {
result = Resource.create();
allocated.add(result);
}
}**
return result;
}
private Deque<Resource> available;
private List<Resource> allocated;
}
catch 절은 더 이상 호출되지 않으므로 어서션을 추가한다.
public class ResourcePool {
public Resource get() {
Resource result;
if (available.isEmpty()) {
result = Resource.create();
allocated.add(result);
} else {
try {
result = available.pop();
allocated.add(result);
} catch (NoSuchElementException e) {
**throw new AssertionError("도달 불가");**
}
}
return result;
}
private Deque<Resource> available;
private List<Resource> allocated;
}
어서션 추가 후 테스트에 통과하면 try catch 블록을 삭제한다.
public class ResourcePool {
public Resource get() {
Resource result;
if (available.isEmpty()) {
result = Resource.create();
allocated.add(result);
} else {
result = available.pop();
allocated.add(result);
}
return result;
}
private Deque<Resource> available;
private List<Resource> allocated;
}
한 번 더 테스트에 통과하면 이번 리팩터링은 끝이다.
그런데 이번 리팩터링 결과로 얻어진 코드에는 정리할 거리가 더 있다.
// 우선 문장 슬라이드하기 부터 적용한다.
public class ResourcePool {
public Resource get() {
Resource result;
if (available.isEmpty()) {
result = Resource.create();
~~allocated.add(result);~~
} else {
result = available.pop();
~~allocated.add(result);~~
}
**allocated.add(result);**
return result;
}
private Deque<Resource> available;
private List<Resource> allocated;
}
3항 연산자로 바꾼다
public class ResourcePool {
public Resource get() {
~~Resource result;
if (available.isEmpty()) {
result = Resource.create();
} else {
result = available.pop();
}~~
Resource result = available.isEmpty() ? Resource.create() : available.pop();
allocated.add(result);
return result;
}
private Deque<Resource> available;
private List<Resource> allocated;
}
'Study > Refactoring' 카테고리의 다른 글
리팩터링 1 (0) | 2023.08.31 |
---|---|
리팩터링 12 (0) | 2023.08.30 |
리팩터링 챕터 10 (0) | 2023.08.28 |
리팩터링 챕터 9 (0) | 2023.08.27 |
리팩토링 챕터 8 (0) | 2023.07.25 |