본문 바로가기

리팩토링(마틴 파울러)

리팩토링 : 1장 조건문을 다형성으로 바꾸기

조건문을 다형성으로 바꾸기

Refactoring/src/main/java com.heejin.ex01_2

 

저번에는 htmlStatement와 같이 같은 기능을 하는 다른 메서드를 만들 때 기존 코드를 재사용할 수 있도록, 그리고 변경사항이 생기더라도 여러 메서드가 아니라 한 메서드만 고칠 수 있도록 statement 메서드의 여러 기능을 다양한 메서드로 분해했다.

 

이번에는 영화 분류법, 그리고 이에 따른 요금과 포인트 할당법을 쉽게 바꿀 수 있도록 리팩토링을 해볼 것이다.

 

Rental의 getCharge() 메서드를 Movie 클래스로 옮기기

 

Why??

  • 메서드가 priceCode라는 Movie클래스의 변수를 주로 사용하고 있기 때문
  • Movie의 종류가 앞으로의 주 변경사항이기 때문에 그 변화의 파장을 최소화하기 위해
package com.heejin.ex01;

public class Movie {
 
  double getCharge(int daysRented) {
    double result = 0;
    switch (this.getPriceCode()) {
      case REGULAR:
        result += 2;
        if (daysRented > 2) {
          result += (daysRented - 2) * 1.5;
        }
        break;
      case NEW_RELEASE:
        result += daysRented * 3;
        break;
      case CHILDREN:
        result += 1.5;
        if (daysRented > 3) {
          result += (daysRented - 3) * 1.5;
        }
        break;
    }
    return result;
  }
}

Rental 클래스에서도 기존의 getCharge 메서드에서 Movie의 getCharge 메서드를 호출한다.

  double getCharge() {
    return this._movie.getCharge(this._daysRented);
  }

포인트계산 메서드도 같은 방식으로 Movie 클래스로 옮겨준다.

package com.heejin.ex01;

public class Movie {
  
  int getFrequentRenterPoints(int daysRented) {
 // 최신을 이틀 이상 대여하는 경우 추가 포인트 제공
    if ((this.getPriceCode() == NEW_RELEASE) &&
        (daysRented > 1)) {
      return 2;
    }
    return 1;
  
  }
}

Rental 클래스의 포인트계산 메서드도 다음과 같이 바꿔준다.

int getFrequentRenterPoints() {
    return this._movie.getFrequentRenterPoints(this._daysRented);
  }

 

영화의 서브 클래스 만들기

만약 Children Movie, New Release Movie, Regular Movie 이렇게 클래스를 만들어, Movie 클래스를 상속 받게 한다면?

각각의 서브 클래스들이 getCharge() 와 getFrequentRenterPoint() 메서드를 갖고, 영화 종류에 따라 다른 요금과 포인트를 계산할 수 있게 된다. 그러나 이 방식에는 치명적인 약점이 존재한다.

영화 객체가 만들어지면 객체가 사라지기 전까지는 영화의 종류를 바꿀 수 없다는 것이다.

 

이를 해결하기 위해서 스테이트 패턴(State Pattern, Gang of Four)을 사용해야한다.  

 

* 스테이트 패턴(State Pattern)이란?

객체가 상태에 따라 행위를 달리 해야하는 상황에서, 각각 상황과 조건에 따른 조건문 코드를 작성하는 것의 문제점(재사용 불가, 늘어나는 코드 길이 등등)을 피하기 위해 고안된 패턴이다. 각각의 상태를 객체화시키기 위해 상태 인터페이스를 만들고 이를 구현하는 각각의 하위 클래스들을 만든다. 따라서 클라이언트에서 인터페이스를 호출하면 그에 따른 하위 클래스가 호출되는 방식을 취한다.

https://victorydntmd.tistory.com/294 

 

[디자인패턴] 스테이트 패턴 ( State Pattern )

스테이트 패턴 ( State Pattern ) 스테이트 패턴은 객체가 특정 상태에 따라 행위를 달리하는 상황에서, 자신이 직접 상태를 체크하여 상태에 따라 행위를 호출하지 않고, 상태를 객체화 하여 상태�

