이 글은 리팩터링 2판(마틴 파울러) 책을 참고하였습니다.
순서
I. 배경
II. 절차
III. 예시
IV. 기타
I.배경
함수를 그 함수만을 위한 객체 안으로 캡슐화 하는 작업이다.
이 때 이 객체는 "명령(Command) 객체"로 불리우며 캡술화된 메서드 하나로 구성된다.
이는 Design Pattern 중 "Command Pattern"에 해당된다.
이 객체는 평범한 함수 메커니즘보다 훨씬 유연하게 제어하고 표현할 수 있는데,
일급-함수(First-class Function)의 대부분을 흉내낼 수 있다.
일급-함수? (First-Class Function)
- 함수를 다른 함수에 인수로 전달
- 다른 함수의 값으로 반환
- 변수에 할당하거나 데이터 구조에 저장
<Example>
std::accumulate(vec.begin(), vec.end(), 1, [](int a, int b) { return a*b;});
II. 절차
1. 대상 함수의 기능을 옮길 빈 클래스를 만든다.
클래스 이름은 함수 이름에 기초한다.
2. 해당 빈 클래스로 함수를 옮긴다.
명령 관련 이름은 프로그래밍 언어의 명명규칙을 따른다.
규칙이 딱히 없다면 "Execute()", "Call()"
3.함수의 인수들 각각은 명령의 필드로 만들어 생성자를 통해 설정할지 고민한다.
III. 예시
코드 원본
#include <cmath>
#include "gtest/gtest.h"
class Candidate {
public:
int origin_state_;
};
class MedicalExam {
public:
bool smoker_;
};
class ScoringGuide {
public:
bool StateWithLowCertification(int state) {
return state <= 3 ? true : false;
}
};
enum class CertificationGrade {
kRegular,
kLow,
};
int Score(Candidate* candidate, MedicalExam* medical_exam, ScoringGuide* scoring_guide) {
int result = 0;
int health_level = 0;
bool high_medical_risk_flag = false;
if (medical_exam->smoker_) {
health_level += 10;
high_medical_risk_flag = true;
}
CertificationGrade certification_grade = CertificationGrade::kRegular;
if (scoring_guide->StateWithLowCertification(candidate->origin_state_)) {
certification_grade = CertificationGrade::kLow;
result -= 5;
}
// lots more code like this
result -= std::max(health_level - 5, 0);
return result;
}
TEST(ReplaceFunctionWithCommand, HighCertificationSmoker) {
Candidate candidate = {4};
MedicalExam medical_exam = {true};
ScoringGuide scoring_guide;
ASSERT_EQ(-5, Score(&candidate, &medical_exam, &scoring_guide));
}
TEST(ReplaceFunctionWithCommand, HighCertificationNonSmoker) {
Candidate candidate = {4};
MedicalExam medical_exam = {false};
ScoringGuide scoring_guide;
ASSERT_EQ(0, Score(&candidate, &medical_exam, &scoring_guide));
}
TEST(ReplaceFunctionWithCommand, LowCertificationSmoker) {
Candidate candidate = {3};
MedicalExam medical_exam = {true};
ScoringGuide scoring_guide;
ASSERT_EQ(-10, Score(&candidate, &medical_exam, &scoring_guide));
}
TEST(ReplaceFunctionWithCommand, LowCertificationNonSmoker) {
Candidate candidate = {3};
MedicalExam medical_exam = {false};
ScoringGuide scoring_guide;
ASSERT_EQ(-5, Score(&candidate, &medical_exam, &scoring_guide));
}
1. Score함수를 위한 빈 클래스 및 생성자 만들기 (CScore)
// 기타 Class 구현 및 TEST생략
#include <cmath>
#include "gtest/gtest.h"
class Candidate;
class MedicalExam;
class ScoringGuide;
enum class CertificationGrade;
class CScore { // 1. 새로운 빈 클래스 생성
public:
CScore(Candidate* candidate, MedicalExam* medical_exam, ScoringGuide* scoring_guide)
: m_pCandidate(candidate),
m_pMedicalExam(medical_exam),
m_pScoringGuide(scoring_guide){}; // 1. 클래스 생성자로 인자를 받아준다.
private: // 1. 클래스 내부에 기타 Class 멤버변수를 선언
Candidate m_pCandidate;
MedicalExam m_pMedicalExam;
ScoringGuide m_pScoringGuide;
};
// 이 Score함수는 수행함수로 보면 된다. 이 부분을 구현하면 되는 상황.
int Score(Candidate* candidate, MedicalExam* medical_exam, ScoringGuide* scoring_guide) {
int result = 0;
int health_level = 0;
bool high_medical_risk_flag = false;
if (medical_exam->smoker_) {
health_level += 10;
high_medical_risk_flag = true;
}
CertificationGrade certification_grade = CertificationGrade::kRegular;
if (scoring_guide->StateWithLowCertification(candidate->origin_state_)) {
certification_grade = CertificationGrade::kLow;
result -= 5;
}
// lots more code like this
result -= std::max(health_level - 5, 0);
return result;
}
2. 실행부 (Execute()) 함수 만들기
// 기타 Class 구현 및 TEST생략
#include <cmath>
#include "gtest/gtest.h"
class Candidate;
class MedicalExam;
class ScoringGuide;
enum class CertificationGrade;
class CScore { // 1. 새로운 빈 클래스 생성
private: // 1. 클래스 내부에 기타 Class 멤버변수를 선언
Candidate m_pCandidate;
MedicalExam m_pMedicalExam;
ScoringGuide m_pScoringGuide;
public:
CScore(Candidate* candidate, MedicalExam* medical_exam, ScoringGuide* scoring_guide)
: m_pCandidate(candidate),
m_pMedicalExam(medical_exam),
m_pScoringGuide(scoring_guide){}; // 1. 클래스 생성자로 인자를 받아준다.
int Execute() { // 2. 기존 함수의 구현부를 그대로 가져온다.
int result = 0;
int health_level = 0;
bool high_medical_risk_flag = false;
if (medical_exam->smoker_) {
health_level += 10;
high_medical_risk_flag = true;
}
CertificationGrade certification_grade = CertificationGrade::kRegular;
if (scoring_guide->StateWithLowCertification(candidate->origin_state_)) {
certification_grade = CertificationGrade::kLow;
result -= 5;
}
result -= std::max(health_level - 5, 0);
return result;
}
};
// 이 Score함수는 수행함수로 보면 된다. 이 부분을 구현하면 되는 상황.
int Score(Candidate* candidate, MedicalExam* medical_exam, ScoringGuide* scoring_guide) {
// 2. CScore함수 구현 후 수행부분은 아래와 같이 변경된다.
CScore scorer(candidate, medical_exam, scoring_guide); // 2. 생성자 호출
return scorer.Execute(); // 2. 수행함수
}
3. Execute를 다시 리팩토링하기 (변수 멤버화, 조건문 쪼개기)
// 기타 Class 구현 및 TEST생략
#include <cmath>
#include "gtest/gtest.h"
class Candidate;
class MedicalExam;
class ScoringGuide;
enum class CertificationGrade;
class CScore { // 1. 새로운 빈 클래스 생성
private: // 1. 클래스 내부에 기타 Class 멤버변수를 선언
Candidate m_pCandidate;
MedicalExam m_pMedicalExam;
ScoringGuide m_pScoringGuide;
int result; // 3. Execute() 내부 변수 멤버화
int health_level;
bool high_medical_risk_flag;
scoreSmoking() { // 3. 멤버함수로 Smoking 조건문 분리
if (medical_exam->smoker_) {
health_level += 10;
high_medical_risk_flag = true;
}
}
public:
CScore(Candidate* candidate, MedicalExam* medical_exam, ScoringGuide* scoring_guide)
: m_pCandidate(candidate),
m_pMedicalExam(medical_exam),
m_pScoringGuide(scoring_guide){}; // 1. 클래스 생성자로 인자를 받아준다.
int Execute() { // 2. 기존 함수의 구현부를 그대로 가져온다.
result = 0; // 3. 멤버변수 초기화
health_level = 0;
high_medical_risk_flag = false;
scoreSmoking(); // 3. 멤버함수로 Smoking 조건문 분리
// 이 부분도 멤버함수로 쪼갤 수 있음.
CertificationGrade certification_grade = CertificationGrade::kRegular;
if (scoring_guide->StateWithLowCertification(candidate->origin_state_)) {
certification_grade = CertificationGrade::kLow;
result -= 5;
}
result -= std::max(health_level - 5, 0);
return result;
}
};
// 이 Score함수는 수행함수로 보면 된다. 이 부분을 구현하면 되는 상황.
int Score(Candidate* candidate, MedicalExam* medical_exam, ScoringGuide* scoring_guide) {
// 2. CScore함수 구현 후 수행부분은 아래와 같이 변경된다.
CScore scorer(candidate, medical_exam, scoring_guide); // 2. 생성자 호출
return scorer.Execute(); // 2. 수행함수
}
IV. 기타
중요한 것은 한 번에 리팩토링 하는 것이 아니다.
점진적 리팩토링을 진행해야 하는데, 예시에서는 한 번에 변경된 모습을 보여준다.
한 가지 변경이 진행될 때 마다 빌드를 해보고 Test를 진행 해야하고,
새로운 함수를 만들어 기존 코드를 대체 할 때는 기존 코드를 항상 남겨두었다가
생성된 함수의 테스트가 모두 완료된 후 최종적으로 교체하도록 하자.
'Programming > Refactoring(C++)' 카테고리의 다른 글
[Refactoring] 5. 반복문 쪼개기 (For Long Function Smell) (0) | 2022.10.17 |
---|---|
[Refactoring] 4. 조건문 분해하기 (For Long Function Smell) (0) | 2022.10.17 |
[Refactoring] 2. 임시변수를 질의함수로 바꾸기 (For Long Function Smell) (1) | 2022.10.16 |
[Refactoring] 1. 함수 추출하기 (For Long Function Smell) (0) | 2022.10.13 |