이 글은 리팩터링 2판(마틴 파울러) 책을 참고하였습니다.
순서
I. 배경
II. 절차
III. 예시
IV. 기타
I. 배경
타입코드란 뭘까?
알고리즘을 개발하다보면 각 단계별로, 혹은
각 타입에 맞는 알고리즘을 수행해야 할 때가 있다.
즉, 열거형, 심볼, 문자열, 숫자 등으로 구분된 역할을 나누어 수행하는 것이다.
이 자체만으로 불편한 것은 딱히 없지만, 그 이상 동작이 더 많아진다면
무언가가 더 필요할 때가 있다. 여기서 무언가는 "서브클래스"이다.
서브클래스가 필요한 이유는
1. 조건에 따라 다르게 동작하도록 "다형성"을 제공하며
2. 특정 타입에서만 의미 있는 값을 사용하는 필드 or 메소드가 있을 때 발현된다.
각 구현 방법은
1. 대상 클래스에 직접 상속하는 팩토리 패턴을 사용한다.
2. 기존 클래스에 간접 상속하여 "속성"을 부여한다.
II. 절차
1. 타입 코드 필드를 자가 캡슐화한다.(함수형태로)
2. 타입 코드 값 하나를 선택하여 그 값에 해당하는 서브클래스를 만든다.
타입 코드 Getter를 오버라이드 하여 타입 코드를 리터럴 값 반환하게 한다.
-> 그냥 타입 코드를 반환해주는 Getter를 가상함수로 만든다는 뜻이다.
3. 매개변수로 받은 타입 코드와, 위에서 만든 서브클래스를 매핑하는 로직만든다.
-> 직접 상속시 : 생성자를 팩토리 패턴으로 적용
-> 간접 상속시 : 선택 로직 자체를 생성자에 두기
4. 테스트한다.
5. 타입 코드 각각에 대해 적용해준다. -> 기존 필드 제거 -> 다시 테스트
6. 타입 코드 접근자를 이용하는 모든 메소드에 메소드 내리기와
조건부 로직을 다형성으로 바꾸기를 적용한다.
-> 조건부 로직을 다형성으로 바꾸기 : 다음 글
III. 예시
<직접 상속할 때>
코드 원본
#include <iostream>
#include <string>
#include "gtest/gtest.h"
class Employee {
public:
std::string name_;
std::string type_;
Employee(std::string name, std::string type) : name_(name), type_(type) {
ValidateType(type);
}
/* 타입코드 선택이 지저분하다 */
void ValidateType(std::string arg) {
if (arg != "engineer" && arg != "manager" && arg != "salesperson") {
std::cout << "Employee cannot be of type " << arg << std::endl;
}
}
std::string ToString() {
return name_ + " (" + type_ + ")";
}
}; // Class Employee End
/* 실제 동작하는 구현부 */
TEST(ReplaceTypeCodeWithSubclasses, ToString) {
Employee employee("Martin Fowler", "engineer");
ASSERT_EQ("Martin Fowler (engineer)", employee.ToString());
}
1. 타입 코드 Getter 오버라이드하기
#include <iostream>
#include <string>
#include "gtest/gtest.h"
class Employee {
public:
std::string name_;
std::string type_;
Employee(std::string name, std::string type) : name_(name), type_(type) {
ValidateType(type);
}
/* 타입코드 선택이 지저분하다 */
void ValidateType(std::string arg) {
if (arg != "engineer" && arg != "manager" && arg != "salesperson") {
std::cout << "Employee cannot be of type " << arg << std::endl;
}
}
/* 1. Change to using getType() */
std::string ToString() {
return name_ + " (" + getType() + ")";
}
/* 1. New Method getType() */
std::string getType() {
return type_;
}
}; // Class Employee End
/* 실제 동작하는 구현부 */
TEST(ReplaceTypeCodeWithSubclasses, ToString) {
Employee employee("Martin Fowler", "engineer");
ASSERT_EQ("Martin Fowler (engineer)", employee.ToString());
}
2. 이를 기준으로 각각의 타입들을 가진 서브 클래스들을 만들고 싶다.
Engineer / Salesperson / Manager를 만들어보자.
#include <iostream>
#include <string>
#include "gtest/gtest.h"
class Employee {
public:
std::string name_;
std::string type_;
Employee(std::string name, std::string type) : name_(name), type_(type) {
ValidateType(type);
}
/* 타입코드 선택이 지저분하다 */
void ValidateType(std::string arg) {
if (arg != "engineer" && arg != "manager" && arg != "salesperson") {
std::cout << "Employee cannot be of type " << arg << std::endl;
}
}
/* 1. Change to using getType() */
std::string ToString() {
return name_ + " (" + getType() + ")";
}
/* 2. Make Virtual Method */
virtual std::string getType() {
return getType();
}
}; // Class Employee End
/* 2. new Classes inherited by Employee */
class Engineer : public Employee {
public:
using Employee::Employee;
virtual std::string getType() {
return "Engineer";
}
};
class Salesperson : public Employee {
public:
using Employee::Employee;
virtual std::string getType() {
return "Slaesperson";
}
};
class Manager : public Employee {
public:
using Employee::Employee;
virtual std::string getType() {
return "Manager";
}
};
using Employee::Employee
-> C++11 부터 위와 같이 선언해주면 부모의 생성자를 사용할 수 있다.
뭐.. 자식 생성자에 들어오는 인자를 부모로 넘기고... 안해도 된다
3. 내가 원하는 타입 코드를 가진 인스턴스를 생성할 수 있도록
"createEmployee" 함수를 만들자.
#include <iostream>
#include <string>
#include "gtest/gtest.h"
class Employee {
public:
std::string name_;
Employee(std::string name) : name_(name){}
}
/* 3. ValidateType 과 type 변수 필요없음 */
/* 1. Change to using getType() */
std::string ToString() {
return name_ + " (" + getType() + ")";
}
/* 2. Make Virtual Method */
virtual std::string getType() {
return getType();
}
}; // Class Employee End
/* 2. new Classes inherited by Employee */
class Engineer : public Employee {
public:
using Employee::Employee;
virtual std::string getType() {
return "Engineer";
}
};
class Salesperson : public Employee {
public:
using Employee::Employee;
virtual std::string getType() {
return "Slaesperson";
}
};
class Manager : public Employee {
public:
using Employee::Employee;
virtual std::string getType() {
return "Manager";
}
};
/* 3. Create Employee */
Employee* createEmployee(std::string type) {
if (type == "engineer") {
return new Engineer(name);
}
if (type == "manager") {
return new Manager(name);
}
if (type == "salesperson") {
return new SalesPerson(name);
}
std::cout << "Employee cannot be of type " << type << std::endl;
return nullptr;
}
그럼, 실제 구현 코드에서는 createEmployee("type")으로 인스턴스를 반환받는다.
<간접 상속할때>
이미 서브클래스로 Salesperson과 Manager 클래스가 있다면 어떨까?
코드 원본
#include <algorithm>
#include <iostream>
#include <string>
#include "gtest/gtest.h"
class Employee {
public:
std::string name_;
std::string type_;
Employee(std::string name, std::string type) : name_(name), type_(type) {
ValidateType(type);
}
void ValidateType(std::string arg) {
if (arg != "engineer" && arg != "manager" && arg != "salesperson") {
std::cout << "Employee cannot be of type " << arg << std::endl;
}
}
std::string GetType() {
return type_;
}
void SetType(std::string arg) {
type_ = arg;
}
std::string CapitalizedType() {
std::string type = type_;
std::transform(type.begin(), type.begin() + 1, type.begin(), toupper);
return type;
}
std::string ToString() {
return name_ + " (" + CapitalizedType() + ")";
}
};
TEST(ReplaceTypeCodeWithSubclasses, CapitalizedType) {
Employee employee("Martin Fowler", "engineer");
ASSERT_EQ("Engineer", employee.CapitalizedType());
}
TEST(ReplaceTypeCodeWithSubclasses, ToString) {
Employee employee("Martin Fowler", "engineer");
ASSERT_EQ("Martin Fowler (Engineer)", employee.ToString());
}
1. EmployeeType 이라는 클래스를 새로 만들고 이를 상속해준다.
#include <algorithm>
#include <iostream>
#include <string>
#include "gtest/gtest.h"
/* 1. 새로운 타입만 주는 클래스와 신규 클래스 2개 */
/* 예시라서 Engineer와 Manager 두개만 써놓음 */
class CEmployeeType {
virtual std::string to_string() = 0;
};
class Engineer : public CEmployeeType {
virtual std::string to_string() {
return "Engineer";
}
};
class Manager : public CEmployeeType {
virtual std::string to_string() {
return "Manger";
}
};
class Employee {
public:
std::string name_;
std::string type_;
Employee(std::string name, std::string type) : name_(name), type_(type) {
ValidateType(type);
}
void ValidateType(std::string arg) {
if (arg != "engineer" && arg != "manager" && arg != "salesperson") {
std::cout << "Employee cannot be of type " << arg << std::endl;
}
}
std::string GetType() {
return type_;
}
void SetType(std::string arg) {
type_ = arg;
}
std::string CapitalizedType() {
std::string type = type_;
std::transform(type.begin(), type.begin() + 1, type.begin(), toupper);
return type;
}
std::string ToString() {
return name_ + " (" + CapitalizedType() + ")";
}
};
2. 원래 있던 Employee의 setType()을 신규 클래스들과 매핑해준다.
#include <algorithm>
#include <iostream>
#include <string>
#include "gtest/gtest.h"
/* 1. 새로운 타입만 주는 클래스와 신규 클래스 2개 */
/* 예시라서 Engineer와 Manager 두개만 써놓음 */
class CEmployeeType {
virtual std::string to_string() = 0;
/* 2. 공통 메소드 올리기 */
std::string CapitalizedType() {
std::string type = type_;
std::transform(type.begin(), type.begin() + 1, type.begin(), toupper);
return type;
}
};
class Engineer : public CEmployeeType {
virtual std::string to_string() {
return "Engineer";
}
};
class Manager : public CEmployeeType {
virtual std::string to_string() {
return "Manger";
}
};
class Employee {
public:
std::string name_;
CEmployeeType* type_; /* 3. 클래스 받아옴 */
Employee(std::string name, std::string type) : name_(name) {
setType(type); /* 3.setType로 타입 정하기 */
}
std::string GetType() {
return type_;
}
/* 3. create Employee 새롭게 매핑 */
void SetType(std::string arg) {
createEmployee(arg);
}
/* 3. 이전에 구현했던 것과 동일 */
static EmployeeType* CreateEmployeeType(std::string value) {
if (value == "engineer") {
return new Engineer();
}
if (value == "manager") {
return new Manager();
}
std::cout << "Employee cannot be of type " << value << std::endl;
return nullptr;
}
std::string ToString() {
return name_ + " (" + type_->CapitalizedType() + ")";
}
};
IV. 기타
코드가 긴데 핵심은 무엇이냐,
직접상속
: 타입코드를 가지고 있는 거대한 클래스안에서 구현하여 직접 서브클래스에 상속을 내려준다.
객체를 만들어주는 함수를 생성하여 서브클래스 인스턴스 생성 -> 부모클래스에서 객체 받음
-> 팩토리 패턴
간접상속
: 이미 코드가 있는 부분(부모클래스)을 두고 타입 클래스만 정의해준다.
서브클래스는 타입 클래스를 상속받고, 객체 생성은 부모클래스를 통해 만들어준다.
어렵다.. 팩토리패턴을 공부해봐야겠다.
특히 간접 상속부분은... 활용도를 본 적이 없어서 그런가 확 와닿지 않는다.
하지만 리팩토링의 관점(이미 완성되어있는 코드를 수정)에서 보면 간접상속이 더 의미있지 않을까
'Programming > Refactoring(C++)' 카테고리의 다른 글
[Refactoring] 15. 조건부로직을 다형성으로 바꾸기2 (For Repeated Switches) (2) | 2022.11.02 |
---|---|
[Refactoring] 14. 조건부로직을 다형성으로 바꾸기 (For Repeated Switches) (0) | 2022.10.31 |
[Refactoring] 12. 슈퍼클래스 추출하기 (For Large Class) (0) | 2022.10.23 |
[Refactoring] 11. 클래스 추출하기 (For Large Class) (0) | 2022.10.21 |