리팩토링 40. 서브클래스를 위임으로 바꾸기

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다.


    리팩토링 40. 서브클래스를 위임으로 바꾸기 (Replace Subclass With Delegate)

    • 어떤 객체의 행동이 카테고리에 따라 바뀐다면, 보통 상속을 사용해서 일반적인 로직은 슈퍼클래스에 두고 특이한 케이스에 해당하는 로직을 서브클래스를 사용해 표현한다.
    • 하지만 대부분의 프로그래밍 언어에서 상속은 오직 한번만 사용할 수 있다.
      • 만약 어떤 객체를 두 가지 이상의 카테고리로 구분해야 한다면?
      • 위임을 사용하면 얼마든지 여러가지 이유로 여러 다른 객체로 위임을 할 수 있다.
    • 슈퍼클래스가 바뀌면 모든 서브클래스에 영향을 줄 수 있다.
      • 따라서 슈퍼클래스를 변경할 때 서브 클래스까지 신경써야한다.
      • 만약 서브클래스가 전혀 다른 모듈에 있다면?  위임을 사용한다면 중간에 인터페이스를 만들어 의존성을 줄일 수 있다.
    • '상속 대신 위임을 선호하라'는 결코 '상속은 나쁘다'라는 말이 아니다
      • 처음엔 상속을 적용하고 언제든지 이런 리팩토링을 사용해 위임으로 전환할 수 있다. 

     

    새들이 있고, 새들의 종류에 따라 깃털/소리/비행 거리가 다른 경우라면 상속 구조로 나눌 수 있다. 이처럼 여러가지 카테고리가 있는 경우에 상속 구조로 나눌 수 있다. 또한 일반적인 로직이 있고 특정한 케이스에만 조금 다르게 동작하는 경우도 상속 구조를 이용해볼 수 있다. 이 경우, 특수한 경우가 서브 클래스가 되고 공통적인 부분은 슈퍼 클래스가 된다. 

    그런데 상속은 단 한번만 사용이 가능하다는 한계가 있다. 두 개 이상의 상속을 써야하는 경우가 불가능한데, 이 때 위임을 사용하면 얼마든지 여러 분류로 위임을 할 수 있다. 예를 들어 새의 서식지로 분류, 새의 생물학적 특징으로 분류를 하겠다고 하면 두 개의 상속으로 처리할 수가 없다. 그런데 이런 것들을 필드값으로 선언하면 해당 부분을 만족시킬 수 있게 된다. 예를 들어 새 클래스가 있고, 필드로 서식지 / 생물학적 특징을 추가한다. 그리고 새 클래스의 각 메서드는 서식지 / 생물학적 특징 인스턴스를 통해 위임하도록 수정하는 것이다. 

    상속을 사용하면 강하게 결합하기 때문에 느슨한 결합을 만들기 위해서 '상속 대신 위임'이라는 말이 많이 나온다. 그런데 그렇다고 '상속 자체가 나쁜 것'은 아니다. 일단은 처음에 봤을 때, 상속이 유용할 것 같으면 상속을 먼저 사용하고, 나중에 상속 자체가 걸림돌이 되는 것 같다면 리팩토링을 해서 상속을 위임으로 변환하는 것이다. 


    코드

    아래 코드에 Booking / PremiumBooking 클래스가 존재한다. 일반적인 로직을 가진 Booking 클래스가 있고, 특수한 로직만 가지고 있는 PremiumBooking 클래스가 있기 때문에 적절히 상속을 사용한 예시로 볼 수 있다. 그런데 나중에 더욱 다양한 상속 구조를 쓴다거나, 아니면 다른 상속 구조를 쓰는게 더 좋다는 이유로 Booking에 대한 상속 구조를 포기해야 할 수도 있다. 이 때는 PremiumBooking - Booking의 상속 구조를 위임으로 변경할 수 있다. 

    여기서는 상속이 아니라 위임을 통해서 PremiumBooking을 구현하도록 리팩토링 한다.

     

    Step1. PremiumDeligation 역할을 할 클래스 생성

    PremiumBooking 클래스를 제거할 것이다. PremiumBooking 클래스가 하던 역할을 Booking에서 해야할텐데, 이 때 Delegation을 통해서 처리할 것이다. 따라서 위임할 PremiumDelegate 클래스를 하나 생성한다. 

    PremiumDelegate는 host, premiumExtra라는 필드를 가진다. 이 필드는 PremiumBooking 클래스에서 필요로 하던 필드다. 

    public class PremiumDelegate {
    
        private Booking host;
        private PremiumExtra premiumExtra;
    
        public PremiumDelegate(Booking host, PremiumExtra premiumExtra) {
            this.host = host;
            this.premiumExtra = premiumExtra;
        }
    }

     

     

    Step2. Booking 클래스에 필드 추가 및 팩토리 메서드 생성

    Booking 클래스는 PremiumBooking 클래스를 없애는 대신 PremiumDelegate를 이용해서 위임할 것이다. 따라서 PremiumDelegate 필드를 Booking 클래스에 추가한다. 

    이제 Booking 클래스는 PremiumDelegate의 유무에 따라서 PremiumBooking / Booking 두 가지로 동작할 수 있다. 따라서 분리해서 생성하기 위해 다음 팩토리 메서드를 추가한다. 우선 아직은 PremiumBooking 클래스가 삭제 되지 않았기 때문에 일시적으로 PremiumBooking 클래스를 반환하도록 한다. 

    public static Booking createBooking(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.delegate = new PremiumDelegate(booking, extra);
        return booking;
    }

    PremiumBooking 클래스를 만들 때, delegate 필드에 PremiumDelgate를 생성해서 끼워넣는다. 이 장치를 통해서 추후 PremiumBooking 클래스가 제거되었을 때, Booking 클래스에서 PremiumBooking 인스턴스를 Booking 인스턴스로 손쉽게 바꿔 끼울 수 있게 된다. 

     

    Step3. hasTallBack() 메서드 이동

    PremiumDelegate 클래스로 hasTallBack() 메서드를 이동시킨다. 그리고 Booking 클래스는 Delegate 인스턴스를 통해 hasTallBack() 메서드를 위임하도록 수정하면 된다.

    public class PremiumDelegate {
    
    	...
    
        // 메서드 이동
        public boolean hasTalkback() {
            return this.host.show.hasOwnProperty("talkback");
        }
        
    }
    
    public class Booking {
    
    	...
    
    	// 필요하다면 위임을 사용하도록 수정.
        public boolean hasTalkback() {
            return (this.delegate != null) ? this.delegate.hasTalkback() :
                    this.show.hasOwnProperty("talkback") && !this.isPeakDay();
        }

    이제 테스트 코드에서는 일반 생성자가 아니라, 팩토리 메서드를 사용해서 생성하도록 테스트 코드를 수정한다 . PremiumBooking에서는 hasTalkBack()을 중재자로 만든다. 이 녀석은 this.premiumDelegate.hasTalkback()을 호출하도록 한다. 이것은 PremiumBooking에 있던 실제 로직이 deligate로 옮겨진 것을 의미한다. 여기서 테스트 코드를 돌려서 확인한다.

     

    Step4. BasePrice() 메서드의 이동

    PremiumBooking에 있는 basePrice()도 넘어가야한다. 아래와 메서드를 넘기고, Booking 클래스에서 위임해서 사용하도록 수정한다. 

    public class PremiumDelegate {
    
    	...
    
        public double extendBasePrice(double result) {
            return Math.round(result + this.extra.getPremiumFee());
        }
    }
    
    
    public class Booking {
    
    	...
    
        public double basePrice() {
            double result = this.show.getPrice();
            if (this.isPeakDay()) result += Math.round(result * 0.15);
            
            // delegation
            return (this.delegate != null) ? this.delegate.extendBasePrice(result) : result;
        }
    
    }

     

    Step5. hasDinner() 메서드 옮기기

    hasDinner() 메서드는 PremiumBooking에만 있는 메서드다. Booking에서는 인터페이스를 지원하지 않기 때문에 해당 메서드는 Booking, PremiumDelegate에서 모두 지원되어야 한다. 다음과 같이 메서드를 옮긴 후, 위임할 수 있다.

    public class PremiumDelegate {
    
    	...
        // 메서드 이동
        public boolean hasDinner() {
            return this.extra.hasOwnProperty("dinner") && !host.isPeakDay();
        }
    }
    
    
    public class Booking {
    
    	...
    	// Delegation 있으면 쓰고, 없으면 항상 false
        public boolean hasDinner() {
            return (this.delegate != null) ? this.delegate.hasDinner() : false;
        }
    }

     

    Step6. PremiumBooking 제거하기

    이제 PremiumBooking에 있는 기능을 모두 PremiumDelegate로 옮겼다. 따라서 PremiumBooking 클래스를 삭제하고, 팩토리 메서드에서도 일반 Booking을 지원하도록 수정한다. 생성자 팩토리 메서드의 장점은 이렇게 상속 구조에서 타입이 변경되어도 큰 영향을 미치지 않는다는 점이다. 

    public static Booking createPremiumBooking(Show show, LocalDateTime time, PremiumExtra extra) {
        Booking booking = new Booking(show, time);
        booking.delegate = new PremiumDelegate(booking, extra);
        return booking;
    }

    'etc > 리팩토링' 카테고리의 다른 글

    냄새 20. 거대한 클래스  (0) 2023.05.10
    냄새 19. 내부자 거래  (0) 2023.05.10
    리팩토링 39. 슈퍼클래스를 위임으로 바꾸기  (0) 2023.05.10
    리팩토링 38. 중재자 제거하기  (0) 2023.05.10
    냄새 18. 중재자  (0) 2023.05.10

    댓글

    Designed by JB FACTORY