1. 알고자 하는 것
- 함수를 명령으로 바꾸기
2. 알게된 것
함수를 명령으로 바꾸기
- 기본적으로 코드가 길어지고 이로 인해 가독성이 떨어진다면, 함수 추출(Extract Method)을 수행해 가독성을 높이고 복잡도를 낮출 수 있다.
- 함수 추출(Extract Method)을 수행해도 코드의 위치가 해당 위치가 아니라고 생각될 때 함수를 명령으로 바꾸는(Replace Function with Command) 방법을 고려한다.
- 해당 위치가 아니라고 생각된다 = 클래스의 맥락에서 해당 함수가 별도의 위치로 분리될 필요가 있다.
- 함수를 독립적인 객체(=Command)로 분리한다.
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);
writer.print(markdownForHomework);
});
}
- 서비스 로직 내 participant 정보를 마크다운 파일로 생성하는 코드이다.
- 해당 코드는 추후 마크다운 뿐 아니라 csv, 콘솔 출력과 같이 다양한 형태로 Print 될 수 있다.
- 즉, 해당 코드는 더 다양한 기능을 포함할 수 있는, 복잡해질 가능성이 있는 코드이다.
- 해당 코드를 Command로 변경해본다.
- 먼저, 해당 코드를 메서드로 추출한다.
private void execute(List<Participant> participants) 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);
writer.print(markdownForHomework);
});
}
}
- 그리고, 해당 메서드를 가지는 클래스를 별도로 만든다. (=Command 클래스)
public class StudyPrinter {
public void execute(List<Participant> participants) 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);
writer.print(markdownForHomework);
});
}
}
}
- 기존 execute 메서드에 사용된 서비스 로직에 존재했던 나머지 메서드도 함께 Command 클래스에 옮긴다.
public class StudyPrinter {
public void execute(List<Participant> participants) 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);
writer.print(markdownForHomework);
});
}
}
private String getMarkdownForParticipant(Participant p) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, this.totalNumberOfEvents),
p.getRate(this.totalNumberOfEvents));
}
/**
* | 참여자 (420) | 1주차 | 2주차 | 3주차 | 참석율 |
* | --- | --- | --- | --- | --- |
*/
private String header(int totalNumberOfParticipants) {
StringBuilder header = new StringBuilder(String.format("| 참여자 (%d) |", totalNumberOfParticipants));
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();
}
/**
* |:white_check_mark:|:white_check_mark:|:white_check_mark:|:x:|
*/
private String checkMark(Participant p, int totalEvents) {
StringBuilder line = new StringBuilder();
for (int i = 1 ; i <= totalEvents ; i++) {
if(p.homework().containsKey(i) && p.homework().get(i)) {
line.append("|:white_check_mark:");
} else {
line.append("|:x:");
}
}
return line.toString();
}
}
- 이 때, Command 클래스로 파일 생성 관련 기능을 분리함으로써 해당 기능에만 필요한 Field가 무엇인지 직관적으로 파악할 수 있다.
- 필요한 Field를 생성자로 받아올 수 있도록 처리한다.
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 execute() throws IOException {
try (FileWriter fileWriter = new FileWriter("participants.md");
PrintWriter writer = new PrintWriter(fileWriter)) {
this.participants.sort(Comparator.comparing(Participant::username));
writer.print(header(participants.size()));
this.participants.forEach(p -> {
String markdownForHomework = getMarkdownForParticipant(p);
writer.print(markdownForHomework);
});
}
}
private String getMarkdownForParticipant(Participant p) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, this.totalNumberOfEvents),
p.getRate(this.totalNumberOfEvents));
}
/**
* | 참여자 (420) | 1주차 | 2주차 | 3주차 | 참석율 |
* | --- | --- | --- | --- | --- |
*/
private String header(int totalNumberOfParticipants) {
StringBuilder header = new StringBuilder(String.format("| 참여자 (%d) |", totalNumberOfParticipants));
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();
}
/**
* |:white_check_mark:|:white_check_mark:|:white_check_mark:|:x:|
*/
private String checkMark(Participant p, int totalEvents) {
StringBuilder line = new StringBuilder();
for (int i = 1 ; i <= totalEvents ; i++) {
if(p.homework().containsKey(i) && p.homework().get(i)) {
line.append("|:white_check_mark:");
} else {
line.append("|:x:");
}
}
return line.toString();
}
}
- 이제 Print와 관련된 코드가 서비스 로직에서 Command 클래스로 완벽하게 분리되었다.
- 기존 서비스 로직은 Print 관련 코드가 모두 제거되고, 단지 Command 객체에서 execute 메서드를 수행해 Print 동작을 처리한다.
{
...
new StudyPrinter(this.totalNumberOfEvents, participants).execute();
}
- 기존 서비스 로직에 존재하던 Print 관련 코드가 모두 사라져 서비스 로직의 가독성이 좋아지고 복잡도가 줄어들었다.
- 또한 Print 관련 기능을 별도로 분리하였으므로 추후 콘솔 출력, csv 생성 등 다양한 방식으로 확장하기 용이해졌다.
- StudyPrinter를 인터페이스 또는 상위클래스로 만든다.
- StudyPrinter를 구현하는 MarkdownPrinter, CsvPrinter, ConsolePrinter 구현체를 만든다.
- 다형성을 활용해 목적에 맞는 구현체의 execute를 수행한다.
- 하지만 함수를 명령으로 바꾸게 되면 위와 같이 새로운 클래스를 생성하고 코드의 구조가 변경되어 복잡도가 오히려 증가할 수도 있다.
- 따라서 맥락에 따른 함수의 위치가 적절하지 않다고 판단되거나, 추후 기능이 복잡해지고 확장될 가능성이 있는 코드에 대해 해당 리팩토링을 적용해야 한다.
3. 정리
- 함수 추출을 수행해도 해당 함수의 위치가 맥락에 맞지 않거나, 추후 복잡해지거나 확장될 가능성이 있는 코드에 대해서는 함수를 명령(Command)으로 바꾸는 방식을 고려할 수 있다.
- 특정 기능을 수행하는 메서드를 별도의 커맨드 객체로 분리하고, 기능을 수행하는 데 필요한 필드는 생성자와 같은 방식을 통해 받아온다.
- 기능을 사용하는 로직에서는 단지 커맨드 객체의 메서드를 호출하기만 하면 된다.
- 기능을 별도의 클래스로 분리함으로써 서비스 로직의 가독성이 높아지고 복잡도가 감소한다.
- 또한, 추후 기능이 확장될 때 커맨드를 인터페이스 또는 상위클래스로 만들어 다형성을 활용한 확장이 용이해진다.
- 하지만, 클래스 생성 및 코드 구조 변경으로 인해 복잡성이 오히려 증가할 수도 있다.
- 함수의 위치, 복잡도 및 확장성을 충분히 고려하여 해당 리팩토링을 수행해야한다.
Reference
'Clean Code' 카테고리의 다른 글
Java 리팩토링 - 조건문을 다형성으로 바꾸기 (0) | 2023.11.05 |
---|---|
Java 리팩토링 - 조건문 분해하기 (0) | 2023.10.31 |
Java 리팩토링 - 메서드 추출 시 매개변수가 많아질 경우 [3. 객체 통째로 넘기기] (0) | 2023.09.26 |
Java 리팩토링 - 메서드 추출 시 매개변수가 많아질 경우 [2. 매개변수 객체 만들기] (0) | 2023.06.20 |
Java 리팩토링 - 메서드 추출 시 매개변수가 많아질 경우 [1. 임시 변수를 질의 함수로 바꾸기] (0) | 2023.06.20 |