Typescript- OOP Programing-4

0

Typescript- OOP Programing-4

상속의 문제점과 composition, 추상화 클래스에 대해 알아보자

이 챕터를 마친 후, 수직적인 깊은 관계가 맺은 상속을 사용하기 보다는 가능한 composition을 활용하는 것을 지향하며 ,
프로젝트에 따라서 상속이 유용하게 쓰일 경우도 있으니 상황에 맞게 판단하되 상속과 컴포지션을 이용해서 깊은 수직 관계는 피하는 것이 좋지 않을까 생각한다.

Inhertiance의 문제점

지난 oop-programing-3이어서 예시와 함꼐 알아보자.

현재 저번 시간 까지 커피 머신이 있고 그것을 상속하는 SweetCoffeMachine, CaffeLatteMachine이 있었다.

만약 여기서 우유와 설탕이 동시에 들어가는 달달한 라떼 머신을 만들려면 구조에 대한 고민이 필요하다. 혹은 흑설탕이 들어가는 유유 라떼 머신을 만들다고 생각하면??

  • 상속의 깊이가 길어지면 서로 간의 관계가 복잡해질 수 있다.
  • 상속의 치명적인 문제는 부모의 클래스를 수정하면 모든 자식 클래스의 영향을 미칠 수 있는 단점이 존재한다.
  • 타입스크립트에서는 하나의 클래스만 상속이 가능하다.

Composition

  • 아래 코드에서의 문제점은 SweetCaffeLatteMachineSweetCoffeeMakerAutomaticSugarMixer너무 밀접하게 묶여있어서, 다른 설탕제조기를 만들 필요가 있다거나 하면 그 관련 부분을 전부 업데이트를 해줘야 하는 상황이 발생한다.
    • 즉, 클래스끼리 너무 연관이 있으면, 좋지 않다.
{
  type CoffeeCup = {
    shots: number;
    hasMilk?: boolean;
    hasSugar?: boolean;
  };

  interface CoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
  }

  class CoffeeMachine implements CoffeeMaker{ ... }

  // 저렴한 스팀기
  class CheapMilkSteamer {
    private steamMilk(): void {
      console.log(`Steaming some milk🥛...`);
    }

    makeMilk(cup: CoffeeCup): CoffeeCup {
      // 커피컵을 데워서 반환하는 함수
      this.steamMilk();
      return {
        ...cup,
        hasMilk: true,
      };
    }
  }

  // 자동 설탕 믺서기가 있다고 가정하고 만들어보자.
  class AutomaticSugarMixer {
    private getSugar() {
      // 슈가를 가져오는 함수라고 해보자. 복잡한 과정이 있다고 해보자 사탕을 뿌시거나 사탕수수를 정제하는 등...
      console.log(`Getting some sugar from 🍭`);
      return true;
    }

    addSugar(cup: CoffeeCup): CoffeeCup {
      const sugar = this.getSugar(); // 슈가를 가져왔다고 가정하고..
      console.log(`Adding sugar...`);
      return {
        ...cup,
        hasSugar: sugar,
      };
    }
  }

  class CaffeLatteMachine extends CoffeeMachine {
    //dependency injection을 활용하여 milkFother의 외부요소를 넣어줌
    constructor(
      beans: number,
      public readonly serialNumber: string,
      private milkFother: CheapMilkSteamer
    ) {
      super(beans);
    }

    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      return this.milkFother.makeMilk(coffee);
    }
  }

  class SweetCoffeeMaker extends CoffeeMachine {
    constructor(private beans: number, private sugar: AutomaticSugarMixer) {
      super(beans); //자식에서는 super호출 해야함
    }

    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      return this.sugar.addSugar();
    }
  }

  class SweetCaffeLatteMachine extends CoffeeMachine {
    constructor(
      private beans: number,
      private milk: CheapMilkSteamer,
      private sugar: AutomaticSugarMixer 
       // 사탕을 부셔서 복잡한 과정으로 설탕을 만든지 모름 단순히 DI(depenecy Injection)을 통해 외부에서 주입됨.
    ) {
      super(beans);
    }

    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      const addedSugar = this.sugar.addSugar(coffee);
      return this.milk.makeMilk(addedSugar);
    }
  }
}

