현재 나에게는 중복 코드가 발생하는 일이 많다. 많은 경험이 쌓이지 않아서라고 위안을 삼고 중복 코드를 분리하고 재활용하는 방안으로 여러 디자인 패턴도 공부하고 적용해본다.
그렇지만 설계하는 방향에서는 항상 중복 코드가 보이긴 했다.
책에선 다음과 같이 한 클래스에 딸린 두 메서드가 같은 표현식을 사용한다면 함수를 추출하여 양쪽 모두 추출된 메서드를 호출하게 바꾸라 한다.
나도 이를 보고 재활용 가능하게 함수를 추출하여 리팩터링을 진행한 적이 있다. 이 과정에서 미묘한 차이에 대해 처리하는 부분이 가장 힘들었다.
미묘한 차이에 대한 처리에 대한 팁은 다음과 같은데 비슷한 부분을 한 곳에 모아 함수를 쉽게 추출할 수 있는지 살펴보자.
또한 서브 클래스에 코드가 중복된다면 상위 클래스로 옮겨 중복을 제거할 수 있고 인터페이스를 사용한다면 나는 default 메서드를 활용하는 편이다.
함수 추출
저자의 원칙을 엿보자.
코드를 언제 함수로 추출해야하는지에 대해 의견은 수없이 많다. 함수의 길이, 재사 용성면에서 두 번 이상 사용될 코드는 함수로 만들고, 한 번만 쓰이는 코드는 인라인 상태로 유지한다.
하지만 저자는 목적과 구현을 분리하는 방식이 가장 합리적으로 생각한다고 한다.
코드를 보고 무슨 일을 하는지 파악하는데 시간이 걸린다면 그 부분을 함수로 추출한 뒤 무슨 일에 걸맞은 이름을 짓는다.
private void printParticipants(int eventId) throws IOException {
// Get github issue to check homework
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("...");
GHIssue issue = repository.getIssue(eventId);
// Get participants
Set<String> participants = new HashSet<>();
issue.getComments().forEach(c -> participants.add(c.getUserName()));
// Print participants
participants.forEach(System.out::println);
}
해당 함수에서 구현과 의도를 생각하면서 읽어보자.
해당 함수는 특정한 이벤트에 참가한 참가자의 이름을 출력하는 함수이다.
1. 해당 이벤트를 가져온다.
2. 모든 코멘트를 가져와 참가자의 이름을 별도로 추출한다.
3. 그 다음 참가자 이름을 출력한다.
추출되는 함수가 중복되어 사용되지 않을 수 있다. 그렇지만 구현에 맞춰서 함수를 추출하게 되면 코드를 읽느라 많은 시간을 허비하지 않아도 될 것 같다.
다음과 같이 코드를 변경해보자.
private void printParticipants(int eventId) throws IOException {
GHIssue issue = getGhIssue(eventId);
Set<String> participants = getUsernames(issue);
print(participants);
}
한 줄짜리 코드도 과감히 함수로 추출했다. 이제 더 이상 함수를 읽는데 많은 노력을 하지 않아도 함수의 의도를 좀 더 빠르게 파악할 수 있다.
코드 정리
코드가 분산되어 있어 동일한 작업을 하는지 모를 때가 있다. 그럴 때 중복되는 코드를 찾기 위해 코드를 조절하면서 함수 추출을 위해 코드를 정리할 수 있다.
여기서 하나의 팁을 얻었는데 나는 습관인지 모르겠지만 모든 변수는 최상단에 선언하는 습관이 존재한다.
저자는 이럴 경우 해당 변수를 사용하는 부분 위에서 초기화 또는 선언을 하라고 조언한다.
이러한 팁은 해당 코드 덩어리를 쉽게 하나의 단위로 추출할 수 있도록 도와줄 수 있기 때문이다.
바로 회사가서 적용해봐야겠다.!
private void printParticipants(int eventId) throws IOException {
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("...");
GHIssue issue = repository.getIssue(eventId);
Set<String> participants = new HashSet<>();
issue.getComments().forEach(c -> participants.add(c.getUserName()));
participants.forEach(System.out::println);
}
private void printReviewers() throws IOException {
Set<String> reviewers = new HashSet<>();
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("...");
GHIssue issue = repository.getIssue(30);
issue.getComments().forEach(c -> reviewers.add(c.getUserName()));
reviewers.forEach(System.out::println);
}
한눈에 해당 코드가 동일한 구조를 가질 수 있는 가능성이 보이지 않는다.
이를 위 팁을 이용해 처리하면 중복 코드를 빠르게 파악해 함수 추출의 발판을 만들 수 있다는 것이
코드 정리 ( 문장 슬라이스)에 의도이지 않을까 싶다.
private void printParticipants(int eventId) throws IOException {
// Get github issue to check homework
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("...");
GHIssue issue = repository.getIssue(eventId);
// Get participants
Set<String> participants = new HashSet<>();
issue.getComments().forEach(c -> participants.add(c.getUserName()));
// Print participants
participants.forEach(System.out::println);
}
private void printReviewers() throws IOException {
// Get github issue to check homework
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("...");
GHIssue issue = repository.getIssue(30);
// Get reviewers
Set<String> reviewers = new HashSet<>();
issue.getComments().forEach(c -> reviewers.add(c.getUserName()));
// Print reviewers
reviewers.forEach(System.out::println);
}
메서드 올리기
메서드를 하위에서 상위로 올리는 방법.
사실 이러한 방법은 항상 고민하는데 가장 복잡한 경우를 소개하려고 한다.
해당 메서드의 본문에서 참조하는 필드가 서브클래스에만 존재하는 경우이다. 이러한 필드를 상위 클래스로 올리는 것은 약간의 스턴을 가한다. 공통되지 않는 필드를 올리는 기분이랄까. 저자는 이런 경우 슈퍼 클래스로 올려 처리하라고 한다.
보통 이러한 메서드를 상위로 올리기 위해선 선행 단계가 필요할 때가 많다.
public class ParticipantDashboard extends Dashboard {
public void printParticipants(int eventId) throws IOException {
// Get github issue to check homework
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("...");
GHIssue issue = repository.getIssue(eventId);
// Get participants
Set<String> participants = new HashSet<>();
issue.getComments().forEach(c -> participants.add(c.getUserName()));
// Print participants
participants.forEach(System.out::println);
}
}
public class ReviewerDashboard extends Dashboard {
public void printReviewers() throws IOException {
// Get github issue to check homework
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("...");
GHIssue issue = repository.getIssue(30);
// Get reviewers
Set<String> reviewers = new HashSet<>();
issue.getComments().forEach(c -> reviewers.add(c.getUserName()));
// Print reviewers
reviewers.forEach(System.out::println);
}
}
해당 클래스의 메서드는 비슷하게 보이는 메서드를 코드 정리를 통해 찾았다. 하지만 미세하게 다른 부분이 있는데
이럴 경우 적용할 수 있는 리팩터링 방법이 있다.
1. 매개변수
이 방법은 매개변수를 받도록 변경하는 방법이다.
public void printReviewers() throws IOException {
printUsernames(30);
}
private void printUsernames(int eventId) throws IOException {
// Get github issue to check homework
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("whiteship/live-study");
GHIssue issue = repository.getIssue(eventId);
// Get usernames
Set<String> usernames = new HashSet<>();
issue.getComments().forEach(c -> usernames.add(c.getUserName()));
// Print usernames
usernames.forEach(System.out::println);
}
public void printParticipants(int eventId) throws IOException {
printUsernames(eventId);
}
private void printUsernames(int eventId) throws IOException {
// Get github issue to check homework
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("whiteship/live-study");
GHIssue issue = repository.getIssue(eventId);
// Get participants
Set<String> participants = new HashSet<>();
issue.getComments().forEach(c -> participants.add(c.getUserName()));
// Print participants
participants.forEach(System.out::println);
}
이제 printUsernames가 동일해졌기 때문에 우리는 메서드 올리기를 해볼 수 있다.
public class Dashboard {
public static void main(String[] args) throws IOException {
ReviewerDashboard reviewerDashboard = new ReviewerDashboard();
reviewerDashboard.printReviewers();
ParticipantDashboard participantDashboard = new ParticipantDashboard();
participantDashboard.printParticipants(15);
}
protected void printUsernames(int eventId) throws IOException {
// Get github issue to check homework
GitHub gitHub = GitHub.connect();
GHRepository repository = gitHub.getRepository("...");
GHIssue issue = repository.getIssue(eventId);
// Get participants
Set<String> participants = new HashSet<>();
issue.getComments().forEach(c -> participants.add(c.getUserName()));
// Print participants
participants.forEach(System.out::println);
}
}
public void printParticipants(int eventId) throws IOException {
super.printUsernames(eventId);
}
public void printReviewers() throws IOException {
super.printUsernames(30);
}
메서드를 상위로 올리면서 하위 클래스의 중복 코드를 제거하여 깔끔하게 변경되었다.
사실 요즘 상속 구조를 이용하는 코드를 접하지 못하는 것도 사실이다.
상속의 한계를 알기 때문에 대부분 인터페이스를 활용한 다형성을 구현하기 때문인데, 만약 레거시에 다음과 같은 코드가 있다면 이렇게 리팩터링 해보는 것은 어떨까?
'독서에서 한걸음' 카테고리의 다른 글
긴 함수 (2) (0) | 2022.12.02 |
---|---|
긴 함수 (1) (0) | 2022.12.02 |
Refactoring_2 이해하기 힘든 이름 (0) | 2022.11.27 |
Clean Code .Part14 (1) (0) | 2022.08.15 |
Refactoring .Chapter 02 (0) | 2022.08.15 |
댓글