개발일지

[TS] 4. 객체지향 프로그래밍 본문

JS, TS/[TS | 강의] TS·OOP

[TS] 4. 객체지향 프로그래밍

lyjin 2022. 10. 30.

22.09.04 일~

OOP 원칙:

  • 캡슐화 : 관련있는 요소끼리 묶는 것
  • 추상화 : 외부에서는 내부가 어떻게 동작하는 지 몰라도 사용할 수 있어야 함
  • 상속
  • 다형성

절차지향 vs 객체지향:

절차지향적 커피머신

{
    type CoffeeCup = {
    shots: number;
    hasMilk: boolean;
  };

  const BEANS_GRAMM_PER_SHOT: number = 7;
  let coffeeBeans: number = 0;

  function makeCoffee(shots: number): CoffeeCup {
    if (coffeeBeans < shots * BEANS_GRAMM_PER_SHOT) {
      throw new Error('Not enough coffee beans!');
    }
    coffeeBeans -= shots * BEANS_GRAMM_PER_SHOT;
    return {
      shots,
      hasMilk: false,
    };
  }

  coffeeBeans += 3 * BEANS_GRAMM_PER_SHOT;
  const coffee = makeCoffee(2);
  console.log(coffee);
}

 

 

객체지향적 커피머신

{
  type CoffeeCup = {
    shots: number;
    hasMilk: boolean;
  };

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

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

    **static** makeMachine(coffeeBeans: number): CoffeeMaker {
      return new CoffeeMaker(coffeeBeans);
    }

    makeCoffee(shots: number): CoffeeCup {
      if (this.coffeeBeans < shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT) {
        throw new Error('Not enough coffee beans!');
      }
      this.coffeeBeans -= shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT;
      return {
        shots,
        hasMilk: false,
      };
    }
  }

  const maker = new CoffeeMaker(32);
  console.log(maker);   // CoffeeMaker: { "coffeeBeans": 32 }
  const maker2 = new CoffeeMaker(14);
  console.log(maker2);  // CoffeeMaker: { "coffeeBeans": 34 }

  const maker3 = CoffeeMaker.makeMachine(3);
  const maker4 = CoffeeMaker.makeMachine(5);
}

 


캡슐화:

  • static?? private??
    • static : class level
    • public, private, protected: 접근 제한자
  • 캡슐화는 외부에서 접근 가능한 요소는 무엇이고 접근하면 안되는 요소는 무엇인지 결정하는 것
{
  type CoffeeCup = {
    shots: number;
    hasMilk: boolean;
  };

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

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

    static makeMachine(coffeeBeans: number): CoffeeMaker {
      return new CoffeeMaker(coffeeBeans);
    }

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

    makeCoffee(shots: number): CoffeeCup {
      if (this.coffeeBeans < shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT) {
        throw new Error('Not enough coffee beans!');
      }
      this.coffeeBeans -= shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT;
      return {
        shots,
        hasMilk: false,
      };
    }
  }

  const maker = CoffeeMaker.makeMachine(32);
  maker.coffeeBeans = -34; // invalid
  maker.fillCoffeeBeans(100);

  /**
   * private coffeBeams > 외부에서 직접 접근 못하게
   * public fillCoffeeBeans > public 메소드로 private property 수정할 수 있게!
   */

    >> 캡슐화는 외부에서 접근 가능한 요소는 무엇이고 접근하면 안되는 요소는 무엇인지 결정하는 것
}

 

 

getter와 setter

class User {
    firstName: string;
    lastName: string;
    fullName: string;

    constructor(firstName: string, lastName: string) {
      this.firstName = firstName;
      this.lastName = lastName;
      this.fullName = `${firstName} ${lastName}`;
    }
}

const user = new User('Steve', 'Jobs');
console.log(user.fullName); // Steve Jobs

// Not using getter
user.firstName = 'Ellie';
console.log(user.fullName); // Steve Jobs
console.log(user.firstName); // Ellie

 

class User {
    firstName: string;
    lastName: string;

    get fullName(): string { ⭐️
      return `${this.firstName} ${this.lastName}`;
    }

    constructor(firstName: string, lastName: string) {
      this.firstName = firstName;
      this.lastName = lastName;
    }
}

const user = new User('Steve', 'Jobs');

