728x90
반응형
중재자(Meddle Man)
- 어떤 클래스의 메서드가 대부분 다른 클래스로 메서드 호출을 위임하고 있다면, 중재자를 제거하고 클라이언트가 해당 클래스를 직접 사용하도록 코드 개선 가능 (= 불필요한 추상화/캡슐화 제거)
- 이전에 본 "메시지 체인"의 반대에 해당하는 것
- 관련 리팩토링
- "중재자 제거하기(Remove Middel Man)" -> 클라이언트가 필요한 클래슬르 직접 사용하도록 개선 가능
- "함수 인라인(Inline Function)" -> 메서드 호출한 쪽으로 코드를 보내서 중재자를 없앨 수 있음
- "슈퍼클래스를 위임으로 바꾸기(Replace Superclass with Delegate)"
- "서브클래스를 위임으로 바꾸기(Replace Subclass with Delegate)"
중재자 제거하기(Remove Middel Man)
- "위임 숨기기"의 반대에 해당하는 리팩토링
- 캡슐화의 정도를 "중재자 제거하기"와 "위임 숨기기" 리팩토링을 통해 조절할 수 있음
- => 캡술화 정도에 정답은 없음
- 위임하고 있는 객체를 클라이언트가 사용할 수 있도록 getter를 제공하고, 클라이언트는 메시지 체인을 사용하도록 코드를 고친 뒤, 캡슐화에 사용했던 메서드를 제거하는 순서로 진행
예제 코드
public class Department {
private Person manager;
public Department(Person manager) {
this.manager = manager;
}
public Person getManager() {
return 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();
}
}
위 예제는 이전 "위임 숨기기"에서 다뤘던 예제와 동일하다. 정확히 반대의 과정을 거쳐서 "중재자 제거하기" 리팩토링을 수행할 수 있다. 이를 위해 getManager()를 통해 캡슐화 했던 것을 제거해주자.
public class Person {
private Department department;
private String name;
public Person(String name, Department department) {
this.name = name;
this.department = department;
}
public Person getDepartment() {
return this.department;
}
}
슈퍼클래스를 위임으로 바꾸기(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은 별도의 Item이지 카테고리 아이템이라고 보기는 어려울 것 같다. 상속 구조를 제거하고 합성으로 변경하는 것이 좋아보인다.
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);
}
}
서브클래스를 위임으로 바꾸기(Replace Subclass with Delegate)
- 어떤 객체의 행동이 카테고리에 따라 바뀐다면, 보통 상속을 사용해서 일반적인 로직은 슈퍼클래스에 두고 특이한 케이스에 해당하는 로직을 서브클래스를 사용해 표현한다
- 하지만, 대부분의 프로그래밍 언어에서 상속은 오직 한번만 사용할 수 있다..
- => 만약 어떤 객체를 2가지 이상의 카테고리로 구분해야 한다면?
- => 위임을 사용하면 얼마든지 여러가지 이유롤 여러 다른 객체로 위임을 할 수 있다
- 슈퍼클래스가 바뀌면 모든 서브 클래스에 영향을 줄 수 있다. 때문에, 슈퍼클래스를 변경할 때 서브클래스까지 신경써야 한다.
- => 만약 서브클래스가 전혀 다른 모듈에 있다면?
- => 위임을 사용한다면 중간에 '인터페이스'를 만들어 의존성을 줄일 수 있다
- "상속 대신 위임을 선호하라"는 결고 "상속은 나쁘다"라는 말이 아니다
- 처음엔 상속을 적용하고 언제든지 이런 리팩토링을 사용해 위임으로 전환할 수 있다
예제 코드
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();
}
}
일반적인 Booking과 PremiumBooking이 있고, 둘은 peakDay일 때 talkback 가능 여부와 가격도 다르다. 이는 상속을 적절히 사용한 구조라고 볼 수 있다. 하지만, 요구사항 추가 등으로 인해 나중에 상속 구조가 변경될 수도 있으며, Booking에서 PremiumBooking으로 업그레이드 하는 요구사항이 생기면 인스턴스를 갈아끼우는 과정이 번거로워진다.
여기에 위임을 적용해서 설계를 변경해보자.
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();
}
}
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 extra) {
Booking booking = createBooking(show, time);
// premiumDelegate는 PremiumBooking을 만들 때에만 생성
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();
}
}
PremiumDelegate라는 위임을 추가하고, Booking에서는 팩토리 메서드를 통해 Booking을 생성하도록 변경하였다. 그 결과, PremiumBooking이라는 서브클래스를 제거하고 상속 구조를 위임으로 변경할 수 있었다.
내부자 거래(Insider Trading)
- 어떤 모듈이 다른 모듈의 내부 정보를 지나치게 많이 알고 있는 코드 냄새를 의미
- => 그로 인해 지나치게 강한 결합도(coupling)가 생길 수 있음
- 강한 결합도를 줄이기 위해 책임을 분배하거나 이동시켜줄 필요가 있음
- 관련 리팩토링
- "함수 옮기기(Move Function)" & "필드 옮기기(Move Field)"
- "위임 숨기기(Hide Delegate)" -> 특정 모듈의 중재자를 두어 결합도를 줄일 수 있음
- 혹은, 굳이 중재자를 두지 않더라도 여러 모듈이 자주 사용하는 공통적인 기능은 새로운 모듈을 만들어 관리할 수 있음
- "슈퍼클래스 또는 서브클래슬르 위임으로 바꾸기" -> 상속으로 인한 결합도를 줄일 때 사용 가능
728x90
반응형
'ETC' 카테고리의 다른 글
[Refactoring] 리팩토링 요약 (0) | 2025.01.06 |
---|---|
[Refactoring] 20 ~ 24. 거대한 클래스 / 서로 다른 인터페이스의 대안 클래스들 / 데이터 클래스 / 상속 포기 / 주석 (0) | 2025.01.05 |
[Refactoring] 15 ~ 17. 추측성 일반화 / 임시 필드 / 메시지 체인 (2) | 2025.01.04 |
[Refactoring] 12 ~ 14. 반복되는 switch 문 / 반복문 / 성의없는 요소 (0) | 2025.01.04 |
[Refactoring] 11. 기본형 집착 (0) | 2025.01.04 |