Inheritance(상속) 그리고 Composition(구성)
다음은 언랭의 창시자 Joe Armstrong의 유명한 "고릴라 바나나 문제"의 인용입니다.
I think the lack of reusability comes in object-oriented languages, not functional languages. Because the problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
https://www.johndcook.com/blog/2011/07/19/you-wanted-banana/
나는 바나나를 원했을 뿐인데 전체 정글의 고릴라가 바나나를 들고 있게 된 것이죠. 상상만 해도 웃긴 일이죠.
객체지향 프로그래밍에서 상속의 문제는 지속적으로 지적되어 왔지요. 대표적인 것이 이 고릴라 바나나 문제 입니다.
고민없이 상속을 이용하여 기능을 확장 시킨다면 사용하지도 않는 부모 클래스의 불필요한 기능들을 자식 클래스가 그대로 가지게 되고 부모 자식들 간에 코드의 결합도 또한 점점 높아져서 유연하고 재사용 가능한 코드와는 점점 멀어 질 수 있습니다.
is-a(상속) 보다 has-a(구성)가 나을 수 있습니다.
- 상속 : 화이트박스 재사용(white-box reuse)
내부를 볼 수 있다는 의미에서 나온 말입니다.
상속을 받으면 부모 클래스의 내부가 자식 클래스에게 공개되기 때문입니다.
강력하고 편한 기능이지만 경우에 따라 높은 커플링으로 인해 유연성과 재사용성을 떨어뜨릴 수 있습니다. - 구성(Composition) : 블랙박스 재사용(black-box reuse)
클래스 상속의 대안입니다. 다른 객체를 여러 개 붙여서 새로운 기능 혹은 객체를 구성하는 것입니다.
구성하려는 객체의 인터페이스를 명확하게 정의하여야 합니다.
이런 방식은 객체의 내부는 공개되지 않고 인터페이스를 통해서만 재사용되기 때문에 블랙박스 재사용이라 합니다.
행동이 변화하는 것을 인터페이스로 정의하고 각 행동의 변화를 구상 클래스로 구현합니다.
그런다음 인스턴스가 생성될 때 적절한 행동을 하는 구상 클래스를 동적으로 연결합니다.
이것이 바로 "구성"이며 디자인 패턴 전반에 걸쳐 가장 많이 쓰이고 가장 중요한 개념입니다.
재사용 기법으로 "상속"을 많이 쓰지만 "구성"으로 더욱 재사용 가능한 설계를 만들수 있습니다.
상속과 구성을 재미있게 설명한 글을 한번 보시죠. 영상도 한번 보시고요.
https://medium.com/humans-create-software/composition-over-inheritance-cb6f88070205
이 글의 내용을 요약하여 설명해 보겠습니다.
개와 고양이가 다음과 같이 울죠. 배설도 하고요.
struct Dog
{
void bark();
void poop();
};
struct Cat
{
void meow();
void poop();
};
poop()은 중복된 기능이어서 상속을 이용하여 Animal 부모 클래스를 만들고 poop()을 공유하여 사용합니다.
struct Animal
{
void poop();
};
struct Dog : public Animal
{
void bark();
};
struct Cat : public Animal
{
void meow();
};
청소 로봇과 살인자 로봇이 필요하고 로봇들은 마찬가지로 drive()를 상속을 이용하여 기능을 공유하여 재활용합니다.
struct Robot
{
void drive();
};
struct CleaningRobot : public Robot
{
void clean();
};
struct MurderRobot : public Robot
{
void kill();
};
코드는 간결하여 훌륭합니다. 개발자 역시 스스로 이러한 개발을 스스로 뿌듯해하고 있죠.
그런데 이때 고객의 요청이 들어 왔습니다.
"살인자 로봇이 필요한데 짖었으면 하고 똥은 싸지 말아야 합니다."
이런 요청을 받는다면 완전 맨붕에 빠지겠죠. 기존 구조는 모두 엉망이 되고 대전제가 모두 흔들리니까요. 살인자 로봇이 멍멍하고 짖어야 한다니... 그렇지만 이것이 현실입니다. 어플리케이션 개발을 할때는 항상 고객의 요구를 만족시켜야 하고 이전의 요구는 항상 변할 수 있으니까요.
코드를 많이 수정하지 않는 다면 다음과 같이 고객의 요구를 만족 시켜 줄 수 있겠죠.
struct GameObject
{
void bark();
};
struct Robot : public GameObject
{
void drive();
};
struct CleaningRobot : public Robot
{
void clean();
};
struct MurderRobot : public Robot
{
void kill();
};
struct MurderRobotDog : public MurderRobot
{
};
그러나 이 구현은 "고릴라 바나나 문제"가 발생 했죠. 청소 로봇도 짖을 수 있네요. @.@
아니면 중복 함수를 만든다면 다음과 같이 구현 할 수도 있고요. bark() 구현이 중복 되었죠.
struct Robot
{
void drive();
};
struct CleaningRobot : public Robot
{
void clean();
};
struct MurderRobot : public Robot
{
void kill();
};
struct MurderRobotDog : public MurderRobot
{
void bark();
};
구성이 바로 구원자 입니다.
상속은 그것이 무엇 인지(is-a)를 디자인 하는 것이고 구성은 무엇을 가지는(has-a)를 디자인하는 것입니다.
MurderRobotDog을 구성으로 구현하면 다음과 같습니다. 비슷한 방법으로 Dog, Cat, CleaningRobot 등도 구현해주면 됩니다. 구성을 이용하면 OCP를 위한하지 않고 기존 코드에 영향을 주지 않으면서 마음껏 기능 확장을 할 수 있습니다.
struct Behavior
{
virtual void process() = 0;
};
struct Robot
{
map<string, Behavior*> behaviors;
void execute(string command)
{
behaviors[command]->process();
}
};
struct Driver : public Behavior
{
void process() override
{
cout << "drive";
}
};
struct Backer : public Behavior
{
void process() override
{
cout << "bark";
}
};
struct Cleaner : public Behavior
{
void process() override
{
cout << "clean";
}
};
struct Murder : public Behavior
{
void process() override
{
cout << "kill";
}
};
struct MurderRobotDog : public Robot
{
MurderRobotDog()
{
behaviors.emplace("drive", new Driver());
behaviors.emplace("bark", new Backer());
behaviors.emplace("kill", new Murder());
}
};
struct MurderRobot : public Robot
{
MurderRobot()
{
behaviors.emplace("drive", new Driver());
behaviors.emplace("kill", new Murder());
}
};
struct MurderRobot : public Robot
{
MurderRobot()
{
behaviors.emplace("drive", new Driver());
behaviors.emplace("clean", new Cleaner());
}
};
int main()
{
MurderRobotDog robot;
robot.execute("bark");
robot.execute("kill");
}
OCP를 모르고 있다면 다음글 참고해주세요.
또 다른 구현 방법으로 Minxin을 이용하여 다음과 같이 구현 할 수도 있어요.
struct Robot
{
};
template<class Base> struct Backer : Base
{
bark();
};
template<class Base> struct Driver : Base
{
drive();
};
template<class Base> struct Murder : Base
{
kill();
};
using MurderRobotDog = Murder<Driver<Backer<Robot>>>;