이전 시간 긴 함수를 리팩터링 하면서 매개변수를 추출하는 방법을 배웠다.
매개변수 객체를 만들거나, 임시 변수를 질의 함수로 변경하고 내부적으로 사용하여 매개변수를 줄이거나, 객체를 통째로 넘겨 매개변수를 줄이는 과정이다.
각 과정에 대해 리팩토링을 하다 보면 동일 매개변수들이 여러 메서드에서 사용된다면 동일 관심사로 판단하여 별도의 클래스로 분리하여 사용할 수도 있고, 매개변수 대신 필드로 리팩터링 하여 줄일 수도 있었다.
이렇게 긴 매개변수를 보았을 때 매개변수를 제거하거하는 방법 외에 고민해보아야 할 것이 있다.
매개변수를 질의 함수로 변경하기 ( Replace Parameter with Query)
기존 긴 함수를 리팩토링할 때 우리는 임시 변수를 질의 함수로 변경하여 함수의 매개변수의 개수를 줄일 수 있었다.
동일하게 해당 리팩토링을 적용할 수 있다.
함수의 매개변수 목록은 함수의 다양성을 대변하며, 짧을수록 이해하기 좋다. 이는 함수가 오버 로드되었다는 것은 여러 기능을 처리할 수 있다는 것을 의미하는데, 그렇지 않은 경우인데 매개 변수가 많다면 혼동의 여지가 있다는 뜻을 줄 수 있다.
어떤 한 매개변수를 다른 매개변수를 통해 알아낼 수 있다면 이는 중복 매개변수로 생각되어질 수 있다.
매개변수에 값을 전달하는 것은 호출자의 책임이다. 가능하면 함수의 호출자의 책임을 줄이고 함수 내부에서 책임지도록 노력해야 한다.
public double finalPrice() {
double basePrice = this.quantity * this.itemPrice;
int discountLevel = this.quantity > 100 ? 2 : 1;
return this.discountedPrice(basePrice, discountLevel);
}
private double discountedPrice(double basePrice, int discountLevel) {
return discountLevel == 2 ? basePrice * 0.90 : basePrice * 0.95;
}
discountedPrice()의 매개변수를 줄일 수 있다.
public double finalPrice() {
return this.discountedPrice();
}
private double basePrice() {
return this.quantity * this.itemPrice;
}
private int discountLevel() {
return this.quantity > 100 ? 2 : 1;
}
private double discountedPrice() {
return discountLevel() == 2 ? basePrice() * 0.90 : basePrice() * 0.95;
}
플래그 인수 제거하기 ( Remove Flag Argument )
플래그는 보통 함수에 매개변수로 전달해서, 함수 내부의 로직을 분기하는데 사용한다.
사실 플래그를 사용한다는 것은 함수가 하나의 일이 아닌 여러 일을 하는 것을 의미하기도 한다.
결국 이는 클라이언트가 함수를 들어가서 확인해야하는 작업이 반드시 필요하다.
public LocalDate deliveryDate(Order order, boolean isRush) {
if (isRush) {
int deliveryTime = switch (order.getDeliveryState()) {
case "WA", "CA", "OR" -> 1;
case "TX", "NY", "FL" -> 2;
default -> 3;
};
return order.getPlacedOn().plusDays(deliveryTime);
} else {
int deliveryTime = switch (order.getDeliveryState()) {
case "WA", "CA" -> 2;
case "OR", "TX", "NY" -> 3;
default -> 4;
};
return order.getPlacedOn().plusDays(deliveryTime);
}
}
해당 함수를 사용하는 클라이언트 코드는 다음과 같을 것이다.
Order order = new Order(LocalDate.now(), "LIVE");
Shipment shipment = new Shipment();
shipment.deliveryDate(order, true);
deliveryDate의 파라미터를 보고 어떤 주문처리인지 확인하기가 매우 힘들다. 따라서 클라이언트가 해당 함수에 플래그 인수를 전달하기 전에 함수를 들어가 구현부를 살펴보아야 한다.
우리는 이를 조건문 분해하기 방식으로 개선할 수 있다.
public LocalDate regularDeliveryDate(Order order) {
int deliveryTime = switch (order.getDeliveryState()) {
case "WA", "CA" -> 2;
case "OR", "TX", "NY" -> 3;
default -> 4;
};
return order.getPlacedOn().plusDays(deliveryTime);
}
public LocalDate rushDeliveryDate(Order order) {
int deliveryTime = switch (order.getDeliveryState()) {
case "WA", "CA", "OR" -> 1;
case "TX", "NY", "FL" -> 2;
default -> 3;
};
return order.getPlacedOn().plusDays(deliveryTime);
}
실제 조건문에 해당하는 코드 구현 부를 함수 추출하기로 분리하고 클라이언트에서는 플래그 인수 없이 적절한 메서드를 호출하여 사용할 수 있도록 개선했다. 이로 인해 클라이언트 코드는 다음과 같이 리팩터링 된다.
Order order = new Order(LocalDate.now(), "LIVE");
Shipment shipment = new Shipment();
shipment.regularDeliveryDate(order);
shipment.rushDeliveryDate(order);
필요한 상황에 따라 regularDeliveryDate, rushDeliveryDate를 사용함에 따라 의도를 드러내어 개선한 것이다.
여러 함수를 클래스로 묶기 ( Combine Funtions into Class)
동일한 매개변수를 사용하는 메서드가 이쁘게 한곳에 뭉쳐있으면 해당 부분을 추출하여 클래스로 묶을 수 있다.
하지만 대부분 동일 매개변수를 사용하는 메서드들이 여러 클래스에 분산되어 있으면 별도의 분석과정이 필요하다.
이를 분석하여 별도의 클래스로 추출하게 되면 메소드의 전달하던 매개변수를 줄일 수 있다.
해당 코드 구현 부분을 함수로 추출하려고 한다고 생각해보자.
try (FileWriter fileWriter = new FileWriter("participants.md");
PrintWriter writer = new PrintWriter(fileWriter)) {
participants.sort(Comparator.comparing(Participant::username));
writer.print(header(participants.size()));
participants.forEach(p -> {
String markdownForHomework = getMarkdownForParticipant(p.username(), p.homework());
writer.print(markdownForHomework);
});
}
사용되는 메서드는 다음과 같다.
private String header(int totalNumberOfParticipants)
private String getMarkdownForParticipant(String username, Map<Integer, Boolean> homework)
//getMarkdownForParticipant 함수 구현부에서 다음 메서드를 사용한다.
private String checkMark(Map<Integer, Boolean> homework)
사용되는 필드는 다음과 같다.
private final int totalNumberOfEvents;
사용되는 지역변수는 다음과 같다.
List<Participant> participants = new CopyOnWriteArrayList<>();
이를 별도의 클래스로 만든다고 했을 때 클래스는 다음과 같이 구성된다.
public class StudyPrinter {
private int totalNumberOfEvents;
private List<Participant> participants;
public StudyPrinter(int totalNumberOfEvents, List<Participant> participants) {
this.totalNumberOfEvents = totalNumberOfEvents;
this.participants = participants;
}
public void print() throws IOException {
try (FileWriter fileWriter = new FileWriter("participants.md");
PrintWriter writer = new PrintWriter(fileWriter)) {
participants.sort(Comparator.comparing(Participant::username));
writer.print(header(participants.size()));
participants.forEach(p -> {
String markdownForHomework = getMarkdownForParticipant(p.username(), p.homework());
writer.print(markdownForHomework);
});
}
}
}
1차적으로 필요한 필드를 주입받음으로써 변경되는 클라이언트 코드는 다음과 같아진다.
new StudyPrinter(totalNumberOfEvents, participants).print();
기존 List를 넘기던 파라미터를 별도의 클래스의 필드로 가지고 메서드를 옮김으로써 전달되는 매개변수를 제거했다.
public class StudyPrinter {
private int totalNumberOfEvents;
private List<Participant> participants;
public StudyPrinter(int totalNumberOfEvents, List<Participant> participants) {
this.totalNumberOfEvents = totalNumberOfEvents;
this.participants = participants;
}
public void print() throws IOException {
try (FileWriter fileWriter = new FileWriter("participants.md");
PrintWriter writer = new PrintWriter(fileWriter)) {
participants.sort(Comparator.comparing(Participant::username));
writer.print(header());
participants.forEach(p -> {
String markdownForHomework = getMarkdownForParticipant(p);
writer.print(markdownForHomework);
});
}
}
private String header() {
StringBuilder header = new StringBuilder(String.format("| 참여자 (%d) |", participants.size()));
for (int index = 1; index <= this.totalNumberOfEvents; index++) {
header.append(String.format(" %d주차 |", index));
}
header.append(" 참석율 |\n");
header.append("| --- ".repeat(Math.max(0, this.totalNumberOfEvents + 2)));
header.append("|\n");
return header.toString();
}
private String getMarkdownForParticipant(Participant participant) {
return String.format("| %s %s | %.2f%% |\n", participant.username(), participant.checkMark(totalNumberOfEvents), participant.getRate(totalNumberOfEvents));
}
}
실제 리팩토링을 진행하면서 매개변수를 제거하고 매개변수 목록 대신 객체를 넘김으로써 매개변수를 제거하는 리팩터링도 진행했다.
추가적으로 레코드가 가져야할 책임이 보여 관련 메서드를 레코드로 옮기고 레코드에서 호출하도록 했다.
public record Participant(String username, Map<Integer, Boolean> homework) {
public Participant(String username) {
this(username, new HashMap<>());
}
public void setHomeworkDone(int index) {
this.homework.put(index, true);
}
public double getRate(int totalNumberOfEvents) {
long count = homework.values().stream()
.filter(v -> v == true)
.count();
return (double) (count * 100 / totalNumberOfEvents);
}
public String checkMark(int totalNumberOfEvents) {
StringBuilder line = new StringBuilder();
for (int i = 1 ; i <= totalNumberOfEvents ; i++) {
if(homework.containsKey(i) && homework.get(i)) {
line.append("|:white_check_mark:");
} else {
line.append("|:x:");
}
}
return line.toString();
}
}
'독서에서 한걸음' 카테고리의 다른 글
Split Variable (0) | 2022.12.05 |
---|---|
변수 캡슐화 (0) | 2022.12.04 |
긴 함수 (2) (0) | 2022.12.02 |
긴 함수 (1) (0) | 2022.12.02 |
Refactoring_2 중복 코드 (0) | 2022.11.27 |
댓글