이 글은 리팩터링 2판(마틴 파울러) 책을 참고하였습니다.

순서

I. 배경
II. 절차
III. 예시
IV. 기타


I. 배경

코드 알고리즘에는 For If (Switch)가 굉장히 많은 것은 모두 느낄 것이다.
이런 복잡한 "조건부 로직"은 코드를 해석하기 어렵게 만드는 주범이다.

따라서 이 조건부 로직을 가장 직관적으로 만드는 것이 중요한데,
조건부 로직을 그대로 둔 채 해결해도 되지만 클래스와 다형성을 이용하면
더 확실하게 분리할 수도 있다.

이는 "13. 타입코드를 서브클래스로 바꾸기"와 비슷한데
case 별로 클래스를 하나씩 만들어 공통 switch 로직의 중복을 없앨 수 있다.
(각 타입의 조건부 로직을 자신만의 방식으로 처리하는 상황)

다형성(polymorphism)은 무엇일까?
다형성은 "상속"과 관련이 깊으며
같은 모양을 가졌지만 전혀 다른 기능을 수행하는 것으로 객체지향의 핵심이다.
1. Override  : 상속관계에 있는 가상함수(Virtual).
2. Overload : 한 객체안에 동일한 관계에 있는 이름이 같은 메소드지만 인자가 다름.

 


II. 절차

1. 다형성 동작을 표현하는 클래스들이 아직 없다면 만들어준다.
이왕이면 적합한 인스턴스를 알아서 만들어 반환하는 팩토리함수도 함께 만든다.

2. 호출하는 코드에서 팩토리 함수를 사용하게 한다.

3. 조건부 로직 함수를 부모클래스로 옮긴다.

-> 조건부 로직이 온전한 함수로 분리되어 있지 않다면
    먼저 함수로 추출한다.

4. 부모클래스의 조건부 로직 메소드를 오버라이드하여 내부를 수정한다.

5. 같은 방식으로 각 조건절을 해당 서브클래스에서 메소드로 구현한다.

6. 부모클래스 메소드에는 기본 동작 부분만 남기거나, 가상함수로 정의한다.

 


III. 예시

 


코드 원본

 

#include "gtest/gtest.h"

enum class BirdType { // 타입
  kEuropeanSwallow,
  kAfricanSwallow,
  kNorwegianBlueParrot,
  kUnknown,
};

enum class BirdPlumage { // 깃털
  kAverage,
  kTired,
  kBeautiful,
  kScorched,
  kUnknown,
};

class Bird { // 모든 새 다 포함
 public:
  BirdType type_;
  int number_of_coconuts_;
  int voltage_;
  bool is_nailed_;
};

BirdPlumage Plumage(Bird* bird) {
  switch (bird->type_) {
    case BirdType::kEuropeanSwallow:
      return BirdPlumage::kAverage;
    case BirdType::kAfricanSwallow:
      return (bird->number_of_coconuts_ > 2) ? BirdPlumage::kTired : BirdPlumage::kAverage;
    case BirdType::kNorwegianBlueParrot:
      return (bird->voltage_ > 100) ? BirdPlumage::kScorched : BirdPlumage::kBeautiful;
    default:
      return BirdPlumage::kUnknown;
  }
}

int AirSpeedVelocity(Bird* bird) {
  switch (bird->type_) {
    case BirdType::kEuropeanSwallow:
      return 35;
    case BirdType::kAfricanSwallow:
      return 40 - 2 * bird->number_of_coconuts_;
    case BirdType::kNorwegianBlueParrot:
      return (bird->is_nailed_) ? 0 : 10 + bird->voltage_ / 10;
    default:
      return -1;
  }
}

쓸데없이 새 이름이 길어서
E_Bird, A_Bird, N_Brid라고 변경한 코드..

<hide/>
#include "gtest/gtest.h"

enum class BirdType { // 타입
  E_Bird,
  A_Bird,
  N_Bird,
  kUnknown,
};

enum class BirdPlumage { // 깃털
  kAverage,
  kTired,
  kBeautiful,
  kScorched,
  kUnknown,
};

class Bird { // 모든 새 다 포함
 public:
  BirdType type_;
  int number_of_coconuts_;
  int voltage_;
  bool is_nailed_;
};

