리팩토링에서도 어떤 냄새의 해결 방안으로 제안된 방법을 지나치게 사용한다면, 그 또한 냄새가 될 수 있다.
메세지 체이닝 등을 위임 숨기기로 감추는 작업이 많아지다 보면 "중재자" 냄새가 날 수 있다.
굳이 그럴 필요가 없음에도 불구하고 밖에서 필요로하는 코드를 중재자를 통해 사용하게 되는 경우
그러나, 어떤 클래스의 메소드가 대부분 다른 클래스로 메소드 호출을 위임하고 있다면 중재자를 제거하고 클라이언트가 해당 클래스를 직접 사용하도록 코드를 개선할 수 있다.
관련 리팩토링
“중재자 제거하기 (Remove Middle Man)” 리팩토링을 사용해 클라이언트가 필요한 클래스를 직접 사용하도록 개선할 수 있다.
“함수 인라인 (Inlince Function)”을 사용해서 메소드 호출한 쪽으로 코드를 보내서 중재자를 없앨 수도 있다.
“슈퍼클래스를 위임으로 바꾸기 (Replace Superclass with Delegate)”
“서브클래스를 위임으로 바꾸기 (Replace Subclass with Delegate)"
리팩토링 38. 중재자 제거하기 (Remove Middle Man)
“위임 숨기기(Hide Delegate)”의 반대에 해당하는 리팩토링.
필요한 캡슐화의 정도는 시간에 따라 그리고 상황에 따라 바뀔 수 있다.
캡슐화의 정도를 “중재자 제거하기”와 “위임 숨기기” 리팩토링을 통해 조절할 수 있다.
위임하고 있는 객체를 클라이언트가 사용할 수 있도록 getter를 제공하고, 클라이언트는 메시지 체인을 사용하도록 코드를 고친 뒤에 캡슐화에 사용했던 메소드를 제거한다.
Law of Demeter를 지나치게 따르기 보다는 상황에 맞게 활용하도록 하자.
디미터의 법칙, “가장 가까운 객체만 사용한다.”
메소드 안에서 그 메소드 안에 포함되어있는 다른 클래스의 객체
메소드가 파라미터로 받은 객체, 레코드
가지고 있는 필드의 메소드
예시 코드
Before
getManager()
Department의 필드에 name, accessibleTime, numberOfMembers 등이 추가된다면, 굳이 Person을 통해 department의 manager에 접근하는게 옳은 방법일까?
public class Person {
private Department department;
private String name;
public Person(String name, Department department) {
this.name = name;
this.department = department;
}
public Person getManager() {
return this.department.getManager();
}
}
After
어디까지 캡슐화를 할 것인가, 에 대한 생각이 중요한 부분이다.
public class Person {
private Department department;
private String name;
public Person(String name, Department department) {
this.name = name;
this.department = department;
}
public Department getDepartment() {
return this.department();
}
}
리팩토링 39. 슈퍼클래스를 위임으로 바꾸기 (Replace Superclass with Delegate)
객체지향에서 “상속”은 기존의 기능을 재사용하는 쉬우면서 강력한 방법이지만 때로는 적절하지 않은 경우도 있다.
서브클래스는 슈퍼클래스의 모든 기능을 지원해야 한다.
Stack이라는 자료구조를 만들 때 List를 상속 받는것이 좋을까?
Stack이 List의 모든 기능을 지원하는 것이 옳을까? (index를 사용하는 기능)
Stack 내부적으로 List를 가지는 방식이 옳은게 아닐까?
서브클래스는 슈퍼클래스 자리를 대체하더라도 잘 동작해야 한다.
리스코프 치환 원칙
서브클래스는 슈퍼클래스의 변경에 취약하다.
그렇다면 상속을 사용하지 않는 것이 좋은가?
상속은 적절한 경우에 사용한다면 매우 쉽고 효율적인 방법이다.
따라서, 우선 상속을 적용한 이후에, 적절치 않다고 판단이 된다면 그때에 이 리팩토링을 적용하자.
예시 코드
Before
CategoryItem은 제품의 분류를 의미하고, Scroll은 Item이지 CategoryItem으로 두기에는 무언가 어색하다.
상속 구조가 아니라 위임으로 변경하면 된다.
public class Scroll extends CategoryItem {
private LocalDate dateLastCleaned;
public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
super(id, title, tags);
this.dateLastCleaned = dateLastCleaned;
}
public long daysSinceLastCleaning(LocalDate targetDate) {
return this.dateLastCleaned.until(targetDate, ChronoUnit.DAYS);
}
}
After
public class Scroll {
private LocalDate dateLastCleaned;
private CategoryItem categoryItem;
public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
this.dateLastCleaned = dateLastCleaned;
this.categoryItem = new CategoryItem(id, title, tags);
}
public long daysSinceLastCleaning(LocalDate targetDate) {
return this.dateLastCleaned.until(targetDate, ChronoUnit.DAYS);
}
}
리팩토링 40. 서브클래스를 위임으로 바꾸기 (Replace Subclass with Delegate)
어떤 객체의 행동이 카테고리에 따라 바뀐다면, 보통 상속을 사용해서 일반적인 로직은 슈퍼클래스에 두고 특이한 케이스에 해당하는 로직을 서브클래스를 사용해 표현한다.
하지만, 대부분의 프로그래밍 언어에서 상속은 오직 한번만 사용할 수 있다.
만약에 어떤 객체를 두가지 이상의 카테고리로 구분해야 한다면?
위임을 사용하면 얼마든지 여러가지 이유로 여러 다른 객체로 위임을 할 수 있다.
슈퍼클래스가 바뀌면 모든 서브클래스에 영향을 줄 수 있다. 따라서 슈퍼클래스를 변경할 때 서브클래스까지 신경써야 한다.
만약에 서브클래스가 전혀 다른 모듈에 있다면?
위임을 사용한다면 중간에 인터페이스를 만들어 의존성을 줄일 수 있다.
“상속 대신 위임을 선호하라.”는 결코 “상속은 나쁘다.”라는 말이 아니다.
처음엔 상속을 적용하고 언제든지 이런 리팩토링을 사용해 위임으로 전환할 수 있다.
예시 코드
Before
public class Booking {
protected Show show;
protected LocalDateTime time;
public Booking(Show show, LocalDateTime time) {
this.show = show;
this.time = time;
}
public boolean hasTalkback() {
return this.show.hasOwnProperty("talkback") && !this.isPeakDay();
}
protected boolean isPeakDay() {
DayOfWeek dayOfWeek = this.time.getDayOfWeek();
return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
}
public double basePrice() {
double result = this.show.getPrice();
if (this.isPeakDay()) result += Math.round(result * 0.15);
return result;
}
}
public class PremiumBooking extends Booking {
private PremiumExtra extra;
public PremiumBooking(Show show, LocalDateTime time, PremiumExtra extra) {
super(show, time);
this.extra = extra;
}
@Override
public boolean hasTalkback() {
return this.show.hasOwnProperty("talkback");
}
@Override
public double basePrice() {
return Math.round(super.basePrice() + this.extra.getPremiumFee());
}
public boolean hasDinner() {
return this.extra.hasOwnProperty("dinner") && !this.isPeakDay();
}
}
After 1
현재는 Booking이 생성될 시점에 static하게 일반 booking 인지, Premium booking 인지 정해야한다.
만약 일반 booking에서 premium booking으로 업그레이드할 수 있는 기능이 필요하다면?
여러 이유에 의해 더 이상 상속 관계를 유지하지 말아야하는 경우가 발생할 수 있다.
생성자가 아닌 팩토리 메소드를 사용한 이유
이름을 자율적으로 정해 의도를 드러내기가 쉽다.
반환 타입 또한 자유롭다.
테스트 코드를 수정하고 실행해서 올바르게 로직이 옮겨졌는지 확인할 수 있다.
public class PremiumDelegate {
private Booking host;
private PremiumExtra extra;
public PremiumDelegate(Booking host, PremiumExtra extra) {
this.host = host;
this.extra = extra;
}
}
public class Booking {
protected Show show;
protected LocalDateTime time;
protected PremiumDelegate premiumDelegate;
public Booking(Show show, LocalDateTime time) {
this.show = show;
this.time = time;
}
public static Booking createBook(Show show, LocalDateTime time) {
return new Booking(show, time);
}
public static Booking createPremiumBooking(Show show, LocalDateTime time, PremiumExtra extra) {
PremiumBooking booking = new PremiumBooking(show, time, extra);
booking.premiumDelegate = new PremiumDelegate(booking, extra);
return booking;
}
...
}
After 2
PremiumBooking에 존재하는 logic들을 매개자인 PremiumDelegate를 통해 Booking으로 옮겨줄 수 있다.
Delegate를 통해 Subclass를 안전하게 삭제할 수 있게 된다.
public class Booking {
protected Show show;
protected LocalDateTime time;
protected PremiumDelegate premiumDelegate;
public Booking(Show show, LocalDateTime time) {
this.show = show;
this.time = time;
}
public static Booking createBook(Show show, LocalDateTime time) {
return new Booking(show, time);
}
public static Booking createPremiumBooking(Show show, LocalDateTime time, PremiumExtra extra) {
PremiumBooking booking = new PremiumBooking(show, time, extra);
booking.premiumDelegate = new PremiumDelegate(booking, extra);
return booking;
}
public boolean hasTalkback() {
return (this.premiumDelegate != null) ? this.premiumDelegate.hasTalkback() :
this.show.hasOwnProperty("talkback") && !this.isPeakDay();
}
protected boolean isPeakDay() {
DayOfWeek dayOfWeek = this.time.getDayOfWeek();
return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
}
public double basePrice() {
double result = this.show.getPrice();
if (this.isPeakDay()) result += Math.round(result * 0.15);
return (this.premiumDelegate != null) ? this.premiumDelegate.extendBasePRice(result) : result;
}
public boolean hasDinner() {
return this.premiumDelegate != null && this.premiumDelegate.hasDinner();
}
}
public class PremiumDelegate {
private Booking host;
private PremiumExtra extra;
public PremiumDelegate(Booking host, PremiumExtra extra) {
this.host = host;
this.extra = extra;
}
public boolean hasTalkback() {
return this.host.show.hasOwnProperty("talkback");
}
public double extendBasePrice(double result) {
return Math.round(result + this.extra.getPremiumFee();
}
public boolean hasDinner() {
return this.extra.hasOwnProperty("dinner") && !this.host.isPeakDay();
}
}
냄새 18. 중재자 (Middle Man)
리팩토링 38. 중재자 제거하기 (Remove Middle Man)
예시 코드
Before
getManager()
After
어디까지 캡슐화를 할 것인가, 에 대한 생각이 중요한 부분이다.
리팩토링 39. 슈퍼클래스를 위임으로 바꾸기 (Replace Superclass with Delegate)
예시 코드
Before
CategoryItem은 제품의 분류를 의미하고, Scroll은 Item이지 CategoryItem으로 두기에는 무언가 어색하다. 상속 구조가 아니라 위임으로 변경하면 된다.
After
리팩토링 40. 서브클래스를 위임으로 바꾸기 (Replace Subclass with Delegate)
예시 코드
Before
After 1
After 2
PremiumBooking에 존재하는 logic들을 매개자인 PremiumDelegate를 통해 Booking으로 옮겨줄 수 있다. Delegate를 통해 Subclass를 안전하게 삭제할 수 있게 된다.