메시지 체인의 반대라고 생각하는 중재자라는 뜻을 가진다.
캡슐화를 통해 내부의 구체적인 정보를 최대한 감출 수 있다.
그러나 어떤 클래스의 메서드가 대부분 다른 클래스로 메서드 호출을 위임하고 있다면 중재자를 제거하고 클라이언트가 해당 클래스를 직접 사용하도록 코드를 개선할 수 있다.
도메인 객체를 사용할 때 해당 냄새가 난다면 클라이언트의 참조를 의심해 볼만하다.
관련 리팩토링으로는 다음과 같다.
Remove Middle Man으로 클라이언트가 필요한 클래스를 직접 사용하도록 개선할 수 있다.
Inline Function을 통해 메서드 호출한 쪽으로 코드를 보내 중재자를 없앨 수도 있다.
Replace Superclass with Delegate, Replace Subcalss With Delegate를 사용하여 위임으로 변경할 수도 있다.
Remove Middle Man
필요한 캡슐화의 정도는 시간에 따라 그리고 상황에 따라 바뀔 수 있다.
캡슐화의 정도를 중재자 제거하기와 위임 숨기기 리팩토링을 통해 조절할 수 있다.
위임하고 있는 객체를 클라이언트가 사용할 수 있도록 getter를 제공하고, 클라이언트는 메시지 체인을 사용하도록 코드를 고친 뒤에 캡슐화에 사용했던 메서드를 제거한다.
이는 캡슐화의 정도를 조절하는 리팩토링 중 하나이다. 이에 대한 답은 없다. 각자의 상황에 맞게 디미터의 법칙을 적용해야 한다.
//middle man
public Person getManager() {
return this.department.getManager();
}
//add delegate
public Department getDepartment() {
return this.department;
}
단순하게 기존 위임 숨기기를 통해 적용했던 것을 제거하고 클라이언트가 해당 객체에 직접 접근해 사용하도록 캡슐화를 느슨하게 만드는 것이다.
Replace Superclass with Delegate
상속을 위임으로 변경해보자.
객체지향에서 상속은 기존의 기능을 재사용하는 쉬위면서 강력한 방법이지만 때로는 적절하지 않은 경우도 있다.
이는 서브 클래스는 슈퍼클래스의 모든 기능을 지원해야 한다는 점.
이는 리스코프의 치환원칙에 따라 서브클래스는 슈퍼 클래스를 대체하더라도 문제가 없어야 함을 뜻한다.
또한 서브 클래스는 슈퍼클래스의 변경에 매우 취약하다. 그렇다고 우리는 상속을 지양해야 하는가?
그렇지 않다. 적절한 경우에 사용한다면 상속은 매우 쉽고 효율적인 방법이다.
따라서 우선 상속을 적용한 이후에, 적절치 않다고 판단이 된다면 그때 Replace Superclass with Delegate를 적용해도 늦지 않다.
public class CategoryItem {
private Integer id;
private String title;
private List<String> tags;
public CategoryItem(Integer id, String title, List<String> tags) {
this.id = id;
this.title = title;
this.tags = tags;
}
public Integer getId() {
return id;
}
public String getTitle() {
return title;
}
public boolean hasTag(String tag) {
return this.tags.contains(tag);
}
}
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);
}
}
Scroll은 특정아이템이고 특정 카테고리라고 생각할 수 없다고 생각하자.
간단하다 서브 클래스에서 슈퍼 클래스의 인스턴스를 만들어주면 된다.
private CategoryItem categoryItem;
public Scroll(Integer id, String title, List<String> tags, LocalDate dateLastCleaned) {
this.dateLastCleaned = dateLastCleaned;
this.categoryItem = new CategoryItem(id, title, tags);
}
Replace Subclass with Delegate
어떤 객체의 행동이 분리될 가능성이 존재한다면 보통 상속을 사용해 일반적인 로직은 슈퍼클래스에 두고 특이한 케이스에 해당하는 로직을 서브클래스를 사용해 표현한다.
만약 어떤 객체를 두 가지 이상의 슈퍼클래스를 상속받아야 한다면 이는 불가능할 수 있다. 대부분의 프로그래밍 언어는 상속은 오직 한 번만 사용할 수 있기 때문이다.
이를 위임을 사용하면 얼마든지 여러 가지 이유로 다른 객체로 위임을 할 수 있다.
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();
}
}
좋은 상속 구조의 예로 보인다. Replace Superclass with Delegate는 서브 클래스에서 슈퍼 클래스를 상속 구조에서 벗어나 참조하도록 변경했다면 이번에는 서브 클래스 자체를 위임으로 변경해야 한다.
따라서 위임할 수 있는 클래스가 하나 필요하다.
public class PremiumDelegate {
private Booking booking;
private PremiumExtra extra;
public PremiumDelegate(Booking booking, PremiumExtra extra) {
this.booking = booking;
this.extra = extra;
}
}
서브클래스가 없어짐에 따라 이를 대체할 클래스가 필요하기 때문이다. 이후 슈퍼 클래스에서는 이를 사용한 팩토리를 사용해 반환하는 데 이는 기본 생성자보다 조금 더 유연성을 가져가기 위함이다.
protected PremiumDelegate premiumDelegate;
public static Booking createBooking(Show show, LocalDateTime time) {
return new Booking(show, time);
}
public static Booking createPremiumBooking(Show show, LocalDateTime time, PremiumExtra premiumExtra) {
return new PremiumBooking(show, time, premiumExtra);
}
이제 우리가 만든 PremiumDelegate를 끼어 넣어 변환해 보자.
public static Booking createPremiumBooking(Show show, LocalDateTime time, PremiumExtra premiumExtra) {
PremiumBooking booking = new PremiumBooking(show, time, premiumExtra);
booking.premiumDelegate = new PremiumDelegate(booking, premiumExtra);
return booking;
}
이유는 다음과 같다. 리팩토링 하는 과정에서 우선 삭제될 서브클래스를 위임하는 객체를 통해 코드가 동작되게 한다.
왜냐하면 리팩토링에는 테스트코드를 통해 지속적으로 검증을 해야 하기 때문이다.
이후 마지막으로 실제 PremiumBooking이라는 클래스를 삭제할 것이다.
즉 코드를 옮기는데 중요한 역할을 하는 것이 PremiumDelegate이다.
메서드를 하나씩 옮기기 시작한다.
삭제될 예정인 서브 클래스의 메서드이다.
@Override
public boolean hasTalkback() {
return this.show.hasOwnProperty("talkback");
}
이를 Delegate 클래스로 옮긴다.
public boolean hasTalkback() {
return this.host.show.hasOwnProperty("talkback");
}
이후 삭제될 서브 클래스의 메서드를 delegate를 사용하도록 변경한 이후 테스트 코드를 실행시킨다.
@Override
public boolean hasTalkback() {
return this.premiumDelegate.hasTalkback();
}
이제 서브 클래스는 중재자가 되는 것이다.
이제 슈퍼클래스를 delegate 클래스를 사용하도록 변경하면 서브 클래스의 메서드 하나를 제거할 수 있다.
public boolean hasTalkback() {
return (this.premiumDelegate != null) ? this.premiumDelegate.hasTalkback() :
this.show.hasOwnProperty("talkback") && !this.isPeakDay();
}
오버라이드된 메서드를 지울 때는 위와 같이하면 되지만 서브 클래스에서만 하는 메서드는 난감하다.
이 경우 슈퍼 클래스에 해당 메서드를 pullup 하고 delegate가 존재하면 위임하도록 하고 그렇지 않은 경우 기본값을 반환하도록 혹은 바로 return 하도록 구성하긴 해야 한다.
최종 코드는 다음과 같다.
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 createBooking(Show show, LocalDateTime time) {
return new Booking(show, time);
}
public static Booking createPremiumBooking(Show show, LocalDateTime time, PremiumExtra premiumExtra) {
Booking booking = createBooking(show, time);
booking.premiumDelegate = new PremiumDelegate(booking, premiumExtra);
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 basePrice) {
return Math.round(basePrice + this.extra.getPremiumFee());
}
public boolean hasDinner() {
return this.extra.hasOwnProperty("dinner") && !host.isPeakDay();
}
}
재밌는 리팩토링이었다. 상속구조를 위임으로 변경하는 코드를 요즘은 쉽게 접할 수 없을지 모른다.
왜냐하면 대부분 위임을 통한 구조를 선호하고, 약간의 신념처럼 굳어졌기 때문인데, 이는 아마 수많은 요구사항의 변경에 대처하기 위한 몸부림이라고 볼 수 있다.
상속도 강력한 기능 중 하나로 불분명하게 위임을 사용해 리팩토링을 권장하지는 않는다. 실제로 이러한 요청이 있거나 미래에 예정되어 있다면 위와 같이 상속을 위임으로 변경하는 리팩토링을 진행해 놓는 것도 좋을 것이다.
'독서에서 한걸음' 카테고리의 다른 글
Large Class (0) | 2022.12.26 |
---|---|
Insider Trading (0) | 2022.12.26 |
Message Chains (0) | 2022.12.24 |
Temporary Field (0) | 2022.12.24 |
Speculative Generality (0) | 2022.12.24 |
댓글