// Using getter (접근 방식은 동일)
user.firstName = 'Ellie'; ⭐️
console.log(user.fullName); // Ellie Jobs

 

class User {
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }

  private internalAge = 4;

  get age():** number { ⭐️
    return this.internalAge;
  }

  set age(num: number) { ⭐️
    if (num < 0) {
    }
    this.internalAge = num;
  }

  constructor(private firstName: string, public lastName: string) {} ⭐️
    // 위의 this.~ 방식과 동일
}

const user = new User('Steve', 'Jobs');

// Getter & Setter
user.age = 6; // setter
console.log(user.age); // getter

 


추상화:

  • 외부에서 어떤 것들을 사용할 수 있고, 구현해야하는지? >> 일종의 규약
{
    type CoffeeCup = {
    shots: number;
    hasMilk: boolean;
  };

    // interface: 규약..!
  interface CoffeeMaker { ⭐️
    makeCoffee(shots: number): CoffeeCup;
  }

  interface CommercialCoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
    fillCoffeeBeans(beans: number): void;
    clean(): void;
  }

    // 후에 계속 사용
  class CoffeeMachine implements CoffeeMaker, CommercialCoffeeMaker {
    private static BEANS_GRAMM_PER_SHOT: number = 7; // class level
    private coffeeBeans: number = 0; // instance (object) level

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

    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,
      };
    }

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

  ⭐️ const maker: CoffeeMachine = CoffeeMachine.makeMachine(32);
  maker.fillCoffeeBeans(32);

  ⭐️ const maker2: CoffeeMaker = CoffeeMachine.makeMachine(32);
  maker2.fillCoffeeBeans(32);  // error

  ⭐️ const maker3: CommCoffeeMaker = CoffeeMachine.makeMachine(32);
  maker3.fillCoffeeBeans(32);
  maker3.clean();
}

 

    ...
    interface CoffMaker {...}
    class CoffeeMachine implements CoffeeMaker {...}

    class AmateurUser {
    constructor(private machine: CoffeeMaker) {} ⭐️
    makeCoffee() {
      const coffee = this.machine.makeCoffee(2);
      console.log(coffee);
    }
  }

  class ProBarista { 
    constructor(private machine: CommercialCoffeeMaker) {} ⭐️

    makeCoffee() {
      const coffee = this.machine.makeCoffee(2);
      console.log(coffee);
      this.machine.fillCoffeeBeans(45);
      this.machine.clean();
    }
  }

 

캡슐화 vs 추상화

캡슐화: 외부에서 알면 안되는 정보, 알필요 없는 정보, 적접적으로 수정하면 안되는 정보 (상태와 내부에서만 쓰이는 함수)들을 숨기는 테크닉
추상화: 여러 클래스에 걸쳐서 공통적으로 사용 되는 함수들의 규격을 정의함

 


상속:

    ... 
    interface CoffMaker {...}
    class CoffeeMachine implements CoffeeMaker {...}

    class CaffeLatteMachine **extends CoffeeMachine** {
    constructor(beans: number, public readonly serialNumber: string) {
      super(beans);
    }

    private steamMilk(): void {
      console.log('Steaming some milk... 🥛');
    }

    //overriding
    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      this.steamMilk();
      return {
        ...coffee,
        hasMilk: true,
      };
    }
}

 

 

다형성

    interface CoffMaker {...}
    class CoffeeMachine implements CoffeeMaker {...}

    class CaffeLatteMachine **extends CoffeeMachine** {
    constructor(beans: number, public readonly serialNumber: string) {
      super(beans);
    }
    private steamMilk(): void {
      console.log('Steaming some milk... 🥛');
    }

    //overriding
    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      this.steamMilk();
      return {
        ...coffee,
        hasMilk: true,
      };
    }
  }

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

 

상속의 문제점

상속은 수직적인 관계가 형성되는 것

부모 클래스가 수정되면 의존하던 자식 클래스 모두에게 영향 주게 됨

TS에서는 하나의 클래스 상속만 가능함

→ 불필요한 상속 대신 composition 사용하자!

 

Composition

