리팩토링 - 19, 20, 21

김상운(개발둥이)·2022년 8월 25일
0

리팩토링

목록 보기
16/17
post-thumbnail

들어가기

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



냄새 19. 내부자 거래

Insider Trading

• 어떤 모듈이 다른 모듈의 내부 정보를 지나치게 많이 알고 있는 코드 냄새. 그로인해 지나치게 강한 결합도(coupling)가 생길 수 있다.
• 적절한 모듈로 “함수 옮기기 (Move Function)”와 “필드 옮기기 (Move Field)”를 사용해서 결합도를 낮출 수 있다.
• 여러 모듈이 자주 사용하는 공통적인 기능은 새로운 모듈을 만들어 잘 관리하거나, “위임숨기기 (Hide Delegate)”를 사용해 특정 모듈의 중재자처럼 사용할 수도 있다.
• 상속으로 인한 결합도를 줄일 때는 “슈퍼클래스 또는 서브클래스를 위임으로 교체하기”를사용할 수 있다.

예제 코드

Ticket

public class Ticket {

    private LocalDate purchasedDate;

    private boolean prime;

    public Ticket(LocalDate purchasedDate, boolean prime) {
        this.purchasedDate = purchasedDate;
        this.prime = prime;
    }

    public LocalDate getPurchasedDate() {
        return purchasedDate;
    }

    public boolean isPrime() {
        return prime;
    }
}

CheckIn


public class CheckIn {

    public boolean isFastPass(Ticket ticket) {
        LocalDate earlyBirdDate = LocalDate.of(2022, 1, 1);
        return ticket.isPrime() && ticket.getPurchasedDate().isBefore(earlyBirdDate);
    }
}

냄새

CheckIn 클래스의 isFastPass() 메서드는 Ticket 을 참조하고 있는데 Ticket 안의 내부 정보를 더 많이 참조하고 있다. (어떤 모듈이 다른 모듈의 정보를 지나치게 많이 알고있는..)

해결

메서드를 Ticket 으로 옮겨 해결한다.

리팩토링 후

public class Ticket {

    private LocalDate purchasedDate;

    private boolean prime;

    public Ticket(LocalDate purchasedDate, boolean prime) {
        this.purchasedDate = purchasedDate;
        this.prime = prime;
    }

    public LocalDate getPurchasedDate() {
        return purchasedDate;
    }

    public boolean isPrime() {
        return prime;
    }

    public boolean isFastPass() {
        LocalDate earlyBirdDate = LocalDate.of(2022, 1, 1);
        return isPrime() && getPurchasedDate().isBefore(earlyBirdDate);
    }
}

CheckIn 클래스의 isFastPass() 메서드를 Ticket 클래스로 옮겼다.

냄새 20. 거대한 클래스

Large Class

• 어떤 클래스가 너무 많은 일을 하다보면 필드도 많아지고 중복 코드도 보이기 시작한다.
• 클라이언트가 해당 클래스가 제공하는 기능 중에 일부만 사용한다면 각각의 세부 기능을별도의 클래스로 분리할 수 있다.
• “클래스 추출하기 (Extract Class)”를 사용해 관련있는 필드를 한 곳으로 모을 수 있다.
• 상속 구조를 만들 수 있다면 “슈퍼클래스 추출하기 (Extract Superclass)”또는 “타입 코드를 서브클래스로 교체하기”를 적용할 수 있다.
• 클래스 내부에 산재하는 중복 코드는 메소드를 추출하여 제거할 수 있다.

리팩토링 41. 슈퍼클래스 추출하기

Extract Superclass

• 두개의 클래스에서 비슷한 것들이 보인다면 상속을 적용하고, 슈퍼클래스로 “필드 올리기(Pull Up Field)”와 “메소드 올리기 (Pull Up Method)”를 사용한다.
• 대안으로는 “클래스 추출하기 (Extract Class)”를 적용해 위임을 사용할 수 있다.
• 우선은 간단히 상속을 적용한 이후, 나중에 필요하다면 “슈퍼클래스를 위임으로 교체하기”를 적용한다.

예제 코드

Employee

public class Employee {

    private Integer id;

    private String name;

    private double monthlyCost;

    public double annualCost() {
        return this.monthlyCost * 12;
    }

    public Integer getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public double getMonthlyCost() {
        return monthlyCost;
    }
}

Department

public class Department {

    private String name;

    private List<Employee> staff;

    public String getName() {
        return name;
    }

    public List<Employee> getStaff() {
        return staff;
    }

    public double totalMonthlyCost() {
        return this.staff.stream().mapToDouble(e -> e.getMonthlyCost()).sum();
    }

    public double totalAnnualCost() {
        return this.totalMonthlyCost() * 12;
    }

