이 글은 리팩터링 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 내부의 메소드들을 없애고
바로 접근자를 사용하도록 바꿀 수 있다.
하지만 그럴바에 그냥 전화번호를 값 객체로 만드는게 낫다.
'Programming > Refactoring(C++)' 카테고리의 다른 글
[Refactoring] 13. 타입코드를 서브클래스로 바꾸기 (For Large Class) (0) | 2022.10.27 |
---|---|
[Refactoring] 12. 슈퍼클래스 추출하기 (For Large Class) (0) | 2022.10.23 |
[Refactoring] 10. 반복문을 파이프라인으로 바꾸기 (For Loop Smell) (0) | 2022.10.20 |
[Refactoring] 9. 플래그 인수 제거하기 (For Long Parameter) (0) | 2022.10.20 |