victorydntmd.tistory.com

State Pattern을 사용한 후의 UML 다이어그램

 

스테이트 패턴을 사용하면 Price의 상태를 표현하는 추상클래스가 만들어지고 그 밑에 이를 구현하는 요금의 상태들이 만들어진다. 그리고 Movie에서 상태 객체를 생성하고 Price의 getCharge() 메서드를 사용하면 그 하위 객체에 맞는 메서드가 실행된다. Movie가 직접 상태를 표현하는 것이 아니라 Movie 객체가 Price 상태 객체를 생성하여 사용하기 때문에 언제든지 수정하고 싶다면 다른 객체를 생성해서 사용하면 될 것이다.

 

이 스테이트 패턴을 도입하기 위해 세개의 리팩토링 과정을 거친다. 

  •  Replace Type Code with State/Strategy  - Price 클래스들을 생성하여 Movie가 이 객체를 필드로 갖게 한다.
  •  Move Method  - Movie 클래스에 있는 getCharge() 메서드를 Price 클래스로 옮겨준다.
  •  Replace Conditional with Polymorphism  - getCharge()를 조건 별로 분해해서 그에 맞는 서브클래스의 메서드로 오버라이딩한다.

 Replace Type Code with State/Strategy 

일단 모든 타입 코드가 get/set 메서드에 의해 사용되도록 한다. (Self Encapsulate Field) 대부분의 코드가 다른 클래스에서 온 것이므로 이미 대부분의 메서드는 get 메서드를 사용중이었지만, Movie 생성자는 가격 코드를 직접 사용하고 있다. 이를 set메서드로 바꿔준다.

 public Movie(String title, int priceCode) {
    _title = title;
    setPriceCode(priceCode);
  }

그리고 Price 라는 클래스를 추가해준다. 모든 Price의 하위 클래스들은 상태를 가리키는 객체가 되어, getPriceCode() 메서드를 통해서 가격 코드를 리턴하는 역할을 한다.

package com.heejin.ex01;

abstract class Price {
  abstract int getPriceCode();
}

class ChildrenPrice extends Price {
  @Override
  int getPriceCode() {
    return Movie.CHILDREN;
  }
}

class NewReleasePrice extends Price {
  @Override
  int getPriceCode() {
    return Movie.NEW_RELEASE;
  }
}

class RegularPrice extends Price {
  @Override
  int getPriceCode() {
    return Movie.REGULAR;
  }
}

한편, Movie 클래스에서는 

  • int _priceCode라는 필드를 Price _price로 바꿔준다.
  • getPriceCode() 메서드의 블록을 price 객체를 통해 각 클래스의 getPriceCode() 메서드를 호출하는 코드로 바꿔준다. 
  • setPriceCode() 메서드에서는 입력받는 int값, 즉 가격 코드의 값에 따라 그에 맞는 Price 객체를 생성하는 코드로 바꿔준다.
package com.heejin.ex01;

public class Movie {
  public static final int CHILDREN = 2;
  public static final int REGULAR = 0;
  public static final int NEW_RELEASE = 1;
  
  private String _title;
  private Price _price;
  
  public Movie(String title, int priceCode) {
    _title = title;
    setPriceCode(priceCode);
  }
  
  public int getPriceCode() {
    return _price.getPriceCode();
  }
  
  public void setPriceCode(int arg) {
    switch(arg) {
      case REGULAR:
        _price = new RegularPrice();
        break;
      case CHILDREN:
        _price = new RegularPrice();
        break;
      case NEW_RELEASE:
        _price = new NewReleasePrice();
        break;
      default:
        throw new IllegalArgumentException("Incorrect Price Code");
    }
  }
  
  public String getTitle() {
    return _title;
  }
  