BirdPlumage Plumage(Bird* bird) {
  switch (bird->type_) {
    case BirdType::E_Bird:
      return BirdPlumage::kAverage;
    case BirdType::A_Bird:
      return (bird->number_of_coconuts_ > 2) ? BirdPlumage::kTired : BirdPlumage::kAverage;
    case BirdType::N_Bird:
      return (bird->voltage_ > 100) ? BirdPlumage::kScorched : BirdPlumage::kBeautiful;
    default:
      return BirdPlumage::kUnknown;
  }
}

int AirSpeedVelocity(Bird* bird) {
  switch (bird->type_) {
    case BirdType::E_Bird:
      return 35;
    case BirdType::A_Bird:
      return 40 - 2 * bird->number_of_coconuts_;
    case BirdType::N_Bird:
      return (bird->is_nailed_) ? 0 : 10 + bird->voltage_ / 10;
    default:
      return -1;
  }
}

테스트코드 길어서 hide

<hide/>
TEST(ReplaceConditionalWithPolymorphism, Plumage_EuropeanSwallow) {
  Bird bird = {BirdType::kEuropeanSwallow, 2, 100, true};
  ASSERT_EQ(BirdPlumage::kAverage, Plumage(&bird));
}

TEST(ReplaceConditionalWithPolymorphism, Plumage_AfricanSwallowAverage) {
  Bird bird = {BirdType::kAfricanSwallow, 2, 100, true};
  ASSERT_EQ(BirdPlumage::kAverage, Plumage(&bird));
}

TEST(ReplaceConditionalWithPolymorphism, Plumage_AfricanSwallowTired) {
  Bird bird = {BirdType::kAfricanSwallow, 3, 100, true};
  ASSERT_EQ(BirdPlumage::kTired, Plumage(&bird));
}

TEST(ReplaceConditionalWithPolymorphism, Plumage_NorwegianBlueParrotBeautiful) {
  Bird bird = {BirdType::kNorwegianBlueParrot, 2, 100, true};
  ASSERT_EQ(BirdPlumage::kBeautiful, Plumage(&bird));
}

TEST(ReplaceConditionalWithPolymorphism, Plumage_NorwegianBlueParrotScorched) {
  Bird bird = {BirdType::kNorwegianBlueParrot, 2, 101, true};
  ASSERT_EQ(BirdPlumage::kScorched, Plumage(&bird));
}

TEST(ReplaceConditionalWithPolymorphism, Plumage_Unknown) {
  Bird bird = {BirdType::kUnknown, 2, 100, true};
  ASSERT_EQ(BirdPlumage::kUnknown, Plumage(&bird));
}

TEST(ReplaceConditionalWithPolymorphism, AirSpeedVelocity_EuropeanSwallow) {
  Bird bird = {BirdType::kEuropeanSwallow, 2, 100, true};
  ASSERT_EQ(35, AirSpeedVelocity(&bird));
}

TEST(ReplaceConditionalWithPolymorphism, AirSpeedVelocity_AfricanSwallowAverage) {
  Bird bird = {BirdType::kAfricanSwallow, 2, 100, true};
  ASSERT_EQ(36, AirSpeedVelocity(&bird));
}

TEST(ReplaceConditionalWithPolymorphism, AirSpeedVelocity_NorwegianBlueParrotIsNailedTrue) {
  Bird bird = {BirdType::kNorwegianBlueParrot, 2, 100, true};
  ASSERT_EQ(0, AirSpeedVelocity(&bird));
}

TEST(ReplaceConditionalWithPolymorphism, AirSpeedVelocity_NorwegianBlueParrotIsNailedFalse) {
  Bird bird = {BirdType::kNorwegianBlueParrot, 2, 100, false};
  ASSERT_EQ(20, AirSpeedVelocity(&bird));
}

TEST(ReplaceConditionalWithPolymorphism, AirSpeedVelocity_Unknown) {
  Bird bird = {BirdType::kUnknown, 2, 100, true};
  ASSERT_EQ(-1, AirSpeedVelocity(&bird));
}

