프론트엔드/Typescript

TypeScript에서의 캡슐화와 추상화

테오구 2022. 4. 16. 11:18
728x90

기존의 절차지향 프로그래밍은 단점이 많이 있습니다.

 

프로젝트에 신규로 투입이 되었다면 함수가 여러 가지가 얽혀있어서 다른 곳에 업데이트가 될 때 하나를 수정하기 힘들고 수정하였을 때 다른 사이드 이펙트가 발생할 확률이 높다.

 

반면 OOP는 데이터와 함수를 여러가지 오브젝트로 정의하여 프로그래밍 해나가기 때문에 관련 오브젝트만 수정해주면 되고 반복되어 사용될 수 도 있기 때문에 재사용성도 높다.

type CoffeeCup = {
    shots: number;
  };

  class CoffeeMaker {
    static BEANS_GRAMM_PER_SHOT: number = 7; // class level
    // 오브젝트마다 새로 만들어줘야하는 데이터가 있다면 멤버변수를 사용하고 
    // 클래스에서 공유되어 사용되는 변수들은 중복적으로 데이터가 생성되고
    // 메모리에 낭비가 될 수 있다. 그렇기 때문에 앞에 static을 붙여주어야 한다.
    // 단 this.를 제외하고 클래스의 이름을 붙여주어야한다.
    coffeBeans: number = 0; //instance (object) level

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

    // constructor를 이용하지 않고 커피를 만들고 싶을때
    // 클래스 내부에 있는 어떠한 속성 값도 가지지 않기 때문에 static 사용가능
    static makeMachine(coffeeBeans: number): CoffeeMaker{
      return new CoffeeMaker(coffeeBeans)
    }

    makeCoffe(shots: number): CoffeeCup {
      if (this.coffeBeans < shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT) {
        throw new Error('Not enough coffee beans');
      }
      this.coffeBeans -= shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT;
      return {
        shots,
      };
    }
  }
  const maker = new CoffeeMaker(40); //new:클래스의 인스턴스를 만든다.

  // 단 static을 쓰지 않으면 이렇게 사용할 수 없다.
  const maker2 = CoffeeMaker.makeMachine(40)

클래스마다 새로 만들어줘야하는 데이터가 있다면 멤버변수(const, let)를 사용하고 그렇지 않다면 static을 사용해주는 것이 좋다.

Encapsulation(캡슐화)

중요한 정보들은 은닉화 해주는 것

서로 관련있는 기능들을 캡슐화 하는 것

type CoffeeCup = {
    shots: number
  }

  // public 따로 저장하지 않으면 public으로 저장 됩니다.
  // private
  // protected: 외부에서 접근할 수 없지만 이 클래스르르 상속한
  // 자식 클래스에서만 접근이 가능
  class CoffeeMaker {
    private static BEANS_GRAMM_PER_SHOT: number = 7 //
    protected coffeBeans: number = 0

    protected constructor(coffeBeans: number) {
      this.coffeBeans = coffeBeans
    }

    static makeMachine(coffeBeans: number): CoffeeMaker {
      return new CoffeeMaker(coffeBeans)
    }

    fillCoffeeBeans(beans: number) {
      if (beans < 0) {
        throw new Error('커피콩이 없습니다.')
      }
    }

    makeCoffe(shots: number): CoffeeCup {
      if (this.coffeBeans < shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT) {
        throw new Error('Not enough coffee beans')
      }
      this.coffeBeans -= shots * CoffeeMaker.BEANS_GRAMM_PER_SHOT
      return {
        shots,
      }
    }
  }
  const maker = CoffeeMaker.makeMachine(40) //new:클래스의 인스턴스를 만든다.
  maker.fillCoffeeBeans(40)

 

Abstraction(추상화)

내부의 복잡한 기능을 이해하지 않고 외부에서 간단한 인터페이스를 통해서 쓸 수 있는 것을 의미

추상화를 하는 방법 두가지

  1. 정보 은닉화 사용자가 사용하기 편하게 정말 필요한 함수들만 public으로 노출 시키고 나머지는 private이나 protected로 보호한다.
  2. interface를 사용합니다. interface는 규격으로 반드시 interface의 조건을 만족해야한다.
class CoffeeMachine {
	fillCoffeeBeans(beans:number){
      if(beans < 0){
        throw new Error('커피콩이 없습니다.');
      }
    }

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

preheat():void{
	console.log('heating up')
}

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

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

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

커피를 만들어주는 함수를 만들어주었고 maker를 호출하면 많은 함수가 나오게 됩니다.

 

하지만 우리가 maker를 만들어준 이유는 커피를 만들어주기 위함이고 makeCoffee 함수를 호출하게 되면 다른 함수들도 호출하는 것이기 때문에 다른 함수들을 사용할 수 없게 추상화해주어야 합니다.

 

타입스크립트에서는 접근제어자(private, protected)도 가능하지만 interface도 추상화하는데에 사용되어집니다.

//   예열하는 함수
    private preheat(): void {
      console.log('heating up...');
    }

    //   추출하는 함수
    private extract(shots: number): CoffeeCup {
      this.grindBeans(shots);
      this.preheat();
      return { shots };
    }
type CoffeeCup = {
    shots: number;
  };

  interface CoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
    fillCoffeeBeans(beans: number): void;
  } //지켜야하는 규칙을 명시

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

  const maker: CoffeeMachine = CoffeeMachine.makeMachine(40); //new:클래스의 인스턴스를 만든다.
  maker.fillCoffeeBeans(40);
  maker.makeCoffee(2);

  const maker2: CommercialCoffeeMaker = CoffeeMachine.makeMachine(40); //new:클래스의 인스턴스를 만든다.
  maker2.fillCoffeeBeans(40);
  maker2.makeCoffee(2);

CommercialCoffeeMaker라는 인터페이스를 만들어주고 maker의 타입은CoffeeMachine으로 maker2의 타입은 CommercialCoffeeMaker을 지정해주게 되면 CommercialCoffeeMaker에는 makeCoffee라는 규격 밖에 없기 때문에 fillCoffeeBeans 함수를 사용할 수 없게 됩니다.

 

  interface CoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
    fillCoffeeBeans(beans: number): void;
  } //지켜야하는 규칙을 명시

  interface CommercialCoffeeMaker {
  // 커피를 만들어주는 함수
    makeCoffee(shots: number): CoffeeCup;
    // 커피콩은 만드는 함수
    fillCoffeeBeans(beans: number): void;
    // 청소하는 함수
    clean(): void;
  }

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();
    }
  }

이제 AmateurUser와 ProBarista라는 클래스를 만들어주어 생성자의 타입을 CoffeeMaker와CommercialCoffeeMaker로 해준다면 AmateurUser의 machine은 makeCoffee 함수만 사용할 수 있고 ProBarista의 machine 은  makeCoffee, fillCoffeeBeans, clean함수를 사용할 수 있습니다.

 

728x90

'프론트엔드 > Typescript' 카테고리의 다른 글

4.5 번역  (0) 2022.05.05
TypeScript란?  (0) 2021.08.29