1. 알고자 하는 것
- 조건문을 다형성으로 바꾸기 (Replace Conditional with PolyMorphism)
2. 알게된 것
- 여러 타입에 따라 각각 다른 로직으로 처리해야 하는 경우, 일반적으로 중첩 if문으로 분기하거나, switch-case 문으로 분기하곤 한다.
// if문 분기
if (Type.A) {
A_Logic();
} else if (Type.B) {
B_Logic();
} else {
C_Logic();
}
// switch문 분기
switch(Type) {
case A -> {
A_Logic();
}
case B -> {
B_Logic();
}
case C -> {
C_Logic();
}
}
- 하지만, 만약 타입 간 공통으로 사용되는 로직이 많고, 타입에 따라 일부분의 로직만 달라지는 경우 다음과 같이 중복된 코드가 많아져 가독성이 떨어지고 코드가 불필요하게 장황해질 수 있다.
// 타입에 따라 달라지는 로직은 일부인데, 공통 로직이 타입 개수만큼 중복된다.
switch(Type) {
case A -> {
Common_Logic_1();
Common_Logic_2();
A_Logic();
Common_Logic_3();
}
case B -> {
Common_Logic_1();
Common_Logic_2();
B_Logic();
Common_Logic_3();
}
case C -> {
Common_Logic_1();
Common_Logic_2();
C_Logic();
Common_Logic_3();
}
}
- 이 때, 공통으로 사용되는 로직은 상위 클래스에 구현하고, 달라지는 부분에 대해서만 하위클래스로 두어 중복 코드를 제거하고, 타입에 따라 달라지는 일부 로직에 대해서만 강조함으로써 가독성을 높일 수 있다. (다형성 활용)
public class StudyPrinter {
...
// 각 타입에 따른 복잡한 로직이 혼재되어 있음
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 method까지 추가됨
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 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();
}
private String getMarkdownForParticipant(Participant p) {
return String.format("| %s %s | %.2f%% |\n", p.username(), checkMark(p),
p.getRate(this.totalNumberOfEvents));
}
}
- StudyPrinter 클래스 내에서 printerMode에 따라 CVS, CONSOLE, MARKDOWN 형식으로 데이터를 출력한다.
- 각 타입에 따른 로직이 복잡하게 구성되어 있어 execute 메서드가 길어지고, 가독성이 떨어진다.
- 또한, 각 로직에 필요한 메서드를 추출함으로써 부가적인 메서드도 많아졌다.
- 각 타입을 StudyPrinter를 상속받는 3개의 하위 클래스로 생성하고 각 로직을 옮긴다. (다형성 적용)
- 이 때, 공통으로 사용하는 로직은 StudyPrinter 내 protected 메서드로 두고, 개별적으로 사용되는 로직은 하위 클래스에 두어 로직을 분리한다.
// 각 타입에서 수행할 메서드는 execute라는 abstract method를 구현한다.
// 타입에서 공통으로 수행하는 checkMark 메서드는 protected method로 상위 클래스에서 공통으로 둔다.
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();
}
}
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) {
...
}
private String cvsHeader(int totalNumberOfParticipants) {
...
}
}
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 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));
}
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();
}
}
- 이 후, 해당 로직을 사용하는 코드에서는 팩토리 메서드 등을 활용해 타입에 따라 해당하는 하위 클래스를 반환해 로직을 수행하도록 처리할 수 있다.
// 팩토리 메서드를 통해 case에 따른 하위 클래스 반환
public class PrinterFactory {
private int totalNumberOfEvents;
private List<Participant> participants;
public PrinterFactory(int totalNumberOfEvents, List<Participant> participants) {
this.totalNumberOfEvents = totalNumberOfEvents;
this.participants = participants;
}
public StudyPrinter getPrinter(PrinterMode printerMode) {
return switch (printerMode) {
case CVS -> new CvsPrinter(totalNumberOfEvents, participants);
case CONSOLE -> new ConsolePrinter(totalNumberOfEvents, participants);
case MARKDOWN -> new MarkdownPrinter(totalNumberOfEvents, participants);
};
}
}
// 사용하는 부분에서는 단순히 팩토리 메서드에 타입만 넘겨서 로직을 수행한다.
new PrinterFactory(totalNumberOfEvents, participants).getPrinter(TYPE).execute();
- 주의할 점은, 모든 switch문에 대해서 해당 리팩토링을 적용하는 것은 올바르지 않다.
- 분기에 따른 n개의 하위클래스를 생성 및 구현해야 하므로 이에 따른 비용이 존재한다.
- 복잡하지 않은 로직이나, 공통된 로직이 많지 않은 경우 등에서는 해당 리팩토링을 수행함으로써 드는 위 비용이 더욱 클 수 있다.
- 또한, 기존 간단한 로직에서 다형성을 통해 추상화함으로써 가독성이 더욱 떨어지는 경우도 있다.
- 상황에 따라 공통된 중복 로직이 많거나, 각 case에 대해 로직들이 복잡한 경우 등을 고려해 해당 리팩토링을 적용해야 한다.
3. 정리
- 여러 타입에 따라 로직을 다르게 처리해야 하는 경우, 일반적으로 if문 또는 switch문을 통해 분기 처리한다.
- 이 때, 각 타입에 대한 로직이 복잡하게 구성되어 있거나, 공통된 로직이 많을 경우 다형성을 활용해 리팩토링 할 수 있다.
- 공통된 로직을 상위 클래스에서 구현하고, 이를 상속받는 하위 클래스를 두어 개별 로직을 구현한다.
- 팩토리 메서드 등을 활용해 타입에 따라 해당하는 하위 클래스를 반환해 로직을 수행하도록 처리할 수 있다.
- 상위 클래스에서 공통 로직을 재사용하고, 변경되는 부분만을 강조할 수 있다.
- 하지만 해당 리팩토링은 추상화, 하위클래스 생성에 따른 비용이 존재하므로, 공통된 중복로직이 많거나 각 case에 대해 로직이 복잡한 경우에 비용을 고려해 리팩토링을 진행할 수 있도록 해야한다.
Reference
'Clean Code' 카테고리의 다른 글
Java 리팩토링 - 변수 캡슐화하기 (with. Getter/Setter) (0) | 2023.12.17 |
---|---|
Java 리팩토링 - 플래그 인수 제거하기 (0) | 2023.11.12 |
Java 리팩토링 - 조건문 분해하기 (0) | 2023.10.31 |
Java 리팩토링 - 함수를 명령(Command)으로 바꾸기 (0) | 2023.10.06 |
Java 리팩토링 - 메서드 추출 시 매개변수가 많아질 경우 [3. 객체 통째로 넘기기] (0) | 2023.09.26 |