리팩토링 - 냄새 3. 긴 함수

김상운(개발둥이)·2022년 3월 28일
1

리팩토링

목록 보기
4/17
post-thumbnail

들어가기

해당 포스팅은 인프런 백기선님의 '리팩토링'을 학습 후 정리한 내용입니다.



냄새 3. 긴 함수

Long Function

  • 짧은 함수 vs 긴 함수
    • 함수가 길 수록 더 이해하기 어렵다. vs 짧은 함수는 더 많은 문맥 전환을 필요로 한다.
    • “과거에는” 작은 함수를 사용하는 경우에 더 많은 서브루틴 호출로 인한 오버헤드가 있었다.
    • 작은 함수에 “좋은 이름”을 사용했다면 해당 함수의 코드를 보지 않고도 이해할 수 있다.
    • 어떤 코드에 “주석”을 남기고 싶다면, 주석 대신 함수를 만들고 함수의 이름으로 “의도”를 표현해보자.
  • 사용할 수 있는 리팩토링 기술
    • 99%는, “함수 추출하기 (Extract Function)”로 해결할 수 있다.
    • 함수로 분리하면서 해당 함수로 전달해야 할 매개변수가 많아진다면 다음과 같은 리팩토링을 고려해볼 수 있다.
      • 임시 변수를 질의 함수로 바꾸기 (Replace Temp with Query)
      • 매개변수 객체 만들기 (Introduce Parameter Object)
      • 객체 통째로 넘기기 (Preserve Whole Object)
  • 조건문 분해하기 (Decompose Conditional)를 사용해 조건문을 분리할 수 있다.
  • 같은 조건으로 여러개의 Swtich 문이 있다면, 조건문을 다형성으로 바꾸기 (Replace Conditional with Polymorphism)을 사용할 수 있다.
  • 반복문 안에서 여러 작업을 하고 있어서 하나의 메소드로 추출하기 어렵다면, 반복문 쪼개기 (Split Loop)를 적용할 수 있다.


임시 변수를 질의 함수로 바꾸기

Replace Temp with Query

  • 변수를 사용하면 반복해서 동일한 식을 계산하는 것을 피할 수 있고, 이름을 사용해 의미를 표현할 수도 있다.
  • 긴 함수를 리팩토링할 때, 그러한 임시 변수를 함수로 추출하여 분리한다면 빼낸 함수로 전달해야 할 매개변수를 줄일 수 있다.

예시

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() 함수를 사용하여 불필요한 인자를 줄였다.





매개변수 객체 만들기

Introduce Parameter Object

  • 같은 매개변수들이 여러 메소드에 걸쳐 나타난다면 그 매개변수들을 묶은 자료 구조를 만들 수 있다.
  • 그렇게 만든 자료구조는:
    • 해당 데이터간의 관계를 보다 명시적으로 나타낼 수 있다.
    • 함수에 전달할 매개변수 개수를 줄일 수 있다.
    • 도메인을 이해하는데 중요한 역할을 하는 클래스로 발전할 수도 있다

예시

public String getMarkdownForParticipant(int totalNumberOfEvents, Participant p) {
	....
}

private String header(int totalNumberOfEvents, int totalNumberOfParticipants) {
	....
}

냄새

반복되는 인자를 갖는 함수들이다.

위의 설명처럼 같은 매개변수들이 여러 메소드에 걸쳐 나타난다면 그 매개변수들을 묶은 자료구조를 만들 수 있다.

해결

  • record 와 같은 자료구조를 만들어 해결한다.
  • 함수 내부에서 자주 쓰이는 공통적인 매개변수를 필드로 바꾼다.

리팩토링 후

ParticipantPrinter

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

  • 어떤 한 레코드에서 구할 수 있는 여러 값들을 함수에 전달하는 경우, 해당 매개변수를 레코드 하나로 교체할 수 있다.
  • 매개변수 목록을 줄일 수 있다. (향후에 추가할지도 모를 매개변수까지도…)
  • 이 기술을 적용하기 전에 의존성을 고려해야 한다.
  • 어쩌면 해당 메소드의 위치가 적절하지 않을 수도 있다. (기능 편애 “Feature Envy” 냄새에 해당한다.

예시

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 내부에서 사용한다.

리팩토링 후

getMarkdownForParticipant() 함수

private String getMarkdownForParticipant(Participant participant) {
        return String.format("| %s %s | %.2f%% |\n", participant.username(),
                checkMark(participant, this.totalNumberOfEvents),
                participant.getRate(this.totalNumberOfEvents));
    }

Participant

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);
    }
}