  double getCharge(int daysRented) {
    double result = 0;
    switch (this.getPriceCode()) {
      case REGULAR:
        result += 2;
        if (daysRented > 2) {
          result += (daysRented - 2) * 1.5;
        }
        break;
      case NEW_RELEASE:
        result += daysRented * 3;
        break;
      case CHILDREN:
        result += 1.5;
        if (daysRented > 3) {
          result += (daysRented - 3) * 1.5;
        }
        break;
    }
    return result;
  }
  
  int getFrequentRenterPoints(int daysRented) {
 // 최신을 이틀 이상 대여하는 경우 추가 포인트 제공
    if ((this.getPriceCode() == NEW_RELEASE) &&
        (daysRented > 1)) {
      return 2;
    }
    return 1;
  
  }
}

 Move Method 

이제 getCharge() 메서드를 Movie 클래스가 아닌 Price 클래스로 옮겨준다.

package com.heejin.ex01;

abstract class Price {
  abstract int getPriceCode();
  
  double getCharge(int daysRented) {
    double result = 0;
    switch (this.getPriceCode()) {
      case Movie.REGULAR:
        result += 2;
        if (daysRented > 2) {
          result += (daysRented - 2) * 1.5;
        }
        break;
      case Movie.NEW_RELEASE:
        result += daysRented * 3;
        break;
      case Movie.CHILDREN:
        result += 1.5;
        if (daysRented > 3) {
          result += (daysRented - 3) * 1.5;
        }
        break;
    }
    return result;
  }
}

그리고 Movie 클래스에서 getCharge() 를 호출하면 Price 객체에서 호출되도록 코드를 변경해준다. 

  public double getCharge(int daysRented) {
    return _price.getCharge(daysRented);
  }

 Replace Conditional with Polymorphism 

이제 Price 추상 클래스에 있는 getCharge() 를 각 조건 별로 나눠서 각 하위 클래스의 오버라이딩된 getCharge() 로 옮겨주면 된다.

package com.heejin.ex01;

abstract class Price {
  abstract int getPriceCode();
  
  double getCharge(int daysRented) {
    double result = 0;
    switch (this.getPriceCode()) {
      case Movie.REGULAR:
        result += 2;
        if (daysRented > 2) {
          result += (daysRented - 2) * 1.5;
        }
        break;
      case Movie.NEW_RELEASE:
        result += daysRented * 3;
        break;
      case Movie.CHILDREN:
        result += 1.5;
        if (daysRented > 3) {
          result += (daysRented - 3) * 1.5;
        }
        break;
    }
    return result;
  }
}

class ChildrenPrice extends Price {
  @Override
  int getPriceCode() {
    return Movie.CHILDREN;
  }
  
  @Override
  double getCharge(int daysRented) {
    double result = 1.5;
    if (daysRented > 3) {
      result += (daysRented - 3) * 1.5;
    }
    return result;
  }
}

class NewReleasePrice extends Price {
  @Override
  int getPriceCode() {
    return Movie.NEW_RELEASE;
  }
  
  @Override
  double getCharge(int daysRented) {
    return daysRented * 3;
  }
}

class RegularPrice extends Price {
  @Override
  int getPriceCode() {
    return Movie.REGULAR;
  }
  
  @Override
  double getCharge(int daysRented) {
    double result = 2;
    if (daysRented > 2) {
      result += (daysRented - 2) * 1.5;
    }
    return result;
  }
}

이것으로 스테이트 패턴의 구현이 끝났다. 

이제 같은 방법으로 getFrequentRenterPoints()도 스테이트 패턴을 구현해보자.

  •  Replace Type Code with State/Strategy  - Price 클래스들은 이미 구현되었으니 이 단계는 넘어가자.
  •  Move Method  - Movie 클래스에 있는 getFrequentRenterPoint()를 Price 추상 클래스로 옮긴다.
  •  Replace Conditional with Polymorphism  - 해당 메서드를 조건 별로 분해해서 그에 맞는 서브클래스의 메서드로 오버라이딩한다.

 Move Method 

Movie 클래스에 있는 getFrequentRenterPoint()를 Price 추상 클래스로 옮긴다.

package com.heejin.ex01;

abstract class Price {
  abstract int getPriceCode();
  
