Open tonykang22 opened 2 years ago
짧은 함수 vs 긴 함수
사용할 수 있는 리팩토링 기술
함수 추출하기
매개변수가 많아진다면
임시 변수를 질의 함수로 바꾸기
매개변수 객체 만들기
객체 통째로 넘기기
조건문 분해하기
조건문을 다형성으로 바꾸기
반복문 쪼개기
반복
동일한 식
임시 변수
getMarkdownForParticipant()
넘어가는 인자가 3 개나 되어 유심히 관찰하니, rate 또한 반복해서 동일한 식을 계산하기에 getRate()로 추출할 수 있고, 추출한 메소드를 getMarkdownForParticipant()에서 직접 호출하게 할 수 있다.
rate
getRate()
public class StudyDashboard { . . . private void print() throws IOException, InterruptedException { . . . 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 -> { 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); }); } } }
public class StudyDashboard { . . . private void print() throws IOException, InterruptedException { . . . 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)); } }
관계
도메인
Participant
totalNumberOfEvents
public record Participant(String username, Map<Integer, Boolean> homework) { public Participant(String username) { this(username, new HashMap<>()); } public double getRate(double total) { long count = this.homework.values().stream() .filter(v -> v == true) .count(); return count * 100 / total; } public void setHomeworkDone(int index) { this.homework.put(index, true); } }
public class StudyDashboard { . . . 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)); } }
Participant와 totalNumberOfEvents를 새로운 ParticipantPrinter로 만든다.
ParticipantPrinter
public class StudyDashboard { . . . private double getRate(ParticipantPrinter participantPrinter) { long count = participantPrinter.p().homework().values().stream() .filter(v -> v == true) .count(); double rate = count * 100 / participantPrinter.totalNumberOfEvents(); return rate; } private String getMarkdownForParticipant(ParticipantPrinter participantPrinter) { return String.format("| %s %s | %.2f%% |\n", participantPrinter.p().username(), checkMark(participantPrinter.p(), participantPrinter.totalNumberOfEvents()), getRate(participantPrinter); } }
Introduce parameter field
public class StudyDashboard { private final int totalNumberOfEvents; public StudyDashboard(int totalNumberOfEvents) { this.totalNumberOfEvents = totalNumberOfEvents; } public static void main(String[] args) throws IOException, InterruptedException { StudyDashboard studyDashboard = new StudyDashboard(15); studyDashboard.print(); } private void print() throws IOException, InterruptedException { . . . 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 double getRate(Participant p) { long count = p.homework().values().stream() .filter(v -> v == true) .count(); double rate = count * 100 / this.totalNumberOfEvents; return rate; } private String getMarkdownForParticipant(Participant p) { return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p, this.totalNumberOfEvents), getRate(p)); } }
public class StudyDashboard { . . . private void print() throws IOException, InterruptedException { . . . participants.forEach(p -> { String markdownForHomework = getMarkdownForParticipant(p.username(), p.homework()); writer.print(markdownForHomework); }); } } private double getRate(Map<Integer, Boolean> homework) { long count = homework.values().stream() .filter(v -> v == true) .count(); return (double) (count * 100 / this.totalNumberOfEvents); } private String getMarkdownForParticipant(String username, Map<Integer, Boolean> homework) { return String.format("| %s %s | %.2f%% |\n", username, checkMark(homework, this.totalNumberOfEvents), getRate(homework)); } }
Move method
public class StudyDashboard { . . . private void print() throws IOException, InterruptedException { . . . participants.forEach(p -> { String markdownForHomework = getMarkdownForParticipant(p); writer.print(markdownForHomework); }); } } private double getRate(Participant participant) { long count = participant.homework().values().stream() .filter(v -> v == true) .count(); return (double) (count * 100 / this.totalNumberOfEvents); } private String getMarkdownForParticipant(Participant participant) { return String.format("| %s %s | %.2f%% |\n", participant.username(), checkMark(participant, this.totalNumberOfEvents), getRate(participant)); } }
함수를 독립적인 개체인, Command로 만들어 사용할 수 있다.
커맨드 패턴을 적용하면 다음과 같은 장점을 취할 수 있다.
대부분의 경우에 "커맨드"보다는 "함수"를 사용하지만, 커맨드 말고 다른 방법이 없는 경우에만 사용한다.
먼저 함수를 분리해보고, 그럼에도 위치가 애매하거나 추후 메소드의 복잡도가 높아질 가능성이 있을 경우에 커맨드 패턴을 통해 장점을 취한다.
긴 메소드를 간추리기도 좋고, 복잡도를 낮출 수 있다. (복잡한 메소드를 잘게 쪼개므로써)
그러나 새로운 클래스와 구조가 변경이 되어 오히려 전체적인 복잡도가 증가할 수 있다.
print()
StudyPrinter
public class StudyDashboard { . . . public static void main(String[] args) throws IOException, InterruptedException { StudyDashboard studyDashboard = new StudyDashboard(15); studyDashboard.print(); } private void print() throws IOException, InterruptedException { . . . // 함수 추출할 부분 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 Participant findParticipant(String username, List<Participant> participants) { . . . } private String getMarkdownForParticipant(Participant p) { . . . } private String header(int totalNumberOfParticipants) { . . . } private String checkMark(Participant p, int totalEvents) { . . . } }
public class StudyDashboard { . . . public static void main(String[] args) throws IOException, InterruptedException { StudyDashboard studyDashboard = new StudyDashboard(15); studyDashboard.print(); } private void print() throws IOException, InterruptedException { . . . // 변경된 부분 new StudyPrinter(this.totalNumberOfEvents, participants).execute(); } . . . }
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) { . . . } private String header(int totalNumberOfParticipants) { . . . } private String checkMark(Participant p, int totalEvents) { . . . } }
의도
public class StudyDashboard { . . . private Participant findParticipant(String username, List<Participant> participants) { Participant participant = null; if (participants.stream().noneMatch(p -> p.username().equals(username))) { Participant participant; participant = new Participant(username); participants.add(participant); } else { participants.stream().filter(p -> p.username().equals(username)).findFirst().orElseThrow(); } return participant; } }
변경 후
public class StudyDashboard { . . . 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)); } }
public class StudyDashboard { . . . private void print() throws IOException, InterruptedException { . . . Date firstCreatedAt = null; Participant first = null; for (GHIssueComment comment : comments) { // 성능 Bottleneck (Github API를 사용) 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; . . . } }
checkHomework()
findFirst()
public class StudyDashboard { . . . private void print() throws IOException, InterruptedException { . . . checkHomework(comments, eventId); firstParticipantsForEachEvent[eventId - 1] = findFirst(comments); . . . } 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.setHomeworkDone(eventId); } } }
PinterMode
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); }); } } } } . . . }
exceute()
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)); }); } }
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)); }); } } . . . }
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); }); } } . . . }
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; 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(); } }
냄새 3. 긴 함수 (Long Function)
짧은 함수 vs 긴 함수
사용할 수 있는 리팩토링 기술
함수 추출하기
(Extract Function)로 해결할 수 있다.매개변수가 많아진다면
다음과 같은 리팩토링을 고려해볼 수 있다.임시 변수를 질의 함수로 바꾸기
(Replace Temp with Query)매개변수 객체 만들기
(Introduce Parameter Object)객체 통째로 넘기기
(Preserve Whole Object)조건문 분해하기
(Decompose Conditional)를 이용해 조건문을 분리할 수 있다.조건문을 다형성으로 바꾸기
(Replace Conditional with Polymorphism)을 사용할 수 있다.반복문 쪼개기
(Split Loop)를 적용할 수 있다.리팩토링 7. 임시 변수를 질의 함수로 바꾸기 (Replace Temp With Query)
반복
해서동일한 식
을 계산하는 것을 피할 수 있고, 이름을 사용해 의미를 표현할 수도 있다.임시 변수
를 함수로 추출하여 분리한다면 빼낸 함수로 전달해야 할 매개변수를 줄일 수 있다.예시
getMarkdownForParticipant()
을 먼저 추출해낼 수 있다.넘어가는 인자가 3 개나 되어 유심히 관찰하니,
rate
또한 반복해서 동일한 식을 계산하기에getRate()
로 추출할 수 있고, 추출한 메소드를getMarkdownForParticipant()
에서 직접 호출하게 할 수 있다.리팩토링 8. 매개변수 객체 만들기 (Introduce Parameter Object)
관계
를 보다 명시적으로 나타낼 수 있다.도메인
을 이해하는데 중요한 역할을 하는 클래스로 발전할 수도 있다.예시
Participant
를 묶어두었음에도 불구하고, 아래의 메소들에서 매개변수totalNumberOfEvents
와Participant
가 다시 중복되는 것을 확인할 수 있다.Participant
와totalNumberOfEvents
를 새로운ParticipantPrinter
로 만든다.Introduce parameter field
사용해서 반복되는 매개변수를 필드로 추출해내는 방법도 있다. (totalNumberOfEvents
)리팩토링 9. 객체 통째로 넘기기 (Preserve Whole Object)
예시
매개변수 객체 만들기
에서는 이미객체 통째로 넘기기
도 사용했다.)Participant
를 받는 것이 맞는지, 변경 전의 HashMap을 받는 것이 나은지 고민이 필요하긴 하다. (다른 도메인에도 적용될 수 있는지 등에 따라)Move method
도 고려)리팩토링 10. 함수를 명령으로 바꾸기 (Replace Function with Command)
함수를 독립적인 개체인, Command로 만들어 사용할 수 있다.
커맨드 패턴을 적용하면 다음과 같은 장점을 취할 수 있다.
대부분의 경우에 "커맨드"보다는 "함수"를 사용하지만, 커맨드 말고 다른 방법이 없는 경우에만 사용한다.
먼저 함수를 분리해보고, 그럼에도 위치가 애매하거나 추후 메소드의 복잡도가 높아질 가능성이 있을 경우에 커맨드 패턴을 통해 장점을 취한다.
긴 메소드를 간추리기도 좋고, 복잡도를 낮출 수 있다. (복잡한 메소드를 잘게 쪼개므로써)
그러나 새로운 클래스와 구조가 변경이 되어 오히려 전체적인 복잡도가 증가할 수 있다.
예제
print()
메소드의 복잡도가 높다고 판단하여함수 추출하기
를 사용했고, 추후 확장성을 위해StudyPrinter
에 위치시킨다.StudyPrinter
의 로직이 조금 더 복잡해져도 적절하게 분리되어 있어 이해하기가 용이하다고 생각한다.리팩토링 11. 조건문 분해하기 (Decompose Conditional)
의도
를 표현해야한다.함수 추출하기
와 동일한 리팩토링이지만 의도만 다르다.예제
함수 추출하기
를 통해 조건문에도의도
를 부여했다.변경 후
리팩토링 12. 반복문 쪼개기 (Split Loop)
예제
checkHomework()
,findFirst()
로 분리하여 코드를 쉽게 이해할 수 있게 되었다. (리팩토링 != 성능 최적화)리팩토링 13. 조건문을 다형성으로 바꾸기 (Replace Conditional with Polymorphism)
예시
PinterMode
에 따라 동작하는 switch 문을 다형성으로 바꾸기StudyPrinter
를 상속받는 세 경우의 클래스를 생성했다.StudyPrinter
에 두고 추상 메소드인exceute()
를 생성한다.