    public int headCount() {
        return this.staff.size();
    }
}

설명

부서와 직원 클래스이다. 각 직원과 부서의 달, 연 별 급여 금액을 계산하는 메서드가 있다.

냄새

두개의 클래스에서 이름과 달, 연 별 급여 금액을 계산하는 비슷한 것들이 보여 상속구조를 사용할 수 있다.

리팩토링 후

Party

public abstract class Party {
    protected String name;

    public Party(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public double annualCost() {
        return this.monthlyCost() * 12;
    }

    protected abstract double monthlyCost();
}

Employee

public class Employee extends Party {

    private Integer id;

    private double monthlyCost;

    public Integer getId() {
        return id;
    }

    @Override
    public double monthlyCost() {
        return monthlyCost;
    }

    public Employee(String name) {
        super(name);
    }
}

Department

public class Department extends Party {

    private List<Employee> staff;

    public List<Employee> getStaff() {
        return staff;
    }

    @Override
    public double monthlyCost() {
        return this.staff.stream().mapToDouble(e -> e.monthlyCost()).sum();
    }

    public int headCount() {
        return this.staff.size();
    }

    public Department(String name) {
        super(name);
    }
}

설명

공통적인 기능을 상위 클래스인 Party 클래스에 정의한다. 각 클래스의 name 을 pull up filed 를 사용하며 monthlyCost 를 추상 메서드로 하여 하위 클래스에서 다르게 구현한다.

냄새 21. 서로 다른 인터페이스의 대안 클래스들

Alternative Classes with Different Interfaces

• 비슷한 일을 여러 곳에서 서로 다른 규약을 사용해 지원하고 있는 코드 냄새.
• 대안 클래스로 사용하려면 동일한 인터페이스를 구현하고 있어야 한다.
• “함수 선언 변경하기 (Change Function Declaration)”와 “함수 옮기기 (Move
Function)을 사용해서 서로 동일한 인터페이스를 구현하게끔 코드를 수정할 수 있다.
• 두 클래스에서 일부 코드가 중복되는 경우에는“슈퍼클래스 추출하기 (Extract
Superclass)”를 사용해 중복된 코드를 슈퍼클래스로 옮기고 두 클래스를 새로운 슈퍼클래스의 서브클래스로 만들 수 있다.

즉 비슷한 일을 하는 클래스의 인터페이스가 서로 다를 때 이다.

예제 코드

OrderProcessor

public class OrderProcessor {

    private EmailService emailService;

    public void notifyShipping(Shipping shipping) {
        EmailMessage emailMessage = new EmailMessage();
        emailMessage.setTitle(shipping.getOrder() + " is shipped");
        emailMessage.setTo(shipping.getEmail());
        emailMessage.setFrom("no-reply@whiteship.com");
        emailService.sendEmail(emailMessage);
    }

}

OrderAlerts

public class OrderAlerts {

    private AlertService alertService;
    
    public void alertShipped(Order order) {
        AlertMessage alertMessage = new AlertMessage();
        alertMessage.setMessage(order.toString() + " is shipped");
        alertMessage.setFor(order.getEmail());
        alertService.add(alertMessage);
    }
}

설명

각 클래스는 이메일, 알림 서비스를 제공하는 클래스이며 사용자에게 알림을 보내는 비슷한 일을 한다. 또한 내부에서 사용하는 sevicer 클래스의 인터페이스가 다르다.

냄새

서로 동일한 인터페이스를 구현하게끔 코드를 수정할 수 있다.

리팩토링 후

NotificationService

public interface NotificationService {

    public void sendNotification(Notification notification);

}

알림을 보내는 공통적인 로직을 수행하기 때문에 공통으로 사용할 인터페이스를 생성한다.

public class EmailNotificationService implements NotificationService{

    private EmailService emailService;

    @Override
    public void sendNotification(Notification notification) {
        EmailMessage emailMessage = new EmailMessage();
        emailMessage.setTitle(notification.getTitle());
        emailMessage.setTo(notification.getReceiver());
        emailMessage.setFrom(notification.getSender());
        emailService.sendEmail(emailMessage);
    }
}

인터페이스를 구현하여 어떻게 알림을 보낼지 구현체 클래스에 구현한다.

public class OrderProcessor {

    private NotificationService notificationService;

    public void notifyShipping(Shipping shipping) {
        Notification notification = Notification.newNotification(shipping.getOrder() + " is shipped")
                .receiver(shipping.getEmail())
                .sender("no-reply@naver.com");
        notificationService.sendNotification(notification);
    }

}

해결

인스턴스 필드의 타입을 인터페이스로하여 비슷한 작업을 하는 클래스들이 동일한 인터페이스를 구현하게 끔 하였다.

profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

0개의 댓글