Skip to content

Latest commit

 

History

History
237 lines (169 loc) · 7 KB

File metadata and controls

237 lines (169 loc) · 7 KB

Mixin

  • Mixin이란
  • Mixin은 어떻게 작동하는가?
  • Constrained Mixins
  • Alternative Pattern
  • Constraints
  • 참고자료



Mixin이란



  • 객체 지향 프로그래밍에서, 재사용 가능한 부품을 만들어서 사용하면, 확장성을 높일 수 있다.
  • 이와 같이 Mixin은 여러 클래스간에 메서드를 공유하기 위한 방법 중 하나이다.
  • Mixin을 사용하면 여러 객체 간에 코드를 재사용할 수 있게 해준다.
  • 즉, 특정 기능을 모듈화하여 여러 클래스에서 혼합할 수 있도록 해준다.

Mixin은 어떻게 작동하는가?



  • 클래스를 혼합하여 더 유여하고 재사용가능한 코드 패턴을 제공한다.

  • 클래스가 다른 클래스로 부터 상속을 받는 대신, 모듈화된 기능이나 메서드를 가져와 하나의 클래스로 결합한다.

  • Mixin 패턴은 클래스를 확장하기 위해, 클래스 상속과 제네릭에 의존하며, 타입스크립트는 클래스 표현식 패턴을 이용한다.

  • 먼저, Mixin을 적용할 클래스를 생성한다.

class Sprite {
  name = "";
  x = 0;
  y = 0;
 
  constructor(name: string) {
    this.name = name;
  }
}

기본 클래스를 확장하고 클래스 표현식을 반활할 타입과 팩토리 함수가 필요하다.

다른 클래스에서 확장할 타입이 필요한데, 주로, 책임이 전달되는 타입이 클래스임을 선언한다.
type Constructor = new (...args: any[]) => {};

 Mixin은 스케일 속성을 추가하며, 캡슐화된 private 속성을 사용해서, 변경할  있는 getter와 setter가 있다.
function Scale<TBase extends Constructor>(Base: TBase) {
  return class Scaling extends Base {
    mixin은 원래 private/protected 속성을 선언할  없지만,
    ES2020 이후는 private 필드를 사용할  있다.
    _scale = 1;
    
    setScale(scale: number) {
      this._scale = scale;
    }
 
    get scale(): number {
      return this._scale;
    }
  };
}

이후 Mixin이 적용된 클래스 나태나는 클래스를 생성할  있다.

Sprite 클래스에서, Mixin Scale 적용자와 새로운 클래스를 생성한다.
// Scaling 클래스는 ()안에 있는 클래스를 베이스로 상속을 받는다.
const EightBitSprite = Scale(Sprite);
 
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log(flappySprite.scale);

Constrained Mixins



  • Mixin을 이용해서 제한된 클래스 타입만을 이용해 리턴 시켜줄 수 있다.
  • 이전 코드에서 생성자 타입에 제네릭 인자를 받아들일 수 있도록 수정한다.
// 이전 생성자 타입
type Constructor = new (...args: any[]) => {};
// 제네릭을 사용해서, 제약을 걸어줄 예정
type GConstructor<T = {}> = new (...args: any[]) => T;

이렇게 된다면, 제약된 베이스 클래스와 함께 작동하는 클래스를 생성할  있다.

type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<Sprite>;
type Loggable = GConstructor<{ print: () => void }>;

이후, 특정 베이스를 기반으로 작동하는 Mixin을 만들  있게된다.

function Jumpable<TBase extends Positionable>(Base: TBase) {
  return class Jumpable extends Base {
    jump() {
      // 이 믹스인은 setPos가 정의된 베이스 클래스를 전달받아야만 작동한다.
      // Positionable 제약 때문이다.
      this.setPos(0, 20);
    }
  };
}

Alternative Pattern



  • Mixin문서에서 과거 문서에는 런타임과 타입 계층을 따로 생성한 뒤, 마지막에 병합하는 방식으로 Mixin을 작성하는 방법을 추천했다.
// 각 믹스인은 전통적인 ES 클래스이다.
class Jumpable {
  jump() {}
}

class Duckable {
  duck() {}
}

// 베이스를 클래스합니다.
class Sprite {
  x = 0;
  y = 0;
}

// 그런 다음 인터페이스를 생성하여 기대되는 믹스인과 베이스의 동일한 이름을 병합한다.
interface Sprite extends Jumpable, Duckable {}
// JS 런타임에서 믹스인을 베이스 클래스에 적용한다.
applyMixins(Sprite, [Jumpable, Duckable]);

let player = new Sprite();
player.jump();
console.log(player.x, player.y);

// 이 코드는 코드베이스의 어느 곳에서나 사용할 수 있는 함수를 작성하는 것이다.
// 클래스들을 가져와 적용한다.
declare function applyMixins(derivedCtor: any, constructors: any[]): void;
  • 하지만, 이 패턴의 가장 큰 문제는 제네릭을 사용한 Mixin보다 까다롭다는 것이다.
  • 컴파일러가 자동적으로 처리해주는 부분이 적고, 개발자가 직접 코드를 관리하면서 런타임 동작과 타입 시스템이 일치하도록 많은 노력이 필요하기 때문이다.
  • 제네릭 사용을 권장한다.

Constraints



  • 타입 스크립트에서는 Control Flow Analysis(타입 Narrowing)을 컴파일 내에서 지원하여, Mixin 패턴도 기본적으로 지원이 된다. 하지만, 한계가 존재한다.
  • Control Flow Analysis를 통해 Mixin을 제공할 때, 데코레이터를 사용할 수 없다.
// 믹스인 패턴을 복제하는 데코레이터 함수
const Pausable = (target: typeof Player) => {
  return class Pausable extends target {
    shouldFreeze = false;
  };
};

@Pausable
class Player {
  x = 0;
  y = 0;
}

// Player 클래스는 데코레이터의 타입이 병합되지 않는다
const player = new Player();
player.shouldFreeze; // 'shouldFreeze' 프로퍼티가 'Player' 타입에 존재하지 않는다.

// 런타임 측면은 수동으로 타입 구성이나 인터페이스 병합을 통해 복제될 수 있다.
type FreezablePlayer = Player & { shouldFreeze: boolean };

const playerTwo = (new Player() as unknown) as FreezablePlayer;
playerTwo.shouldFreeze;
  • 데코레이터를 사용해서, Mixin 패턴을 구현할 때 타입스크립트의 컴파일 타임에는 적용이 되지않아서, 런타임에서 수동으로 타입을 조합하거나 인터페이스를 병합해야한다.

  • 클래스 표현식 패턴을 사용하게 된다면, 싱글톤(싱글톤 그 자체가 아닌, 여러 타입의 정적 프로퍼티를 가진 클래스를 만들기 어렵다는 뜻이다.)이 생성되어 서로 다른 변수 타입을 지원할 수 없게 된다. 즉, 동일한 클래스 표현식을 사용하면, 항상 동일한 타입을 가지게 된다.
  • 이에 우리는 제네릭을 사용해서, 다른 클래스를 반환하는 함수를 사용하는 방법을 제시한다.
function base<T>() {
  class Base {
    static prop: T;
  }
  return Base;
}
 
function derived<T>() {
  class Derived extends base<T>() {
    static anotherProp: T;
  }
  return Derived;
}
 
class Spec extends derived<string>() {}
 
Spec.prop; // string
Spec.anotherProp; // string

참고자료



타입스크립트 공식 문서