조건문을 다형성으로 바꾸기 (Replace conditional with polymorphism)
모든 조건문을 다형성으로 바꿔야 좋은 것은 아닙니다.
여러 타입에 따라 각기 다른 로직으로 처리해야 할 경우에 조건문을 다형성으로 만들 수 있습니다.
공통으로 사용되는 로직은 상위클래스에 두고, 코드 변경의 여지가 있는 달라지는 부분만 하위 클래스에 둠으로써 가독성과 유지보수성을 얻는 것입니다 !
예제
아래의 코드를 보면, printerMode라는 멤버 변수의 값에 따라 switch문에서 각기 다른 로직을 적용하게 됩니다. 각 조건마다 적지 않은 코드가 실행되고, 메서드 호출도 있고, 각각의 하위 메서드도 하나의 클래스에 전부 있는 상태입니다.
public class Printer {
private int totalNumberOfEvents;
private List<Participant> participants;
private PrinterMode printerMode;
public Printer(int totalNumberOfEvents, List<Participant> participants, PrinterMode printerMode) {
this.totalNumberOfEvents = totalNumberOfEvents;
this.participants = participants;
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));
}
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();
}
}
해당하는 조건에 따른 코드를 분리해야 합니다. 새로운 클래스를 만들고 상위 클래스(Printer)를 상속합니다.
그리고, 조건문에서 실행할 로직을 Override함수로 재정의합니다.
[하위 클래스 - ConsolePrinter]
public class ConsolePrinter extends Printer {
public ConsolePrinter(int totalNumberOfEvents, List<Participant> participants, PrinterMode printerMode) {
super(totalNumberOfEvents, participants, printerMode);
}
@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));
});
}
}
사용할 메소드도 상위 클래스가 아닌 하위 클래스로 이동합니다. 만약, 공통 메소드라면 상위 클래스에 두고 protected 제한자를 사용하면 됩니다.
[하위 클래스 - CvsPrinter]
public class CvsPrinter extends Printer {
public CvsPrinter(int totalNumberOfEvents, List<Participant> participants, PrinterMode printerMode) {
super(totalNumberOfEvents, participants, printerMode);
}
@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]
public class MarkdownPrinter extends Printer{
public MarkdownPrinter(int totalNumberOfEvents, List<Participant> participants, PrinterMode printerMode) {
super(totalNumberOfEvents, participants, printerMode);
}
@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));
}
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();
}
}
이제 상위 클래스로 돌아와서 조건에 따라 분기를 치던 메서드(execute)를 추상화(abstract)합니다. 클래스도 추상 클래스로 변경해줘야겠네요 ! 그리고, 공통으로 사용하는 필드가 있다면 상속받아서 사용할 수 있게 protected를 사용합니다.
[상위 클래스 - Printer]
public abstract class Printer {
protected int totalNumberOfEvents;
protected List<Participant> participants;
public Printer(int totalNumberOfEvents, List<Participant> participants) {
this.totalNumberOfEvents = totalNumberOfEvents;
this.participants = participants;
}
public abstract void execute() throws IOException;
}
조건문을 다형성을 사용해서 리팩토링하면 책임도 명확해지고, 클래스가 깔끔해지고 코드 가독성이 좋아진다는 장점이 있습니다!
감사합니다.
출처 : 코딩으로 학습하는 리팩토링 - 인프런 백기선님 강의
'Programming > Refactoring' 카테고리의 다른 글
패키지 의존성 사이클 검사 및 개선하기! (feat. IntelliJ) (0) | 2022.12.12 |
---|---|
DTO를 Inner static class로 간결하게 관리하기! (+ domain 분리) (0) | 2022.04.16 |
리팩토링 - 반복문 분리 (split loop) (0) | 2022.02.26 |
리팩토링 - 메소드 올리기 (Pull up method) (0) | 2022.02.20 |
리팩토링 - 함수 추출 (Extract function) (0) | 2022.02.19 |