Interface를 이용한 Composition

  • 위에 composition을 이용하여 아래처럼 커피머신을 만들었다고 해보자. 이런 경우 cheapMilk가 아니 더 좋은 우유나 다른 설탕제조기를 쓴다면, sweetLatteMachine이나 latteeMahcine은 무용지물이 되버린다.
  • 이런 경우 클래스 자신을 노출하는 것이 아니라 interface(계약서)에 의해서 클래스끼리 상호작용을 하도록 변경해줌으로써 해결할 수 있다.
	const cheapMilkMaker = new CheapMilkSteamer();
  const candySugar = new CandySugarMixer(24);
  const sweetMachine = new SweetCoffeeMaker(24, candySugar);
  const latteMachine = new CaffeLatteMachine(24, "ssn", cheapMilkMaker);
  const sweetLatteMachine = new SweetCaffeLatteMachine(24,cheapMilkMaker,candySugar)

After Solution

아래와 같이 interface의 확장을 통해 클래스끼리의 커넥션을 디 커플링 해줄 수 있고, 다양한 머신으로 재 탄생하게끔 할 수 있다.

  • 우유스팀기의 경우 , 고급버전과 낮은 버전이 있고 슈가에서도 흑설탕처럼 고급 슈가와 저렴한 슈가 버전이 있다고 해보자. 또한, No sugar처럼 단맛을 뺀 옵션이나 우유 없는 옵션도 있다고 해보자.
{
  type CoffeeCup = {
    shots: number;
    hasMilk?: boolean;
    hasSugar?: boolean;
  };

  interface MilkFrother {
    makeMilk(cup: CoffeeCup): CoffeeCup;
  }

  interface SugarSource {
    addSugar(cup: CoffeeCup): CoffeeCup;
  }

  // 스팀기
  //싸구려 스팀기 
  class CheapMilkSteamer implements MilkFrother {
    makeMilk(cup: CoffeeCup): CoffeeCup {
      console.log(`Steaming some milk🥛...`);
      return {
        ...cup,
        hasMilk: true,
      };
    }
  }

  // 고급 스팀기
  class FancyMilkSteamer implements MilkFrother {
    makeMilk(cup: CoffeeCup): CoffeeCup {
      console.log(`Fancy!!!! Steaming some milk🥛...`);
      return {
        ...cup,
        hasMilk: true,
      };
    }
  }
  
  class Nomilk implements MilkFrother {
    makeMilk(cup: CoffeeCup): CoffeeCup {
      return cup;
    }
  }

  // 설탕제조기 
  // 고급 자동 믹서
  class AutomaticSugarMixer implements SugarSource {
    addSugar(cuppa: CoffeeCup): CoffeeCup {
      console.log(`Adding sugar...`);
      return {
        ...cuppa,
        hasSugar: true,
      };
    }
  }
  
   class SugarMixer implements SugarSource {
    addSugar(cuppa: CoffeeCup): CoffeeCup {
      console.log(`Adding awsome sugar...`);
      return {
        ...cuppa,
        hasSugar: true,
      };
    }
  }
  
   class Nosugar implements SugarSource {
    addSugar(cup: CoffeeCup): CoffeeCup {
      return cup;
    }
  }

  interface CoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
  }

  class CoffeeMachine implements CoffeeMaker {
    private static BEANS_GRAMM_PER_SHOT: number = 7; // class level
    private coffeeBeans: number = 0; // instance (object) level

    -------------------------
    //before
    // 기존에는 단순히 커피 콩만 가지고 커피 콩을 내렸다면 다양한 옵션에 대응하게 끔 Consturetor를 변경해줄 수 있다.
    constructor(coffeeBeans: number) {
      this.coffeeBeans = coffeeBeans;
    }
  
  	//after
  	constructor(
      coffeeBeans: number,
      private milk: CheapMilkSteamer,
      private sugar: AutomaticSugarMixer
    ) {
      this.coffeeBeans = coffeeBeans;
    }
  
   ----------------------------
    //원래 아래의 statci의 makeMachine을 대체해서 다양한 머신으로 활용하게 끔 한다. 
   // constructor에 우유와 설탕 옵션을 넣어줌으로써 	    static 메소드를 제거 해준다.
    static makeMachine(coffeeBeans: number): CoffeeMachine {
      return new CoffeeMachine(coffeeBeans);
    }
  ----------------------

    fillCoffeeBeans(beans: number) {
      if (beans < 0) {
        throw new Error("value for beans should be greater than 0");
      }
      this.coffeeBeans += beans;
    }

    clean() {
      console.log("cleaning the machine...🧼");
    }

    private grindBeans(shots: number) {
      console.log(`grinding beans for ${shots}`);
      if (this.coffeeBeans < shots * CoffeeMachine.BEANS_GRAMM_PER_SHOT) {
        throw new Error("Not enough coffee beans!");
      }
      this.coffeeBeans -= shots * CoffeeMachine.BEANS_GRAMM_PER_SHOT;
    }

    private preheat(): void {
      console.log("heating up... 🔥");
    }

    private extract(shots: number): CoffeeCup {
      console.log(`Pulling ${shots} shots... ☕️`);
      return {
        shots,
        hasMilk: false,
      };
    }

    --------------------------------------------------

    //before
    //기존에는 커피만 내리는 것이였다면 이제는 다양한 옵션에 대응하게 끔 변경해준다.
    makeCoffee(shots: number): CoffeeCup {
      this.grindBeans(shots);
      this.preheat();
      return this.extract(shots);
    }
  }
  
   //after- 밀크가 있거나 단맛이 추가된 경우에 따라 최종 커피가 결정된다.
	  makeCoffee(shots: number): CoffeeCup {
      this.grindBeans(shots);
      this.preheat();
      const coffee = this.extract(shots);
      const addedSugar = this.sugar.addSugar(coffee);
      return this.milk.makeMilk(addedSugar);
    }

  -------------------------------------
  class CaffeLatteMachine extends CoffeeMachine {
    constructor(beans: number, public readonly serialNumber: string) {
      super(beans);
    }
    private steamMilk(): void {
      console.log("Steaming some milk... 🥛");
    }
    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      this.steamMilk();
      return {
        ...coffee,
        hasMilk: true,
      };
    }
  }

  class SweetCoffeeMaker extends CoffeeMachine {
    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      return {
        ...coffee,
        hasSugar: true,
      };
    }
  }

 
  -----------------------------------------------------------------
  // milk - 저렴한 버전과 고급버전 2가지를 만들었다.
  const cheapMilkMaker = new CheapMilkSteamer();
  const fancyMilkMaker = new FancyMilkSteamer();
	const noMilk = new NoMilk();
  
  // Sugar 
  const candySugar = new CandySugarMixer(24);
  const sugar = new SugarMixer();
	const noSugar = new NoSugar();
  
  // machine
  const sweetCandyMachine = new SweetCoffeeMaker(24, candySugar); // 저렴한 cadySugar를 만드는 머신
  const sweetMachine = new SweetCoffeeMaker(24,sugar); //고급 슈가를 사용하는 머신
  -------------------------------------------------------------------------------------
  const latteMachine = new CaffeLatteMachine(24, "ssn", cheapMilkMaker); //저렴한 라떼머신
  const highLatteMachine = new CaffeLatteMachine(24,"ssn",fancyMilkMaker); //고급 라떼머신
  
}

