이 글은 리팩터링 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. 기타

코드가 긴데 핵심은 무엇이냐,

직접상속
: 타입코드를 가지고 있는 거대한 클래스안에서 구현하여 직접 서브클래스에 상속을 내려준다.
  객체를 만들어주는 함수를 생성하여 서브클래스 인스턴스 생성 -> 부모클래스에서 객체 받음
-> 팩토리 패턴

간접상속
: 이미 코드가 있는 부분(부모클래스)을 두고 타입 클래스만 정의해준다.
  서브클래스는 타입 클래스를 상속받고, 객체 생성은 부모클래스를 통해 만들어준다.

어렵다.. 팩토리패턴을 공부해봐야겠다.
특히 간접 상속부분은... 활용도를 본 적이 없어서 그런가 확 와닿지 않는다.
하지만 리팩토링의 관점(이미 완성되어있는 코드를 수정)에서 보면 간접상속이 더 의미있지 않을까

+ Recent posts