관리 메뉴

너와 나의 스토리

[리팩토링] Extract Class 클래스 추출하기 본문

개발/Refactoring

[리팩토링] Extract Class 클래스 추출하기

노는게제일좋아! 2021. 11. 1. 22:26
반응형
클래스는 반드시 명확하게 추상화하고 소수의 주어진 역할만 처리해야 한다

* 추상화: 공통의 속성이나 기능을 묶는 것(클래스 정의)

 

배경

메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않으니 잘 살펴보고 적절하게 분리하는 것이 좋다.

  • 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다.
  • 함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다.
  • 특정 데이터나 메서드 일부를 제거해도 다른 필드나 메서드들이 논리적으로 문제가 없다면 분리할 수 있다는 뜻이다.

 

절차

  1. 클래스의 역할을 분리할 방법을 정한다.
  2. 분리될 역할을 담당할 클래스를 새로 만든다
    • 원래 클래스에 남은 역할과 클래스 이름이 어울리지 않는다면 적절히 바꾼다.
  3. 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 필드에 저장해둔다.
  4. 분리될 역할에 필요한 필드들을 새 클래스로 옮긴다. 하나씩 옮길 때마다 테스트한다.
  5. 메서드들도 새 클래스로 옮긴다. 이때 저수준 메서드부터 옮긴다.
    • * 저수준 메서드: 다른 메서드를 호출하기보다는 호출을 당하는 일이 많은 메서드
    • 하나씩 옮길 때마다 테스트한다.
  6. 양쪽 클래스의 인터페이스를 살펴보면서 불필요한 메서드를 제거하고, 이름도 새로운 환경에 맞게 바꾼다.
  7. 새 클래스를 외부로 노출할지 정한다. 노출하려거든 새 클래스에 참조를 값으로 바꾸기를 적용할지 고민한다.

 

 

예시

