이 글은 리팩터링 2판(마틴 파울러) 책을 참고하였습니다.

순서

I. 배경
II. 절차
III. 예시
IV. 기타


I. 배경

사실 이 클래스 관련 내용부터가 제일 중요하지 않을까...

클래스는 반드시 명확하게 추상화하고 소수의 주어진 역할만 처리해야 한다.

하지만 실무에서는 점점 비대해지며 복잡해진다.

이 클래스를 적절히 잘 나누어야 하는데,
1. 일부 데이터와 메서드를 따로 묶을 수 있을 때
2. 일부 데이터나 메서드를 제거해도 타 부분에 논리적 영향 없을 때
3. 추가적인 서브클래스 생성의 방식이 달라졌을 때

 


II. 절차

1. 클래스의 역할을 분리할 방법을 정한다.

2. 분리될 역할을 담당할 클래스를 새로 만든다.

3. 원래 클래스의 생성자에서
새로운 클래스의 인스턴스를 생성하여 필드에 저장.

4. 필드들을 새 클래스로 옮긴다.
하나씩 옮길 때 마다 테스트한다 (점진적 리팩토링)

5. 함수들도 옮긴다. 특히, 호출을 당하는 일이 많은 함수(저수준) 위주.

6. 양 클래스의 인터페이스 살펴보며 불필요한 메서드 제거.

7. 새 클래스를 외부로 노출할지 정한다.
노출할거면 새 클래스에 참조를 값으로 바꿀지도 고민..

 


III. 예시


코드 원본

뭔가 사람클래스와, Office클래스를 나누고 싶다.
Person클래스 안에 Office를 선언해서 변수로 가지고 있으면?

#include <string>
#include "gtest/gtest.h"

class Person {
 public:
  std::string name_;
  std::string office_area_code_;
  std::string office_number_;

  std::string GetName() { return name_; }
  void SetName(std::string name) { name_ = name; }
  std::string GetOfficeAreaCode() { return office_area_code_; }
  void SetOfficeAreaCode(std::string arg) { office_area_code_ = arg; }
  std::string GetOfficeNumber() { return office_number_; }
  void SetOfficeNumber(std::string arg) { office_number_ = arg; }
  std::string GetTelephoneNumber() { return office_area_code_ + ' ' + office_number_; }
};

TEST(ExtractClass, GetName) {
  Person person;
  person.SetName("Martin Fowler");
  ASSERT_EQ("Martin Fowler", person.GetName());
}

TEST(ExtractClass, GetOfficeAreaCode) {
  Person person;
  person.SetOfficeAreaCode("07796");
  ASSERT_EQ("07796", person.GetOfficeAreaCode());
}

TEST(ExtractClass, GetOfficeNumber) {
  Person person;
  person.SetOfficeNumber("+82-2-2033-7217");
  ASSERT_EQ("+82-2-2033-7217", person.GetOfficeNumber());
}

TEST(ExtractClass, GetTelephoneNumber) {
  Person person;
  person.SetOfficeAreaCode("07796");
  person.SetOfficeNumber("02-2033-7217");
  ASSERT_EQ("07796 02-2033-7217", person.GetTelephoneNumber());
}

 


1. 새로운 클래스(COffice) 만들기

테스트 코드 구현 생략

#include <string>
#include "gtest/gtest.h"

/* 1. 새로운 클래스 생성 */
class COffice {
public:
};

class Person {
public:
  COffice m_classOffice;  /* 1. new instance */
  
  std::string name_;
  std::string office_area_code_;
  std::string office_number_;

  std::string GetName() { return name_; }
  void SetName(std::string name) { name_ = name; }
  std::string GetOfficeAreaCode() { return office_area_code_; }
  void SetOfficeAreaCode(std::string arg) { office_area_code_ = arg; }
  std::string GetOfficeNumber() { return office_number_; }
  void SetOfficeNumber(std::string arg) { office_number_ = arg; }
  std::string GetTelephoneNumber() { return office_area_code_ + ' ' + office_number_; }
};

 


2. 역할에 필요한 필드, 함수를 새 클래스로 옮기기

테스트 코드 구현 생략

#include <string>
#include "gtest/gtest.h"

/* 2. Person class에서 옮기기 */
class COffice {
public:
  std::string getAreaCode() { return office_area_code_; }
  void setAreaCode(std::string arg) { office_area_code_ = arg; }
  
  std::string getNumber() { return office_number_; }
  void setNumber(std::string arg) { office_number_ = arg; }
  
  std::string getTelephoneNumber() { return office_area_code_ + ' ' + office_number_; }

private:
  std::string office_area_code_;
  std::string office_number_;
};

class Person {
public:
  COffice m_classOffice;  /* 1. new instance */
  std::string name_;
  
  /* 2. 내부 함수 구현 바꾸기 */
  std::string GetName() { return name_; }
  void SetName(std::string name) { name_ = name; }
  
  std::string GetOfficeAreaCode() { m_classOffice.getAreaCode(); }
  void SetOfficeAreaCode(std::string arg) { m_classOffice.setAreaCode(arg); }
  std::string GetOfficeNumber() { return m_classOffice.getNumber(); }
  void SetOfficeNumber(std::string arg) { m_classOffice.setNumber(arg); }
  std::string GetTelephoneNumber() { return m_classOffice.getTelephoneNumber(); }
};

 


3. Office를 Renaming하기 (Telephone)
순수한 전화번호를 뜻하는 클래스라 Office가 필요없음.

테스트 코드 구현 생략

#include <string>
#include "gtest/gtest.h"

/* 3. 클래스 및 변수 이름들 변경 */
class CTelephoneNumber {
public:
  std::string getAreaCode() { return area_code_; }
  void setAreaCode(std::string arg) { area_code_ = arg; }
  
  std::string getNumber() { return number_; }
  void setNumber(std::string arg) { number_ = arg; }
  
  /* 3. 읽기 좋은 포맷으로 출력하기 위한 method */
  std::string toString() { return area_code_ + ' ' + number_; }

private:
  std::string area_code_;
  std::string number_;
};

class Person {
public:
  COffice m_classTelephoneNumber;  /* 1. new instance */
  std::string name_;
  
  /* 3. 클래스 변수 변경 */
  std::string GetName() { return name_; }
  void SetName(std::string name) { name_ = name; }
  
  std::string GetOfficeAreaCode() { m_classTelephoneNumber.getAreaCode(); }
  void SetOfficeAreaCode(std::string arg) { m_classTelephoneNumber.setAreaCode(arg); }
  std::string GetOfficeNumber() { return m_classTelephoneNumber.getNumber(); }
  void SetOfficeNumber(std::string arg) { m_classTelephoneNumber.setNumber(arg); }
  std::string GetTelephoneNumber() { return m_classTelephoneNumber.toString(); }
};

IV. 기타

전화번호는 여러모로 쓸데가 많으니 CTelephoneNumber class는
클라이언트에게 공개하는 것이 좋겠다는 판단.

구현이 끝나고 CTelephoneNumber 내부의 메소드들을 없애고
바로 접근자를 사용하도록 바꿀 수 있다.

하지만 그럴바에 그냥 전화번호를 값 객체로 만드는게 낫다.

+ Recent posts