설명

  • 위의 해결처럼 매개변수를 Participant 객체화하여 매개변수 목록을 줄였다.
  • 매개변수를 사용하던 getRate() 함수의 위치를 Participant 객체 내부로 이동하였다.

유의사항

  • 이 메서드가 이 클래스에 의존하는게 맞는가?
  • 이메서드는 이 위치에 있는게 맞는가 아님 매개변수 타입에 해당하는 메서드로 가야하는가?

    💡 즉 매개변수를 객체화하여 사용할 경우 해당 매개변수를 인자로 하는 함수에 위 두가지 의문을 가져야 한다.


함수를 명령으로 바꾸기

Replace Function with Command

  • 함수를 독립적인 객체인, Command로 만들어 사용할 수 있다.
  • 커맨드 패턴을 적용하면 다음과 같은 장점을 취할 수 있다.
    • 부가적인 기능으로 undo 기능을 만들 수도 있다.
    • 더 복잡한 기능을 구현하는데 필요한 여러 메소드를 추가할 수 있다.
    • 상속이나 템플릿을 활용할 수도 있다.
    • 복잡한 메소드를 여러 메소드나 필드를 활용해 쪼갤 수도 있다.
  • 대부분의 경우에 “커맨드” 보다는 “함수”를 사용하지만, 커맨드 말고 다른 방법이 없는 경우에만 사용한다.

예시

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();
    }

StudyPrinter

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 를 해준다.



조건문 분해하기

Decompose Conditional

  • 여러 조건에 따라 달라지는 코드를 작성하다보면 종종 긴 함수가 만들어지는 것을 목격할수 있다.
  • “조건”과 “액션” 모두 “의도”를 표현해야한다.
  • 기술적으로는 “함수 추출하기”와 동일한 리팩토링이지만 의도만 다를 뿐이다

예시 코드

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() 함수로 추출하였으며, 각 조건에 맞는 구현을 추출하여 의도로 표현하였다.



반복문 쪼개기

Split Loop

  • 하나의 반복문에서 여러 다른 작업을 하는 코드를 쉽게 찾아볼 수 있다.
  • 해당 반복문을 수정할 때 여러 작업을 모두 고려하며 코딩을 해야한다.
  • 반복문을 여러개로 쪼개면 보다 쉽게 이해하고 수정할 수 있다.
  • 성능 문제를 야기할 수 있지만, “리팩토링”은 “성능 최적화”와 별개의 작업이다. 리팩토링
    을 마친 이후에 성능 최적화 시도할 수 있다.

예시

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);
        }
    }       
        

설명

반복문을 하나 더 만들어 분리하였으며, 반복문을 함수로 추출하였다.



조건문을 다형성으로 바꾸기

Replace Conditional with Polymorphism

  • 여러 타입에 따라 각기 다른 로직으로 처리해야 하는 경우에 다형성을 적용해서 조건문을 보다 명확하게 분리할 수 있다. (예, 책, 음악, 음식 등…) 반복되는 switch문을 각기 다른클래스를 만들어 제거할 수 있다.
  • 공통으로 사용되는 로직은 상위클래스에 두고 달라지는 부분만 하위클래스에 둠으로써, 달라지는 부분만 강조할 수 있다.
  • 모든 조건문을 다형성으로 바꿔야 하는 것은 아니다.

예시

StudyPrinter

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 조건에 따라 수행이 다르다.

해결

해당 클래스를 추상화하고 다형성을 활용하자!

리팩토링 후

StudyPrinter

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 하였다.

ConsolePrinter

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));
        });
    }

}

CvsPrinter

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();
    }

}

MarkdownPrinter

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 를 상속 받은 클래스는 자신의 목적에 알맞은 메서드를 사용한다.
profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

0개의 댓글