각 함수마다 모든 새의 경우에 대해서 switch를 사용하도록
구현되어있다. 이를 고쳐보자.

 


1. 각 Bird Type 별로 자식클래스 만들기

 

#include "gtest/gtest.h"

enum class BirdType {
  E_Bird,
  A_Bird,
  N_Bird,
  kUnknown,
};

enum class BirdPlumage {
  kAverage,
  kTired,
  kBeautiful,
  kScorched,
  kUnknown,
};

class Bird {
 public:
  BirdType type_;
  int number_of_coconuts_;
  int voltage_;
  bool is_nailed_;
};

BirdPlumage Plumage(Bird* bird) {
  switch (bird->type_) {
    case BirdType::E_Bird:
      return BirdPlumage::kAverage;
    case BirdType::A_Bird:
      return (bird->number_of_coconuts_ > 2) ? BirdPlumage::kTired : BirdPlumage::kAverage;
    case BirdType::N_Bird:
      return (bird->voltage_ > 100) ? BirdPlumage::kScorched : BirdPlumage::kBeautiful;
    default:
      return BirdPlumage::kUnknown;
  }
}

int AirSpeedVelocity(Bird* bird) {
  switch (bird->type_) {
    case BirdType::E_Bird:
      return 35;
    case BirdType::A_Bird:
      return 40 - 2 * bird->number_of_coconuts_;
    case BirdType::N_Bird:
      return (bird->is_nailed_) ? 0 : 10 + bird->voltage_ / 10;
    default:
      return -1;
  }
}

/* 1. 모든 새들이 다 가진 특징인
   Bird Class를 상속받는다.
*/
class CE_Bird : public Bird {
};

class CA_Bird : public Bird {
};

class CN_Bird : public Bird {
};

빈 클래스를 만들었다.
이 안에 할 구현은, 각 Type이 모두 가진 메소드들
Plumage(), AirSpeedVelocity() 이다.

각 함수는 공통 이름을 가지나 구현부가 모두 다르다.
-> Bird 클래스 내부의 가상함수로 각 함수 Override 진행한다.

 


2. 공통함수 Override 진행하기

 

#include "gtest/gtest.h"

enum class BirdType {
  E_Bird,
  A_Bird,
  N_Bird,
  kUnknown,
};

enum class BirdPlumage {
  kAverage,
  kTired,
  kBeautiful,
  kScorched,
  kUnknown,
};

class Bird {
 public:
  BirdType type_;
  int number_of_coconuts_;
  int voltage_;
  bool is_nailed_;
  
  /* 2. Override */
  virtual BirdPlumage Plumage() {
    return BirdPlumage::kUnknown;
  }
  
  virtual int AirSpeedVelocity() {
    return -1
  }
};

/* 2. 쓸모없어진 함수들 다 지운다 */

/* 1. 모든 새들이 다 가진 특징인
   Bird Class를 상속받는다.
   2. 상속받은 가상함수 각각 구현
*/
class CE_Bird : public Bird {
public:
  virtual BirdPlumage Plumage() {
    return BirdPlumage::kAverage;
  }
  
  virtual int AirSpeedVelocity() {
    return 35;
  }
};

class CA_Bird : public Bird {
  virtual BirdPlumage Plumage() {
    return (bird->number_of_coconuts_ > 2) ? BirdPlumage::kTired : BirdPlumage::kAverage;
  }
  
  virtual int AirSpeedVelocity() {
    return 40 - 2 * bird->number_of_coconuts_;
  }
};

class CN_Bird : public Bird {
  virtual BirdPlumage Plumage() {
    return (bird->voltage_ > 100) ? BirdPlumage::kScorched : BirdPlumage::kBeautiful;
  }
  
  virtual int AirSpeedVelocity() {
    return (bird->is_nailed_) ? 0 : 10 + bird->voltage_ / 10;
  }
};

Bird의 부모 클래스에서는 순수 가상함수가 아닌 일반 가상함수로 구현되어 있다.
default인 경우의 구현이 필요하기 때문이다.

여기서 추가로,
나는 저 Type Enum도 싫다! 하면 이제 팩토리 패턴으로 만들어주면 된다. 

책은 여기까지만 나와있지만 한번 해볼까~~

