본문 바로가기

클린코드

[Refactoring] 18. 중재자

 

코딩으로 학습하는 리팩토링 - 인프런 | 강의

리팩토링은 소프트웨어 엔지니어가 갖춰야 할 기본적인 소양 중 하나입니다. 이 강의는 인텔리J와 자바를 사용하여 보다 실용적인 방법으로 다양한 코드의 냄새와 리팩토링 기술을 설명하고 직

www.inflearn.com

| 인프런 - 백기선님의 코딩으로 학습하는 리팩토링 강의를 수강하며 정리한 글입니다.


중재자
  • 캡슐화를 통해 내부의 구체적인 정보를 최대한 감출 수 있으나, 어떤 클래스의 메서드가 다른 클래스의 메소드 호출을 대부분 위임하고 있다면 중재자를 제거하거나 클라이언트가 해당 클래스를 직접 사용하도록 코드를 개선할 수 있다.
  • 캡슐화는 항상 정도가 있다. 과도한 캡슐화는 독이 된다.
  • 관련 리팩토링
    • 중재자 제거하기 -> 리팩토링을 사용해 클라이언트가 필요한 클래스를 직접 사용하도록 개선
    • 함수 인라인 -> 메서드를 호출한 쪽으로 코드를 옮겨 중재자 제거
    • 슈퍼 클래스를 위임으로 바꾸기
    • 서브 클래스를 위임으로 바꾸기

Refactoring 1. 중재자 제거하기

  • 위임 숨기기의 반대에 해당하는 리팩토링
  • 필요한 캡슐화의 정도는 시간에 따라 그리고 상황에 따라 바뀔 수 있다.
  • 위임하고 있는 객체를 클라이언트가 사용할 수 있도록 Getter 제공, 클라이언트는 메시지 체인을 사용하도록 코드를 고친다.
  • Law of Demeter를 지나치게 따르기 보다는 상황에 맞게 활용하는 것이 좋다.
    • Law of Demeter -> 가장 가까운 객체만 사용한다.
Before
public class Department {

    private Person manager;
    private String name;
    private int count;

    public Department(Person manager, String name, int count) {
        this.manager = manager;
        this.name = name;
        this.count = count;
    }

    public Person getManager() {
        return manager;
    }

    public String getName() {
        return name;
    }

    public int getCount() {
        return count;
    }
}
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();
    }

    public String getDepartmentName() {
        return this.department.getName();
    }

    public int getDepartmentCount() {
        return this.department.getCount();
    }
}

=> Department의 정보를 지나치게 Person을 거쳐서 가져온다.

After
public class Department {

    private Person manager;
    private String name;
    private int count;

    public Department(Person manager, String name, int count) {
        this.manager = manager;
        this.name = name;
        this.count = count;
    }

    public Person getManager() {
        return manager;
    }

    public String getName() {
        return name;
    }

    public int getCount() {
        return count;
    }
}

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;
    }
}

=> Department를 반환하고 해당 객체에서 접근하도록, Person이라는 중개자를 제거


Refactoring 2. 슈퍼 클래스를 위임으로 바꾸기

  • 객체 지향에서 상속은 기존의 기능을 재사용하기에 매우 쉽고 강력한 방법이지만 때로는 적절하지 않을 때가 존재한다.
    • 서브 클래스는 슈퍼 클래스의 모든 기능을 지원해야 한다.
    • 서브클래스는 슈퍼 클래스의 변경에 취약하다.
  • 상속이 적절하지 않다고 판단될 때 사용하는 리팩토링이다.
Before
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);
    }
}
After
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 {

    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);
    }
}

=> 불필요한 상속 관계를 위임으로 변경


Refactoring 3. 서브 클래스를 위임으로 바꾸기

  • 대부분의 프로그래밍 언어에서 상속은 오직 한 번만 사용할 수 있다.
    • A이면서 B가 될 수 있다.
    • 어떤 객체를 두 가지 이상의 카테고리로 분류해야 한다면? -> 위임을 사용하면 얼마든지 여러 다른 객체로 위임할 수 있다.
  • 슈퍼 클래스를 변경할 때 서브 클래스까지 신경써야 한다.
    • 만약 서브 클래스가 다른 모듈에 존재한다면? -> 위임을 사용한다면 중간에 인터페이스를 만들어 의존성을 줄일 수 있다.
  • 반드시 위임으로 바꿀 필요는 없다. -> 상황에 따라 적용
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();
    }
}

public class PremiumExtra {

    private List<String> properties;

    private double premiumFee;

    public PremiumExtra(List<String> properties, double premiumFee) {
        this.properties = properties;
        this.premiumFee = premiumFee;
    }

    public double getPremiumFee() {
        return premiumFee;
    }

    public boolean hasOwnProperty(String property) {
        return this.properties.contains(property);
    }
}

public class Show {

    private List<String> properties;

    private double price;

    public Show(List<String> properties, double price) {
        this.properties = properties;
        this.price = price;
    }

    public boolean hasOwnProperty(String property) {
        return this.properties.contains(property);
    }

    public double getPrice() {
        return price;
    }
}
After
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);
        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 ? premiumDelegate.extendPrice(result) : result;
    }

    public boolean hasDinner() {
        return 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 extendPrice(double result) {
        return Math.round(result + this.extra.getPremiumFee());
    }

    public boolean hasDinner() {
        return this.extra.hasOwnProperty("dinner") && !this.host.isPeakDay();
    }
}

public class PremiumExtra {

    private List<String> properties;

    private double premiumFee;

    public PremiumExtra(List<String> properties, double premiumFee) {
        this.properties = properties;
        this.premiumFee = premiumFee;
    }

    public double getPremiumFee() {
        return premiumFee;
    }

    public boolean hasOwnProperty(String property) {
        return this.properties.contains(property);
    }
}

public class Show {

    private List<String> properties;

    private double price;

    public Show(List<String> properties, double price) {
        this.properties = properties;
        this.price = price;
    }

    public boolean hasOwnProperty(String property) {
        return this.properties.contains(property);
    }

    public double getPrice() {
        return price;
    }
}

=> PremiumDelegate를 만들어 상속을 위임으로 푼다. -> Booking을 상속한 PremiumBooking 제거

'클린코드' 카테고리의 다른 글

[Refactoring] 20. 거대한 클래스  (0) 2022.09.24
[Refactoring] 19. 내부자 거래  (1) 2022.09.23
[Refactoring] 17. 메시지 체인  (0) 2022.09.21
[Refactoring] 16. 임시 필드  (1) 2022.09.20
[Refactoring] 15. 추측성 일반화  (0) 2022.09.18