⭐팩토리 패턴

모듈(객체) 생성 부분만 따로 때어놓고 추상화하는 패턴이고, 부모 클래스에서 자식 클래스를 생성할때 유형(사용목적)을 변경할수 있도록 전략화할수 있는 패턴 (부모 클래스는 범용적인 속성, 메소드를 미리 구현하여 재구성 비용을 절약한다)

실제 적용 예시

  • 과자 공장에서 판매자 유형에 따라 과자의 종류를 바꿔서 제품을 찍어내는 구조와 비슷하다.
  • 컴포넌트 확장 *(Input > PasswordInput or NameInput ) 으로 용도에 맞는 확장에 사용되어 짐
  • 비지니스 로직 클래스 모델의 확장 등 (상속 관계를 명확하게 하는)
  • 타입스크립트에선 추상클래스를 사용하는것이 좋다고 한다. (이유는 더 찾아보자.)
class Latte {
  constructor() {
    this.name = "latte";
  }
}
class Espresso {
  constructor() {
    this.name = "Espresso";
  }
}

class LatteFactory {
  static createCoffee() {
    return new Latte();
  }
}
class EspressoFactory {
  static createCoffee() {
    return new Espresso();
  }
}
const factoryList = { LatteFactory, EspressoFactory };

class CoffeeFactory {
  static createCoffee(type) {
    const factory = factoryList[type];
    return factory.createCoffee();
  }
}
const main = () => {
  // 라떼 커피를 주문한다.
  const coffee = CoffeeFactory.createCoffee("LatteFactory");
  // 커피 이름을 부른다.
  console.log(coffee.name); // latte
};
main();
/*
CoffeeFactory라는 상위 클래스가 중요한 뼈대를 결정하고 하위 클래스인 LatteFactory
가 구체적인 내용을 결정하고 있습니다. 
참고로 이는 의존성 주입이라고도 볼 수 있습니다. 
CoffeeFactory에서 LatteFactory의 인스턴스를 생성하는 것이 아닌 
LatteFactory에서 생성한 인스턴스를 CoffeeFactory에 주입하고 있기 때문이죠.
또한, CoffeeFactory를 보면 static으로 createCoffee() 정적 메서드를 정의한 것을 알
수 있는데, 정적 메서드를 쓰면 클래스의 인스턴스 없이 호출이 가능하여 메모리를 절약
할 수 있고 개별 인스턴스에 묶이지 않으며 클래스 내의 함수를 정의할 수 있는 장점이 있
습니다. 
*/

언제 사용할까 ? (적용)

  • 의존 관계가 명확할때 효율적으로 컴포넌트 혹은 클래스(모델)을 관리하기 위해서 사용한다.
  • 생성자(Factory)를 통해 생성되는 것이기 때문에 이런 생성자로 많은 수(많은 타입)의 자식을 생성해야 할때 사용한다.

장점

  • 객체지향 설계 원칙을 위해 하지 않는다.
  • 한번 잘 만들어 놓으면 효율적으로 자식농사를 지을수 있다.

단점

  • 자식의 종류에 따라 팩토리가 무거워 질수 있어, 효율적인 확장을 위해선 새로운 계층 구조를 만들거나 새로운 의존성 주입을 해야 한다.

팩토리 메소드

  • 자식 클래스 생성시 자식의 유형을 변경할수 있도록 한다.
const num = new Object(43);
num.constructor.name >>> "Number";

const str = new Object("aaa");
str.constructor.name >>> "String";
  • 컴포넌트 설계에서 많이 사용한다.

추상 팩토리 (Next Step)

  • 생성에 관련한 메소드를 추상화 하여 작성하고, 그 추상화된 메소드를 조립하여 새로운 생성자를 만들어내는 유연한 구조의 팩토리 패턴이다.
  • 팩토리 메소드에 비해 계층구조로 확장하거나, 의존성 주입이 필요없이 조합으로 해결한다.
export interface AbstractProductA {
    methodA(): string;
}
export interface AbstractProductB {
    methodB(): number;
}

export interface AbstractFactory {
    createProductA(param?: any) : AbstractProductA;
    createProductB() : AbstractProductB;
}

export class ProductA1 implements AbstractProductA {
    methodA = () => {
        return "This is methodA of ProductA1";
    }
}
export class ProductB1 implements AbstractProductB {
    methodB = () => {
        return 1;
    }
}

export class ProductA2 implements AbstractProductA {
    methodA = () => {
        return "This is methodA of ProductA2";
    }
}
export class ProductB2 implements AbstractProductB {
    methodB = () => {
        return 2;
    }
}

export class ConcreteFactory1 implements AbstractFactory {
    createProductA(param?: any) : AbstractProductA {
        return new ProductA1();
    }

    createProductB(param?: any) : AbstractProductB {
        return new ProductB1();
    }
}
export class ConcreteFactory2 implements AbstractFactory {
    createProductA(param?: any) : AbstractProductA {
        return new ProductA2();
    }

    createProductB(param?: any) : AbstractProductB {
        return new ProductB2();
    }
}

export class Tester {
    private abstractProductA: AbstractProductA;
    private abstractProductB: AbstractProductB;

    constructor(factory: AbstractFactory) {
        this.abstractProductA = factory.createProductA();
        this.abstractProductB = factory.createProductB();
    }

    public test(): void {
        console.log(this.abstractProductA.methodA());
        console.log(this.abstractProductB.methodB());
    }
}