TEST(ReplaceConditionalWithPolymorphism, Plumage_EuropeanSwallow) {
  Bird bird = {BirdType::kEuropeanSwallow, 2, 100, true};
  ASSERT_EQ(BirdPlumage::kAverage, Plumage(&bird));
}
이게 기존 테스트 코드 구현부인데,
(실제 동작 알고리즘이라고 생각)
1.  createBird를 사용하도록 해보자.
2. 또한 저 4가지 항목을 묶은 class로
   ("객체 통째로 넘기기")도 해보자

 

3. 팩토리로 만들기 (책 내용 X)

 

#include "gtest/gtest.h"

/* 3. 객체 넘기기위한 Class */
class BirdDetails {
public:
  std::string type_;
  int number_of_coconuts_;
  int voltage_;
  bool is_nailed_;
};

enum class BirdPlumage {
  kAverage,
  kTired,
  kBeautiful,
  kScorched,
  kUnknown,
};

class Bird {
 public:
 // 이렇게 해도 되나...?
 //Bird(BirdDetails bird_details) : *CBirdDetails(bird_details)
 Bird(BirdDetails bird_details) {
   CBridDetails->type_ = bird_details.type_;
   CBridDetails->number_of_coconuts_ = bird_details.number_of_coconuts_;
   CBridDetails->voltage_ = bird_details.voltage_;
   CBridDetails->is_nailed_ = bird_details.is_nailed_;
 }
 
 /* 3. Data 수정해줌 */
  CBirdDetails* CBirdDetails_;
  
  /* 2. Override */
  virtual BirdPlumage Plumage() {
    return BirdPlumage::kUnknown;
  }
  
  virtual int AirSpeedVelocity() {
    return -1
  }
};

class CE_Bird : public Bird {
public:
  using Bird::Bird;
  
  virtual BirdPlumage Plumage() {
    return BirdPlumage::kAverage;
  }
  virtual int AirSpeedVelocity() {
    return 35;
  }
};

class CA_Bird : public Bird {
public:
  using Bird::Bird;
  
  virtual BirdPlumage Plumage() {
    return (bird->number_of_coconuts_ > 2) ? BirdPlumage::kTired : BirdPlumage::kAverage;
  }
  virtual int AirSpeedVelocity() {
    return 40 - 2 * bird->number_of_coconuts_;
  }
};

class CN_Bird : public Bird {
public:
  using Bird::Bird;
  
  virtual BirdPlumage Plumage() {
    return (bird->voltage_ > 100) ? BirdPlumage::kScorched : BirdPlumage::kBeautiful;
  }
  virtual int AirSpeedVelocity() {
    return (bird->is_nailed_) ? 0 : 10 + bird->voltage_ / 10;
  }
};

/* 3. Factory 만드는 중 */
Bird* createBird(BirdDetails bird_details) {
  if(bird_details.type_ == "CE_Bird") {
    return new CE_Bird(bird_details);
  }
  if(bird_details.type_ == "CA_Bird") {
    return new CA_Bird(bird_details);
  }
  if(bird_details.type_ == "CB_Bird") {
    return new CB_Bird(bird_details);
  }
  return nullptr;
}

TEST(ReplaceConditionalWithPolymorphism, Plumage_EuropeanSwallow) {
  BirdDetails bird_details = {"CE_Bird", 2, 100, true};
  Bird* myCEBird = createBird(bird_details);
  ASSERT_EQ(BirdPlumage::kAverage, myCEBird->Plumage());
}

 


IV. 기타

 

자바스크립트에서는 타입 계층 구조 없이도 다형성을 표현할 수 있다.
객체가 적절한 이름의 메소드만 구현하고 있다면 아무 문제없이
같은 타입으로 취급하기 때문이다 (덕 타이핑(duck typing))
-> 따라서 부모클래스인 Bird가 없어도 된다. (라고한다)
JS를 안해서 모른다.

이번 예시는 서브클래스와 다형성을 설명하는 전형적인 방식이었다.

하지만 또 다른 쓰임새로, 거의 똑같은 객체지만
다른 부분도 있음을 표현할 때도 상속을 쓴다.

다음 글에서 예시만 살펴보자.

+ Recent posts