이 글은 리팩터링 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를 진행 해야하고,
새로운 함수를 만들어 기존 코드를 대체 할 때는 기존 코드를 항상 남겨두었다가
생성된 함수의 테스트가 모두 완료된 후 최종적으로 교체하도록 하자. 

+ Recent posts