해당 포스팅은 인프런 백기선님의 '리팩토링'을 학습 후 정리한 내용입니다.
• 어떤 모듈이 다른 모듈의 내부 정보를 지나치게 많이 알고 있는 코드 냄새. 그로인해 지나치게 강한 결합도(coupling)가 생길 수 있다.
• 적절한 모듈로 “함수 옮기기 (Move Function)”와 “필드 옮기기 (Move Field)”를 사용해서 결합도를 낮출 수 있다.
• 여러 모듈이 자주 사용하는 공통적인 기능은 새로운 모듈을 만들어 잘 관리하거나, “위임숨기기 (Hide Delegate)”를 사용해 특정 모듈의 중재자처럼 사용할 수도 있다.
• 상속으로 인한 결합도를 줄일 때는 “슈퍼클래스 또는 서브클래스를 위임으로 교체하기”를사용할 수 있다.
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 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 클래스로 옮겼다.
• 어떤 클래스가 너무 많은 일을 하다보면 필드도 많아지고 중복 코드도 보이기 시작한다.
• 클라이언트가 해당 클래스가 제공하는 기능 중에 일부만 사용한다면 각각의 세부 기능을별도의 클래스로 분리할 수 있다.
• “클래스 추출하기 (Extract Class)”를 사용해 관련있는 필드를 한 곳으로 모을 수 있다.
• 상속 구조를 만들 수 있다면 “슈퍼클래스 추출하기 (Extract Superclass)”또는 “타입 코드를 서브클래스로 교체하기”를 적용할 수 있다.
• 클래스 내부에 산재하는 중복 코드는 메소드를 추출하여 제거할 수 있다.
• 두개의 클래스에서 비슷한 것들이 보인다면 상속을 적용하고, 슈퍼클래스로 “필드 올리기(Pull Up Field)”와 “메소드 올리기 (Pull Up Method)”를 사용한다.
• 대안으로는 “클래스 추출하기 (Extract Class)”를 적용해 위임을 사용할 수 있다.
• 우선은 간단히 상속을 적용한 이후, 나중에 필요하다면 “슈퍼클래스를 위임으로 교체하기”를 적용한다.
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;
}
}
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();
}
}
부서와 직원 클래스이다. 각 직원과 부서의 달, 연 별 급여 금액을 계산하는 메서드가 있다.
두개의 클래스에서 이름과 달, 연 별 급여 금액을 계산하는 비슷한 것들이 보여 상속구조를 사용할 수 있다.
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();
}
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);
}
}
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 를 추상 메서드로 하여 하위 클래스에서 다르게 구현한다.
• 비슷한 일을 여러 곳에서 서로 다른 규약을 사용해 지원하고 있는 코드 냄새.
• 대안 클래스로 사용하려면 동일한 인터페이스를 구현하고 있어야 한다.
• “함수 선언 변경하기 (Change Function Declaration)”와 “함수 옮기기 (Move
Function)을 사용해서 서로 동일한 인터페이스를 구현하게끔 코드를 수정할 수 있다.
• 두 클래스에서 일부 코드가 중복되는 경우에는“슈퍼클래스 추출하기 (Extract
Superclass)”를 사용해 중복된 코드를 슈퍼클래스로 옮기고 두 클래스를 새로운 슈퍼클래스의 서브클래스로 만들 수 있다.
즉 비슷한 일을 하는 클래스의 인터페이스가 서로 다를 때 이다.
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);
}
}
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 클래스의 인터페이스가 다르다.
서로 동일한 인터페이스를 구현하게끔 코드를 수정할 수 있다.
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);
}
}
인스턴스 필드의 타입을 인터페이스로하여 비슷한 작업을 하는 클래스들이 동일한 인터페이스를 구현하게 끔 하였다.