Dependency Injection

  class CheapMilkStreamer {
    private steamMilk() {
      console.log(`Steaming some milk🥛...`);
    }

    makeMilk(cup: CoffeeCup): CoffeeCup {
      this.steamMilk();

      return {
        ...cup,
        hasMilk: true,
      };
    }
  }

  class AutoSugarMixer {
    private getSugar() {
      console.log(`Adding sugar...`);
      return true;
    }

    addSugar(cup: CoffeeCup): CoffeeCup {
      const sugar = this.getSugar();
      return {
        ...cup,
        hasSugar: this.getSugar(),
      };
    }
  }

  class CoffeeMachine implements CoffeeMaker {...}

  class CaffeLatteMachine extends CoffeeMachine {
    // Dependency Injection
    constructor(
      beans: number,
      serialNum: string,
      private cheapMilkStreamer: CheapMilkStreamer, // DI ⭐️ 
    ) {
      super(beans);
    }

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

  class SweetCoffeeMaker extends CoffeeMachine {
    constructor(beans: number, private sugarMixer: AutoSugarMixer) { // DI ⭐️ 
      super(beans);
    }

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

  class SweetCaffeeLatteeMachine extends CoffeeMachine {
    constructor(
      beans: number,
      private cheapMilk: CheapMilkStreamer, ⭐️ 
      private sugarMixer: AutoSugarMixer,
    ) {
      super(beans);
    }

    //overriding
    makeCoffee(shots: number): CoffeeCup {
      const coffee = super.makeCoffee(shots);
      return this.cheapMilk.makeMilk(this.sugarMixer.addSugar(coffee));
    }
  }

composition 문제점

클래스 간에 밀접한 관계를 맺는 것은 좋지 않음! (ex. CheapMilkStreamer, AutoSugarMixer DI)

 

Composition 문제점 해결하기

  • 클래스 자신을 노출하는 게 아닌 규약에 의거하여 통신해야함 (인터페이스)
  // interface
  interface CoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
  }

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

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

    // class implements
    class ColdMilkStreamer **implements MilkFrother** {...}
    class NoMilk implements MilkFrother {...}

    class CandySugarMixer **implements SugarProvider** {
    protected getSugar() {
      console.log('Adding candy sugar...');
      return true;
    }

    addSugar(cup: CoffeeCup): CoffeeCup {
      return {
        ...cup,
        hasSugar: this.getSugar(),
      };
    }
  }

  class SugarMixer **implements SugarProvider** {
    protected getSugar() {
      console.log('Adding sugar...');
      return true;
    }

    addSugar(cup: CoffeeCup): CoffeeCup {
      return {
        ...cup,
        hasSugar: this.getSugar(),
      };
    }
  }

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

        // DI type implement로 지정
    constructor(
      coffeeBeans: number,
      private milk: MilkFrother, ⭐️
      private sugar: SugarProvider, ⭐️
    ) {
      this.coffeeBeans = coffeeBeans;
    }

        ...
    }

  const noMilk = new NoMilk();
  const coldMilk = new ColdMilkStreamer();
  const candySugar = new CandySugarMixer();
  const sugar = new SugarMixer();

  const sweetCandyMachine = **new CoffeeMachine(12, noMilk, candySugar);**
  const sweetMachine = **new CoffeeMachine(12, coldMilk, sugar);**
  • CoffeeMachine의 DI type을 interface로 지정해준다.
  • 해당 interface를 implements 하는 milk, sugar 클래스들을 생성해준다.
  • type을 인터페이스로 지정해줬기 때문에 해당 인터페이스를 implements한 클래스들 중 원하는 클래스를 인수로 넣어줄 수 있다.

 


추상화:

  • 추상 클래스는 인스턴스를 생성할 수 없다.
  • 클래스 내부에서 수행되어야 하는 함수 절차가 중요하거나, 자식 클래스에서 overriding 필수일 경우 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;
    }

    ...

    // private 지정하면 자식클래스에서 사용 불가 ⭐️
    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... 🥛');
    }

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

 

'JS, TS > [TS | 강의] TS·OOP' 카테고리의 다른 글

[TS] 7. 제네릭-Quiz  (0) 2022.10.30
[TS] 6. 제네릭  (0) 2022.10.30
[TS] 5. 객체지향 프로그래밍-Quiz  (0) 2022.10.30
[TS] 3. 기본 타입-Quiz  (0) 2022.10.30
[TS] 2. 기본 타입 마스터 하기  (0) 2022.10.30