Abstract

  • abstract의 키워드를 붙여서 추상 클래스를 정의 할 수 있다.

  • 단독으로 클래스를 생성할 수 없고 추상클래스를 상속하는 자식 클래스를 통해 객체를 생성 해야 한다.

  • 구현하는 클래스마다 달라져야 하는 내용이 있다면 그 부분만 abstract메소드로 정의 할 수 있다.(interface 정의한 것처럼 함수이름과 어떤인자를 받받아서 리턴하는지만 정의를 하고 상세 세부 로직은 abstract 클래스를 구현하는 곳에서 작성하면 된다.)

  • 추상 클래스를 상속하는 클래스는 추상 클래스에 정의된 메서드를 반드시 구현해야 한다. 구현하지 않을 경우 오류 메세지 발생

  • interface에는 속성과 행동의 타입만 정의 해놓는 반면에, 추상클래스는 필요한 로직들까지 정의를 할 수 있다.

Abstract vs interface

  • 둘 다 new 키워드를 통애 객체를 만들 수 없다.
  • 차이점이라고 하면 인터페이스(interface)는 ===규격사항(계약서) 로 보면, 추상클래스(abstract)는 규격사항+필수 기능(로직)구현 이라고 볼 수 있다.
{
  type CoffeeCup = {
    shots: number;
    hasMilk?: boolean;
    hasSugar?: boolean;
  };

  interface CoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
  }

  // 클래스 앞에 abstract 키워드를 붙여서 추상클래스로 정의하면, (1)에서 에러가 발생한다.
  // abstract를 붙이면 그 자체로 object를 생성할 수 없는 클래스이고 추상적인 클래스이다. 
  // 공통기능은 구현하고, 달라져야 할 기능만 abstract로 정의
  
  abstract class CoffeeMachine implements CoffeeMaker {
    private static BEANS_GRAMM_PER_SHOT: number = 7; // class level
    private coffeeBeans: number = 0; // instance (object) level

    constructor(coffeeBeans: number) {
      this.coffeeBeans = coffeeBeans;
    }

    -----------------------------------------------------------
    static makeMachine(coffeeBeans: number): CoffeeMachine {
      return new CoffeeMachine(coffeeBeans); // (1)abstarct 클래스는 인스턴스 생성이 불가.!
    }

   ---------------------------------------------------------------
    fillCoffeeBeans(beans: number) {
      if (beans < 0) {
        throw new Error("value for beans should be greater than 0");
      }
      this.coffeeBeans += beans;
    }

    clean() {
      console.log("cleaning the machine...🧼");
    }

    private grindBeans(shots: number) {
      console.log(`grinding beans for ${shots}`);
      if (this.coffeeBeans < shots * CoffeeMachine.BEANS_GRAMM_PER_SHOT) {
        throw new Error("Not enough coffee beans!");
      }
      this.coffeeBeans -= shots * CoffeeMachine.BEANS_GRAMM_PER_SHOT;
    }

    private preheat(): void {
      console.log("heating up... 🔥");
    }

		//자식의 클래스마다 달라지는 행동의 함수 앞에 달라지는 클래스 앞에는 abstract키워드를 붙임
    // 상속한 클래스에서 접근이 가능하도록 접근 제어자인 protected를 abstract 앞에다 붙임
		-----------------------------
    //before
  	private extract(shots: number): CoffeeCup { 
      console.log(`Pulling ${shots} shots... ☕️`);
      return {
        shots,
        hasMilk: false,
      };
    }

		//after

		protected abstract extract(shots:number):CoffeeCup;
		-----------------------------------
      

    makeCoffee(shots: number): CoffeeCup {
      this.grindBeans(shots);
      this.preheat();
      return this.extract(shots);
    }
  }
  

  class CaffeLatteMachine extends CoffeeMachine {
    constructor(beans: number, public readonly serialNumber: string) {
      super(beans);
    }
    private steamMilk(): void {
      console.log("Steaming some milk... 🥛");
    }
    
    --------------------------------------------
		// 기존의 static을 overriding해서 작성할 필요가 없다. 
		//before
    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      this.steamMilk();
      return {
        ...coffee,
        hasMilk: true,
      };
    }

		//after
		protected extract(shots: number): CoffeeCup {
      this.steamMilk();
      return {
        shots,
        hasMilk: true,
      };
    }

		----------------------------------------------
  }

  class SweetCoffeeMaker extends CoffeeMachine {

    ------------------------------------------------
    //before 기존의 상속이면 super를 사용해야 하나 추상화로 바꾼 이후는 다르게 작성이 가능하다.
    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      return {
        ...coffee,
        hasSugar: true,
      };
    }
		-------------------------------------------------
		// 추상메소드만 구현하면된다.
     protected extract(shots: number): CoffeeCup {
      return {
        shots,
        hasSugar: true,
      };
    } 
  }

  const machines: CoffeeMaker[] = [
    new CoffeeMachine(16),
    new CaffeLatteMachine(16, "1"),
    new SweetCoffeeMaker(16),
    new CoffeeMachine(16),
    new CaffeLatteMachine(16, "1"),
    new SweetCoffeeMaker(16),
  ];
  machines.forEach((machine) => {
    console.log("-------------------------");
    machine.makeCoffee(1); // 기존과 동일하게 로그가 찍히는 것을 볼 수 있다.
  });
}
profile
문과생 개발자되다

0개의 댓글