해당 포스팅은 인프런 백기선님의 '리팩토링'을 학습 후 정리한 내용입니다.
• 캡슐화를 통해 내부의 구체적인 정보를 최대한 감출 수 있다.
• 그러나, 어떤 클래스의 메소드가 대부분 다른 클래스로 메소드 호출을 위임하고 있다면 중재자를 제거하고 클라이언트가 해당 클래스를 직접 사용하도록 코드를 개선할 수 있다.
• 관련 리팩토링
• “중재자 제거하기 (Remove Middle Man)” 리팩토링을 사용해 클라이언트가 필요한 클래스를 직접 사용하도록 개선할 수 있다.
• “함수 인라인 (Inlince Function)”을 사용해서 메소드 호출한 쪽으로 코드를 보내서 중재자를 없앨 수도 있다.
• “슈퍼클래스를 위임으로 바꾸기 (Replace Superclass with Delegate)”
• “서브클래스를 위임으로 바꾸기 (Replace Subclass with Delegate)
• “위임 숨기기”의 반대에 해당하는 리팩토링.
• 필요한 캡슐화의 정도는 시간에 따라 그리고 상황에 따라 바뀔 수 있다.
• 캡슐화의 정도를 “중재자 제거하기”와 “위임 숨기기” 리팩토링을 통해 조절할 수 있다.
• 위임하고 있는 객체를 클라이언트가 사용할 수 있도록 getter를 제공하고, 클라이언트는 메시지 체인을 사용하도록 코드를 고친 뒤에 캡슐화에 사용했던 메소드를 제거한다.
• Law of Demeter를 지나치게 따르기 보다는 상황에 맞게 활용하도록 하자.
• 디미터의 법칙, “가장 가까운 객체만 사용한다.”
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();
}
}
department 의 필드를 통해 객체 그래프 탐색을 할 수 가 없다.
객체 그래프 탐색을 할 수 있게 getter 를 만들어준다.
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 department;
}
}
• 객체지향에서 “상속”은 기존의 기능을 재사용하는 쉬우면서 강력한 방법이지만 때로는 적절하지 않은 경우도 있다.
• 서브클래스는 슈퍼클래스의 모든 기능을 지원해야 한다.
• Stack이라는 자료구조를 만들 때 List를 상속 받는것이 좋을까?
• 서브클래스는 슈퍼클래스 자리를 대체하더라도 잘 동작해야 한다.
• 리스코프 치환 원칙
• 서브클래스는 슈퍼클래스의 변경에 취약하다.
• 그렇다면 상속을 사용하지 않는 것이 좋은가?
• 상속은 적절한 경우에 사용한다면 매우 쉽고 효율적인 방법이다.
• 따라서, 우선 상속을 적용한 이후에, 적절치 않다고 판단이 된다면 그때에 이 리팩토링을 적용하자.
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 은 categoryItem 추상화에 맞지 않다. category 가 아닌 상품이다. 또한 상속 구조로서 강력하게 결합이 되어있다.
위임, 의존을 통해 해결하자.
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);
}
}
상속을 위임, 의존으로 해결하였다.
• 어떤 객체의 행동이 카테고리에 따라 바뀐다면, 보통 상속을 사용해서 일반적인 로직은 슈퍼클래스에 두고 특이한 케이스에 해당하는 로직을 서브클래스를 사용해 표현한다.
• 하지만, 대부분의 프로그래밍 언어에서 상속은 오직 한번만 사용할 수 있다.
• 만약에 어떤 객체를 두가지 이상의 카테고리로 구분해야 한다면?
• 위임을 사용하면 얼마든지 여러가지 이유로 여러 다른 객체로 위임을 할 수 있다.
• 슈퍼클래스가 바뀌면 모든 서브클래스에 영향을 줄 수 있다. 따라서 슈퍼클래스를 변경할 때 서브클래스까지 신경써야 한다.
• 만약에 서브클래스가 전혀 다른 모듈에 있다면?
• 위임을 사용한다면 중간에 인터페이스를 만들어 의존성을 줄일 수 있다.
• “상속 대신 위임을 선호하라.”는 결코 “상속은 나쁘다.”라는 말이 아니다.
• 처음엔 상속을 적용하고 언제든지 이런 리팩토링을 사용해 위임으로 전환할 수 있다.
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 으로 바꾸려면? 반대로 premiumBooking 을 booking 으로 바꾸려면? 상속 구조로서 강하게 결합이 되어 있어 힘들다.
서브 클래스를 위임으로 바꾸자.
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 = new Booking(show, time);
booking.premiumDelegate = new PremiumDelegate(booking, extra);
return booking;
}
public boolean hasTalkback() {
return (this.premiumDelegate != null)
? 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) ? premiumDelegate.hasDinner() : false;
}
}
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") && !host.isPeakDay();
}
}
정적 팩토리 메서드를 사용하여 일반 booking 의 경우 createBooking() 을 사용하요 premium 일 경우 createPremiumBooking() 을 사용하여 생성한다. 또한 booking 과 premiumBooking 의 로직은 다르기 때문에 이의 중재자인 PremiumDelegate 클래스를 따로 만들어 부가 로직을 수행한다. 이렇게 하여 booking 은 booking 클래스 내부에서 로직을 수행하며, premium 일 경우 premiumDelegate 클래스 에게 위임한다.