해당 포스팅은 인프런 백기선님의 '리팩토링'을 학습 후 정리한 내용입니다.
participants.forEach(p -> {
long count = p.homework().values().stream()
.filter(v -> v == true)
.count();
double rate = count * 100 / totalNumberOfEvents;
String markdownForHomework = String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), rate);
writer.print(markdownForHomework);
});
위 예제 코드 print() 메서드의 마지막 부분이다.
- rate 를 계산해주기 위한 일련의 로직이 길게 작성되어 있으며 그 과정에 있어 임시 변수를 사용한다. 또한 formating 해주는 부분을 읽기 힘들다..
- 위 코드는 의도를 표현한 것이 아닌 구현을 표현한 코드이다. 리팩토링을 해줄 필요가 있다!
임시 변수를 함수화 하자!
....
try (FileWriter fileWriter = new FileWriter("participants.md");
PrintWriter writer = new PrintWriter(fileWriter)) {
participants.sort(Comparator.comparing(Participant::username));
writer.print(header(totalNumberOfEvents, participants.size()));
participants.forEach(p -> {
String markdownForHomework = getMarkdownForParticipant(totalNumberOfEvents, p);
writer.print(markdownForHomework);
});
}
....
private double getRate(int totalNumberOfEvents, Participant p) {
long count = p.homework().values().stream()
.filter(v -> v == true)
.count();
double rate = count * 100 / totalNumberOfEvents;
return rate;
}
private String getMarkdownForParticipant(int totalNumberOfEvents, Participant p) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, totalNumberOfEvents), getRate(totalNumberOfEvents, p));
}
getMarkdownForParticipant(), getRate() 함수로 추출하였다.
getMarkdownForParticipant 함수에서 내부에 getRate() 함수를 사용하여 불필요한 인자를 줄였다.
public String getMarkdownForParticipant(int totalNumberOfEvents, Participant p) {
....
}
private String header(int totalNumberOfEvents, int totalNumberOfParticipants) {
....
}
반복되는 인자를 갖는 함수들이다.
위의 설명처럼 같은 매개변수들이 여러 메소드에 걸쳐 나타난다면 그 매개변수들을 묶은 자료구조를 만들 수 있다.
- record 와 같은 자료구조를 만들어 해결한다.
- 함수 내부에서 자주 쓰이는 공통적인 매개변수를 필드로 바꾼다.
package me.whiteship.refactoring._03_long_function.practice._02;
public record ParticipantPrinter(int totalNumberOfEvents,
Participant p) {
public int getTotalNumberOfEvents() {
return totalNumberOfEvents;
}
public Participant getP() {
return p;
}
}
public String getMarkdownForParticipant(ParticipantPrinter) {
....
}
private String header(ParticipantPrinter) {
....
}
class 보다 record 를 활용해 더 간결하게 표현하여 파라미터를 객체로 받을 수 있다.
Preserve Whole Object
private String getMarkdownForParticipant(String username, Map<Integer, Boolean> homework) {
return String.format("| %s %s | %.2f%% |\n", username,
checkMark(homework, this.totalNumberOfEvents),
getRate(homework));
}
매개변수를 객체화할 수 있을 것 같다!
- 위의 코드의 매개변수를 Participant 객체로 만들어 매개변수를 줄인다.
- 더 나아가서 내부의 getRate() 함수도 Participant 내부에서 사용한다.
private String getMarkdownForParticipant(Participant participant) {
return String.format("| %s %s | %.2f%% |\n", participant.username(),
checkMark(participant, this.totalNumberOfEvents),
participant.getRate(this.totalNumberOfEvents));
}
package me.whiteship.refactoring._03_long_function.practice._03;
import java.util.HashMap;
import java.util.Map;
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);
}
double getRate(int studyDashboard) {
long count = homework().values().stream()
.filter(v -> v == true)
.count();
return (double) (count * 100 / studyDashboard);
}
}
- 이 메서드가 이 클래스에 의존하는게 맞는가?
- 이메서드는 이 위치에 있는게 맞는가 아님 매개변수 타입에 해당하는 메서드로 가야하는가?
💡 즉 매개변수를 객체화하여 사용할 경우 해당 매개변수를 인자로 하는 함수에 위 두가지 의문을 가져야 한다.
Replace Function with Command
private void print() throws IOException, InterruptedException {
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("whiteship/live-study");
List<Participant> participants = new CopyOnWriteArrayList<>();
ExecutorService service = Executors.newFixedThreadPool(8);
CountDownLatch latch = new CountDownLatch(totalNumberOfEvents);
for (int index = 1 ; index <= totalNumberOfEvents ; index++) {
int eventId = index;
service.execute(new Runnable() {
@Override
public void run() {
try {
GHIssue issue = repository.getIssue(eventId);
List<GHIssueComment> comments = issue.getComments();
for (GHIssueComment comment : comments) {
String username = comment.getUserName();
boolean isNewUser = participants.stream().noneMatch(p -> p.username().equals(username));
Participant participant = null;
if (isNewUser) {
participant = new Participant(username);
participants.add(participant);
} else {
participant = participants.stream().filter(p -> p.username().equals(username)).findFirst().orElseThrow();
}
participant.setHomeworkDone(eventId);
}
latch.countDown();
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
});
}
latch.await();
service.shutdown();
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);
});
}
}
print() 함수 내부에서 실질적으로 print를 해주는 로직을 객체화하여 사용하면 깔끔해지지 않을까?
새로운 클래스를 생성 후 해당 클래스 내부에서 연관된 함수를 구현한다.
private void print() throws IOException, InterruptedException {
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("whiteship/live-study");
List<Participant> participants = new CopyOnWriteArrayList<>();
ExecutorService service = Executors.newFixedThreadPool(8);
CountDownLatch latch = new CountDownLatch(totalNumberOfEvents);
for (int index = 1 ; index <= totalNumberOfEvents ; index++) {
int eventId = index;
service.execute(new Runnable() {
@Override
public void run() {
try {
GHIssue issue = repository.getIssue(eventId);
List<GHIssueComment> comments = issue.getComments();
for (GHIssueComment comment : comments) {
Participant participant = findParticipant(comment.getUserName(), participants);
participant.setHomeworkDone(eventId);
}
latch.countDown();
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
});
}
latch.await();
service.shutdown();
StudyPrinter studyPrinter = new StudyPrinter(this.totalNumberOfEvents, participants);
studyPrinter.execute();
}
package me.whiteship.refactoring._03_long_function.practice._04;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Comparator;
import java.util.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 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(this.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() 함수 내부에 StudyPrinter 객체를 생성하여 해당 객체에서 Participants 등 필요한 인자를 넘긴 후 메서드를 호출하여 print 를 해준다.
private Participant findParticipant(String username, List<Participant> participants) {
Participant participant;
if (participants.stream().noneMatch(p -> p.username().equals(username))) {
participant = new Participant(username);
participants.add(participant);
} else {
participant = participants.stream().filter(p -> p.username().equals(username)).findFirst().orElseThrow();
}
return participant;
}
조건에 맞게 분기처리를 하다보니 함수가 길어졌고, 구현을 표현한 코드들이 다분하다.
조건과 구현을 표현한 부분을 함수로 추출하자!
private Participant findParticipant(String username, List<Participant> participants) {
return isNewParticipant(username, participants) ?
createNewParticipant(username, participants) :
findExistingParticipant(username, participants);
}
private Participant findExistingParticipant(String username, List<Participant> participants) {
return participants.stream().filter(p -> p.username().equals(username)).findFirst().orElseThrow();
}
private Participant createNewParticipant(String username, List<Participant> participants) {
Participant participant;
participant = new Participant(username);
participants.add(participant);
return participant;
}
private boolean isNewParticipant(String username, List<Participant> participants) {
return participants.stream().noneMatch(p -> p.username().equals(username));
}
조건문을 isNewParticipant() 함수로 추출하였으며, 각 조건에 맞는 구현을 추출하여 의도로 표현하였다.
service.execute(new Runnable() {
@Override
public void run() {
try {
GHIssue issue = ghRepository.getIssue(eventId);
List<GHIssueComment> comments = issue.getComments();
Date firstCreatedAt = null;
Participant first = null;
for (GHIssueComment comment : comments) {
Participant participant = findParticipant(comment.getUserName(), participants);
participant.setHomeworkDone(eventId);
if (firstCreatedAt == null || comment.getCreatedAt().before(firstCreatedAt)) {
firstCreatedAt = comment.getCreatedAt();
first = participant;
}
}
firstParticipantsForEachEvent[eventId - 1] = first;
latch.countDown();
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
});
쓰레드 run() 함수 내부 for문에서 과제를 끝낸 참가자를 찾는 로직과, 첫번째로 끝낸 참가자를 찾는다.
하나의 반복문에서 여러 다른 작업을 한다!
반복문을 여러개로 쪼개 보다 쉽게 이해하게 한다.
....
service.execute(new Runnable() {
@Override
public void run() {
try {
GHIssue issue = ghRepository.getIssue(eventId);
List<GHIssueComment> comments = issue.getComments();
checkHomework(comments, eventId);
firstParticipantsForEachEvent[eventId - 1] = findFirst(comments);
latch.countDown();
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
});
}
....
private Participant findFirst(List<GHIssueComment> comments) throws IOException {
Date firstCreatedAt = null;
Participant first = null;
for (GHIssueComment comment : comments) {
Participant participant = findParticipant(comment.getUserName(), participants);
if (firstCreatedAt == null || comment.getCreatedAt().before(firstCreatedAt)) {
firstCreatedAt = comment.getCreatedAt();
first = participant;
}
}
return first;
}
private void checkHomework(List<GHIssueComment> comments, int eventId) {
for (GHIssueComment comment : comments) {
Participant participant = findParticipant(comment.getUserName(), participants);
participant.setHomeworkDone(eventId);
}
}
반복문을 하나 더 만들어 분리하였으며, 반복문을 함수로 추출하였다.
package me.whiteship.refactoring._03_long_function._13_replace_conditional_with_polymorphism;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Comparator;
import java.util.List;
public class StudyPrinter {
private int totalNumberOfEvents;
private List<Participant> participants;
private PrinterMode printerMode;
public StudyPrinter(int totalNumberOfEvents, List<Participant> participants, PrinterMode printerMode) {
this.totalNumberOfEvents = totalNumberOfEvents;
this.participants = participants;
this.participants.sort(Comparator.comparing(Participant::username));
this.printerMode = printerMode;
}
public void execute() throws IOException {
switch (printerMode) {
case CVS -> {
try (FileWriter fileWriter = new FileWriter("participants.cvs");
PrintWriter writer = new PrintWriter(fileWriter)) {
writer.println(cvsHeader(this.participants.size()));
this.participants.forEach(p -> {
writer.println(getCvsForParticipant(p));
});
}
}
case CONSOLE -> {
this.participants.forEach(p -> {
System.out.printf("%s %s:%s\n", p.username(), checkMark(p), p.getRate(this.totalNumberOfEvents));
});
}
case MARKDOWN -> {
try (FileWriter fileWriter = new FileWriter("participants.md");
PrintWriter writer = new PrintWriter(fileWriter)) {
writer.print(header(this.participants.size()));
this.participants.forEach(p -> {
String markdownForHomework = getMarkdownForParticipant(p);
writer.print(markdownForHomework);
});
}
}
}
}
private String getCvsForParticipant(Participant participant) {
StringBuilder line = new StringBuilder();
line.append(participant.username());
for (int i = 1 ; i <= this.totalNumberOfEvents ; i++) {
if(participant.homework().containsKey(i) && participant.homework().get(i)) {
line.append(",O");
} else {
line.append(",X");
}
}
line.append(",").append(participant.getRate(this.totalNumberOfEvents));
return line.toString();
}
private String cvsHeader(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("참석율");
return header.toString();
}
private String getMarkdownForParticipant(Participant p) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p),
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) {
StringBuilder line = new StringBuilder();
for (int i = 1 ; i <= this.totalNumberOfEvents ; i++) {
if(p.homework().containsKey(i) && p.homework().get(i)) {
line.append("|:white_check_mark:");
} else {
line.append("|:x:");
}
}
return line.toString();
}
}
excute() 함수 내부 printerMode 조건에 따라 수행이 다르다.
해당 클래스를 추상화하고 다형성을 활용하자!
package me.whiteship.refactoring._03_long_function.practice._07;
import java.io.IOException;
import java.util.Comparator;
import java.util.List;
public abstract class StudyPrinter {
protected int totalNumberOfEvents;
protected List<Participant> participants;
public StudyPrinter(int totalNumberOfEvents, List<Participant> participants) {
this.totalNumberOfEvents = totalNumberOfEvents;
this.participants = participants;
this.participants.sort(Comparator.comparing(Participant::username));
}
public abstract void execute() throws IOException;
/**
* |:white_check_mark:|:white_check_mark:|:white_check_mark:|:x:|
*/
protected String checkMark(Participant p) {
StringBuilder line = new StringBuilder();
for (int i = 1 ; i <= this.totalNumberOfEvents ; i++) {
if(p.homework().containsKey(i) && p.homework().get(i)) {
line.append("|:white_check_mark:");
} else {
line.append("|:x:");
}
}
return line.toString();
}
}
- 조건문이 들어가는 함수를 추상 메서드로 바꾸고, 해당 클래스를 추상 클래스로 변경하였다.
- 또한 checkMark() 함수는 어떠한 printerMode 에서도 공통적으로 사용하기 때문에 pull up method 하였다.
package me.whiteship.refactoring._03_long_function.practice._07;
import java.io.IOException;
import java.util.List;
public class ConsolePrinter extends StudyPrinter {
public ConsolePrinter(int totalNumberOfEvents, List<Participant> participants) {
super(totalNumberOfEvents, participants);
}
@Override
public void execute() throws IOException {
this.participants.forEach(p -> {
System.out.printf("%s %s:%s\n", p.username(), checkMark(p), p.getRate(this.totalNumberOfEvents));
});
}
}
package me.whiteship.refactoring._03_long_function.practice._07;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
public class CvsPrinter extends StudyPrinter {
public CvsPrinter(int totalNumberOfEvents, List<Participant> participants) {
super(totalNumberOfEvents, participants);
}
@Override
public void execute() throws IOException {
try (FileWriter fileWriter = new FileWriter("participants.cvs");
PrintWriter writer = new PrintWriter(fileWriter)) {
writer.println(cvsHeader(this.participants.size()));
this.participants.forEach(p -> {
writer.println(getCvsForParticipant(p));
});
}
}
private String getCvsForParticipant(Participant participant) {
StringBuilder line = new StringBuilder();
line.append(participant.username());
for (int i = 1 ; i <= this.totalNumberOfEvents ; i++) {
if(participant.homework().containsKey(i) && participant.homework().get(i)) {
line.append(",O");
} else {
line.append(",X");
}
}
line.append(",").append(participant.getRate(this.totalNumberOfEvents));
return line.toString();
}
private String cvsHeader(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("참석율");
return header.toString();
}
}
package me.whiteship.refactoring._03_long_function.practice._07;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
public class MarkdownPrinter extends StudyPrinter {
public MarkdownPrinter(int totalNumberOfEvents, List<Participant> participants) {
super(totalNumberOfEvents, participants);
}
@Override
public void execute() throws IOException {
try (FileWriter fileWriter = new FileWriter("participants.md");
PrintWriter writer = new PrintWriter(fileWriter)) {
writer.print(header(this.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),
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();
}
}
- 리팩토링 하기 이전은 어떠한 조건이든 하나의 클래스에서 수행하였으며 또한 모든 조건에서 사용하는 함수 다 정의하였다.
- 리팩토링 후 StudyPrinter 를 상속 받은 클래스는 자신의 목적에 알맞은 메서드를 사용한다.