class Person {
    get name() { return this.AbortController.apply.call.bind._name; }
    set name(arg) { this._name = arg; }
    get telephoneNumber() { return `(${this.officeAreaCode}) ${this.officeNumber}`; }
    get officeAreaCode() { return this._officeAreaCode; }
    set officeAreaCode(arg) { this._officeAreaCode = arg; }
    get officeNumber() { return this._officeNumber; }
    set officeNumber(arg) { this._officeNumber = arg; }
}
  • Step 1: 전화번호 관련 동작을 별도 클래스로 뽑기
  • Step 2: 전화번호를 표현하는 TelphoneNumber 클래스를 정의
    • class TelephoneNumber {
      }
  • Step 3: Person 클래스의 인스턴스를 생성할 때 전화번호 인스턴스도 함께 생성해 저장해둔다.
    • class Person {
          constructor() {
              this._telephoneNUmber = new TelephoneNumber();
          }
          // ...
      }
  • Step 4: 필드를 하나식 새 클래스로 옮긴다.
    • class Person {
          constructor() {
              this._telephoneNumber = new TelephoneNumber();
          }
          get name() { return this.AbortController.apply.call.bind._name; }
          get name(arg) { this._name = arg; }
          get telephoneNumber() { return `(${this.officeAreaCode}) ${this.officeNumber}`; }
          
          get officeAreaCode() { return this._telephoneNumber.officeAreaCode; }  <-
          get officeAreaCode(arg) { this._telephoneNumber.officeAreaCode = arg; }  <-
      
          get officeNumber() { return this._officeNumber; }
          get officeNumber(arg) { this._officeNumber = arg; }
      }
      
      class TelephoneNumber {
          get officeAreaCode() { return this._officeAreaCode; }  <-
          get officeAreaCode(arg) { this._officeAreaCode = arg; }  <-
      }
    • 테스트해서 문제 없으면 다음 필드로 넘어간다.
    • class Person {
          // ...
          get officeNumber() { return this._telephoneNumber.officeNumber; }  
          get officeNumber(arg) { this._telephoneNumber.officeNumber = arg; }  
      }
      
      class TelephoneNumber {
          // ...
          get officeNumber() { return this._officeNumber; }  
          get officeNumber(arg) { this._officeNumber = arg; }  
      }
    • 다시 테스트..
    • 이렇게 하나씩 옮기고 + 테스트 반복해줍니다.
    • class Person {
          // ...
          get officeAreaCode() { return this._telephoneNumber.officeAreaCode; }
          get officeAreaCode(arg) { this._telephoneNumber.officeAreaCode = arg; }
      
          get officeNumber() { return this._telephoneNumber.officeNumber; }
          get officeNumber(arg) { this._telephoneNumber.officeNumber = arg; }
      }
      
      class TelephoneNumber {
          // ...
          get officeAreaCode() { return this._officeAreaCode; }
          get officeAreaCode(arg) { this._officeAreaCode = arg; }
      
          get officeNumber() { return this._officeNumber; }  
          get officeNumber(arg) { this._officeNumber = arg; }  
      }
  • Step 5: 메서드를 새 클래스로 옮긴다.
    • class Person {
          // ...
          get telephoneNumber() { return `(${this._telephoneNumber.officeAreaCode}) ${this._telephoneNumber.officeNumber}`; }
      }
      
      class TelephoneNumber {
          // ...
          get telephoneNumber() { return `(${this.officeAreaCode}) ${this.officeNumber}`; }
      }
  • Step 6: 정리
    • 새로운 환경에 맞게 이름을 변경해보자.
    • Before
      • class TelephoneNumber {
            get telephoneNumber() { return `(${this.officeAreaCode}) ${this.officeNumber}`; }
        
            get officeAreaCode() { return this._officeAreaCode; }
            get officeAreaCode(arg) { this._officeAreaCode = arg; }
        
            get officeNumber() { return this._officeNumber; }
            get officeNumber(arg) { this._officeNumber = arg; }
        }
    • TelephoneNumber  클래스는 순수한 전화번호를 뜻하므로 사무실(office)이란 단어를 쓸 이유가 없다.
      • // Before
        get officeAreaCode() { return this._officeAreaCode; }
        get officeAreaCode(arg) { this._officeAreaCode = arg; }
        
        get officeNumber() { return this._officeNumber; }
        get officeNumber(arg) { this._officeNumber = arg; }
            
            
        // After
        get areaCode() { return this._areaCode; }
        get areaCode(arg) { this._areaCode = arg; }
        
        get number() { return this._number; }
        get number(arg) { this._number = arg; }
         
    • 마찬가지로 전화번호라는 뜻도 메서드 이름에서 다시 강조할 이유가 없다.
      • // Before
        get telephoneNumber() { return `(${this.officeAreaCode}) ${this.officeNumber}`; }
        
        // After
        get toString() { return `(${this.areaCode}) ${this.number}`; }
  • Step 7: 전화번호 클래스를 외부로 노출할지 결정
    • 전화번호 클래스는 쓸모가 많으니 클라이언트에 공개하는 것이 좋겠다.
    • 그러면 office로 시작하는 메서드를 없애고 TelephoneNumber의 접근자를 바로 사용하도록 바꿀 수 있다.
class Person {
    constructor() {
        this._telephoneNUmber = new TelephoneNumber();
    }
    get name() { return this._name; }
    get name(arg) { this._name = arg; }
    
    get telephoneNumber() { return this._telephoneNumber.toString() }
}

class TelephoneNumber 
    get toString() { return `(${this.areaCode}) ${this.number}`; }

    get areaCode() { return this._officeAreaCode; }
    get areaCode(arg) { this._officeAreaCode = arg; }

    get officeNumber() { return this._officeNumber; }
    get officeNumber(arg) { this._officeNumber = arg; }
}

 

 

 

* 고수준 모듈이란 단일 기능을 제공하는 모듈이고, 저수준 모듈이란 이 고수준 모듈을 구성하는 모듈들입니다.

 

 

 

 

출처:

- [리팩터링 2판]

반응형
Comments