Composition vs Inheritance

Ian·2021년 7월 4일
0

Today I Learned

목록 보기
37/40
post-thumbnail

참조한 글들

공부하게 된 이유

  • 최근들어 객체의 상속(inheritance)이 OOP 에서 anti pattern 이라는 글들을 종종 봤다
  • 그리고 그 글에서는 객체의 합성(composition)을 대안으로 제시했다
  • 그래서 궁금해졌다
    1. 왜 OOP 에서 객체의 상속이 anti pattern 인걸까?
    2. 왜 객체의 합성이 대안으로 제시되는걸까?

Object composition

Def

compose 의 정의는 '다양한 것들로부터 구성하는 행위' 를 뜻한다. 예시를 들자면, 이케아 가구나, 조립 PC를 생각해보면 편하다. 우리가 PC를 맞출 때 메인보드, CPU, 그래픽카드 등을 '조립' 해서 '구성' 한뒤 사용하는 걸 사용하면 된다.

Inherit

우리가 익히 알고있는 상속이다. 강아지라는 Class 를 만들기 전, 포유류라는 Class 를 만들고, 포유류라는 Class 는 동물이라는 Class로부터 상속받는 방식을 의미한다.

코드로 옮기면 이러할 것이다.

interface BasicSpec {
  height: number;
  weight: number;
}

interface BasicSpecMammal extends BasicSpec {
  type: string;
}

interface BasicSpecDog extends BasicSpecMammal {
  dogName: string;
}

class Animal {
  constructor(public animalBasicSpec: BasicSpec) {}
}

class Mammal extends Animal {
  constructor(public mammalBasicSpec: BasicSpecMammal) {
    super(mammalBasicSpec);
  }
}

class Dog extends Mammal {
  constructor(public dogBasicSpec: BasicSpecDog) {
    super(dogBasicSpec);
  }
}

const retriever = new Dog({ 
  dogName: 'goldie', 
  height: 150,
  weight: 30,
  type: 'dog',
});

예제에 나온 코드는 이렇다

class Shape {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class Movable extends Shape {
  move(dx, dy) {
    this.x += dx;
    this.y += dy;
  }
}

class Hero extends Movable {
  constructor(x, y) {
    super(x, y);
    this.name = 'hero';
  }
}

class Monster extends Movable {
  constructor(x, y) {
    super(x, y);
    this.name = 'monster';
  }
}

const movables = [new Hero(1,1), new Monster(2,2)];
movables.forEach(m => m.move(2, 3));

HeroMonster 라는 subclass 는 공통 조상으로 Movable 이라는 class 를 가지고 있다. 그리고 이 정도면 상당히 이상적인 경우이다. 얼마 안 되는 subclass 를 가지고 있으니깐.

그러나, 현실의 Codebase 는 이보다 더더욱 복잡하다. 20개정도 되는 Class 들이 상속되고, 또 상속 되어진다. 의존 관계가 복잡해지고, 이러다 보면 각각의 관계를 파악하기 쉽지 않아 SOLID principle 의 L, Liscov Substitution Principle 을 위반하기가 쉽다.

class NPC extends Movable {
   move(dx, dy) {
     console.log('I wont move')
   }
 }

Movable class 를 상속하여 만든 NPC 라는 class 는 movable 을 대체할 수 없다. 이를 통제하기 위해선 어떻게 해야할까? 객체지향적으로 접근하려면, MovableButDoNotMove 라는 Class 를 또 만들어주어야 하는가? 쉽지 않은 일이다.

Liscov Substitution Principle

Babara Liscov 가 주장한 원칙으로, 'Derived/Child Class 가 Base/Parent Class 대신 사용되는 데 아무런 문제가 없어야 한다' 라는 이야기이다.

참고한 예시에서는 '농부의 아들은 농부인 아빠의 농사짓는 기술을 상속받아야 하며, 아빠 대신에 농사를 짓는 곳에 투입되어도 아무런 문제가 없어야한다' 라고 이해시켜준다.

extends, implements 등으로 상속 및 구현을 하게 되면, 이렇게 충분히 생길 수 있는 예외적인 상황을 대처하기가 쉽지 않아진다.

그리고, 일상속의 우리는 IS-A 관계보다는 HAS-A 관계로 사고했던 기간이 더 길기도 하니, 현실의 생각과 크게 어긋나지 않게 코드를 작성하면 더 좋기도 하다.

우리가 자동차나 비행기를 생각할 때, 이들을 '음... 움직이는 무언가로부터 각각을 상속하고, 각각의 기능들이 구현되었군' 이라고 생각하지 않고, '자동차는 엔진과 바퀴, 그리고 시트, 핸들 등으로 이뤄져있네', '비행기는 날개와 엔진, 조종석 등으로 이뤄져있네' 라고 생각하지 않는가?

Function Composition

코드를 작성하기 전, 수학적 용어인 '함수의 합성' 을 빌려와서 생각해보자. 위키피디아에 따르면 함수의 합성이란 여러 개의 함수를 활용해 또 다른 인자로 받는 것을 의미한다. H(x)=G(F(x))H(x) = G(F(x)) 와 같이 말이다.

Object Composition

우리가 익히 써 왔던 의존성 주입을 활용하는 것이 바로 객체의 합성이란 개념에 가깝다.

class SteeringWheel {
  steer() {}
}

class Engine {
  run() {}
}

class Car {
  constructor(
    public steringWheel: SteeringWheel,
    public engine: Engine,
  ) {}

  steer() {
    this.steringWheel.steer();
  }

  run() {
    this.engine.run();
  }
}

이런 식으로 Class 의 constructor 에 또 다른 Class 를 구성요소로서 넣어주면 된다.

const carInstance = new Car(
  new SteeringWheel(),
  new Engine(),
);

그리고 이런 식으로 객체를 만들 때 또 다른 객체를 구성요소로 집어넣어 만들어주면 된다.

profile
правда и красота, truth and beauty

0개의 댓글