  double getCharge(int daysRented) {
    double result = 0;
    switch (this.getPriceCode()) {
      case Movie.REGULAR:
        result += 2;
        if (daysRented > 2) {
          result += (daysRented - 2) * 1.5;
        }
        break;
      case Movie.NEW_RELEASE:
        result += daysRented * 3;
        break;
      case Movie.CHILDREN:
        result += 1.5;
        if (daysRented > 3) {
          result += (daysRented - 3) * 1.5;
        }
        break;
    }
    return result;
  }
  
  int getFrequentRenterPoints(int daysRented) {
    if ((this.getPriceCode() == Movie.NEW_RELEASE) &&
        (daysRented > 1)) {
      return 2;
    }
    return 1;
  }
}

그리고 Movie 클래스에서 getFrequentRenterPoints()를 호출하면 Price 객체에서 메서드가 호출되도록 변경한다.

  int getFrequentRenterPoints(int daysRented) {
    return _price.getFrequentRenterPoints(daysRented);
  }

 Replace Conditional with Polymorphism 

이제 Price 추상 클래스에 있는 getFrequentRenterPoints() 를 각 조건 별로 나눠서 각 하위 클래스의 오버라이딩된 메서드로 옮겨주면 되는데, NewReleasPrice 클래스의 조건에서만 특별한 계산 적용되는 것이니, 그 클래스에서만 오버라이딩해줘도 괜찮다.

package com.heejin.ex01;

abstract class Price {
  abstract int getPriceCode();
  
  double getCharge(int daysRented) {
    double result = 0;
    switch (this.getPriceCode()) {
      case Movie.REGULAR:
        result += 2;
        if (daysRented > 2) {
          result += (daysRented - 2) * 1.5;
        }
        break;
      case Movie.NEW_RELEASE:
        result += daysRented * 3;
        break;
      case Movie.CHILDREN:
        result += 1.5;
        if (daysRented > 3) {
          result += (daysRented - 3) * 1.5;
        }
        break;
    }
    return result;
  }
  // NewReleasePrice 객체에서 호출한 것이 아니라면
  // 해당 메서드가 호출되어 무조건 1이 리턴될 것이다.
  int getFrequentRenterPoints(int daysRented) {
    return 1;
  }
}

class ChildrenPrice extends Price {
  @Override
  int getPriceCode() {
    return Movie.CHILDREN;
  }
  
  @Override
  double getCharge(int daysRented) {
    double result = 1.5;
    if (daysRented > 3) {
      result += (daysRented - 3) * 1.5;
    }
    return result;
  }
}

class NewReleasePrice extends Price {
  @Override
  int getPriceCode() {
    return Movie.NEW_RELEASE;
  }
  
  @Override
  double getCharge(int daysRented) {
    return daysRented * 3;
  }
  
  @Override
  int getFrequentRenterPoints(int daysRented) {
    return (daysRented > 1) ? 2 : 1;
}

class RegularPrice extends Price {
  @Override
  int getPriceCode() {
    return Movie.REGULAR;
  }
  
  @Override
  double getCharge(int daysRented) {
    double result = 2;
    if (daysRented > 2) {
      result += (daysRented - 2) * 1.5;
    }
    return result;
  }
}

이렇게 getFrequentRenterPoints() 메서드의 처리도 끝이 났다.

 

이렇게 바뀐 클래스들의 관계는 다음과 같다.

 


 결론 

이런 방식으로 조건문을 객체들에 따라 다른 메서드를 호출하도록 바꿔놓으면 굳이 기존에 있던 getCharge() 메서드나getFrequentRenterPoints()의 코드를 바꾸지 않고, Price의 하위 객체를 추가하는 방식으로 새로운 영화 종류를 쉽게 추가할  수 있다. 요금이나 포인트 계산 방법을 바꿀 때에도 Price 클래스의 각 하위 클래스가 갖는 메서드를 수정하기만 하면 된다. 한편, Rental 클래스나 Customer 클래스는 우리가 요금과 포인트 산정 기능을 만드는 데에 스테이트 패턴을 사용했는 줄도 모를 것이다.