1. 알고자 하는 것
여러 메서드에서 특정 데이터를 기준으로 계속 로직이 분기된다면, 이를 아예 클래스로 분리해 코드의 길이를 대폭 줄일 수 있다.
그리고 그 중심에는 다형성이 있다.
무궁무진한 다형성의 활용도를 익히기 위해 더 열심히 공부해야겠다는 괜한 동기부여가 되는 주제인 것 같다.
- 특이 케이스 추가하기 (Introduce Special Case)
2. 알게 된 것
- 위에서 말한 "특정 데이터를 기준으로 계속 로직이 분기" 되는 상황을 특이 케이스 (Special Case)라고 정의할 수 있다.
- 가령, 다음과 같은 형식이다.
public class Customer {
private String name;
private BillingPlan billingPlan;
private PaymentHistory paymentHistory;
...
}
public class CustomerService {
public String customerName(Site site) {
Customer customer = site.getCustomer();
String customerName;
// Customer의 Name이 unknown인지에 따른 분기 (특이 케이스)
if (customer.getName().equals("unknown")) {
customerName = "occupant";
} else {
customerName = customer.getName();
}
return customerName;
}
public BillingPlan billingPlan(Site site) {
Customer customer = site.getCustomer();
// Customer의 Name이 unknown인지에 따른 분기 (특이 케이스)
return customer.getName().equals("unknown") ? new BasicBillingPlan() : customer.getBillingPlan();
}
public int weeksDelinquent(Site site) {
Customer customer = site.getCustomer();
// Customer의 Name이 unknown인지에 따른 분기 (특이 케이스)
return customer.getName().equals("unknown") ? 0 : customer.getPaymentHistory().getWeeksDelinquentInLastYear();
}
}
- Customer의 이름이 'unknown' 인지를 지겹도록 확인한다.
- 3개 메서드 모두 'unknown' 여부에 따라 반환하는 값이 다르다.
- 이 때, 'unknown'에 해당하는 Customer를 Customer를 상속받는 UnknownCustomer 클래스로 분리해서 unknown일 때 반환하는 값들을 필드로 가지게 할 수 있다.
public class UnknownCustomer extends Customer {
public UnknownCustomer() {
super("unknown", null, null);
}
@Override
public boolean isUnknown() {
return true;
}
@Override
public String getName() {
return "occupant";
}
@Override
public BillingPlan getBillingPlan() {
return new BasicBillingPlan();
}
}
- 이렇게 하면, 이제 Site에서는 다형성을 활용해 'unknown' 여부에 따라 Customer를 가지고 있을 수 있다.
public class Site {
private Customer customer;
public Site(Customer customer) {
// UnknownCustomer를 만들었으므로 분기해서 저장 가능
this.customer = customer.isUnknown() ? new UnknownCustomer() : customer;
}
public Customer getCustomer() {
return customer;
}
}
- 이제 CustomerService는 다형성을 통해 단순히 Site가 가지는 Customer의 데이터를 반환하면 된다.
- 이미 Site가 가지는 Customer에서 unknown / 그 외일 때 데이터를 각각 구분지어 가지고 있다.
public class CustomerService {
public String customerName(Site site) {
return site.getCustomer().getName();
// String customerName;
// if (customer.getName().equals("unknown")) {
// customerName = "occupant";
// } else {
// customerName = customer.getName();
// }
//
// return customerName;
}
public BillingPlan billingPlan(Site site) {
return site.getCustomer().getBillingPlan();
// return customer.getName().equals("unknown") ? new BasicBillingPlan() : customer.getBillingPlan();
}
public int weeksDelinquent(Site site) {
// NPE 발생!!!!
return site.getCustomer().getPaymentHistory().getWeeksDelinquentInLastYear();
// return customer.getName().equals("unknown") ? 0 : customer.getPaymentHistory().getWeeksDelinquentInLastYear();
}
}
- 이 때, 문제가 되는 부분은 Customer가 가지고 있는 필드인 PaymentHistory인데, 'unknown'의 경우에는 PaymentHistory를 가지고 있지 않다(null).
- 따라서 마지막 메서드에서도 다른 메서드와 같이 getCustomer().getPaymentHistory().getWeeks..() 를 반환하려고 하면 NPE가 발생한다.
- 이 때에는 null인 경우를 또 다른 특이 케이스로 판단하고 null에 대응하는 클래스를 분리할 수 있는데, 이를 Null Object 패턴이라고 한다.
- PaymentHistory를 상속받는 NullPaymentHistory 클래스를 만들고, 해당 클래스는 weeksDelinquentInLastYear의 값을 0으로 설정해주면 된다.
public class NullPaymentHistory extends PaymentHistory {
public NullPaymentHistory() {
super(0);
}
}
public class UnknownCustomer extends Customer {
public UnknownCustomer() {
super("unknown", null, new NullPaymentHistory()); // NullPaymentHistory 설정
}
...
}
- 이제 Customer가 'unknown' 이라도 안전하고 쉽게 CustomerService에서 weeksDelinquentInLastYear를 찾을 수 있다.
public int weeksDelinquent(Site site) {
// OK
return site.getCustomer().getPaymentHistory().getWeeksDelinquentInLastYear();
// return customer.getName().equals("unknown") ? 0 : customer.getPaymentHistory().getWeeksDelinquentInLastYear();
}
3. 정리
- 여러 메서드에서 특정 데이터를 기준으로 계속 로직이 분기된다면(특이 케이스), 이를 아예 클래스로 분리하고 상속과 다형성을 활용해 코드의 길이를 대폭 줄일 수 있다.
- 분기에 따라 가지는 값을 새로 분리한 클래스의 필드로 가지게 한다.
- 이를 사용하는 로직에서는 단순히 다형성을 활용해 클래스가 가지는 값을 반환하게 하면 된다.
- 분기에 따라 특정 필드가 null인 경우, 이 역시도 특이 케이스로 생각하고 null 조건에 해당하는 클래스를 분리할 수 있다. (Null Object 패턴)
Reference
'Clean Code' 카테고리의 다른 글
Java 리팩토링 - 상속을 위임으로 바꾸기 (Replace Inherit with Delegate) (1) | 2024.01.21 |
---|---|
Java 리팩토링 - 기본형을 객체로 바꾸기 (Replace Primitive with Object) (0) | 2024.01.15 |
Java 리팩토링 - 함수 인라인, 클래스 인라인 (Function Inline, Class Inline) (0) | 2024.01.14 |
Java 리팩토링 - 함수 옮기기 (Move Function) (0) | 2023.12.25 |
Java 리팩토링 - 변수 캡슐화하기 (with. Getter/Setter) (0) | 2023.12.17 |