Typescript- OOP Programing-2

0

Typescript- OOP Programing-2

객체지향의 원칙 4가지를 적용해보며 알아보자.

1. Encapsulation

지난 oop-programing-1에서의 커피머신을 정보를 은닉화해서 캡슐화 하는 과정을 통해 encapsultation에 대해서 알아보자.

객체지향의 원칙 중 하나로 특정 키워드를 사용함으로서 정의한 클래스 외부에서 클래스 내부의 메서드나 멤버변수, 프로퍼티의 접근을 막기 위해 사용한다.

캡슐화라고도 부른다. 캡슐화를 위한 키워드는 public, private, protected가 있다.

외부에서 함수로 설정 할 수 없는 것들을 private와 같은 접근 제어자를 사용하여 외부에서 볼 수 없도록 필요한 정보들만 노출 하는 것도 정보 은닉 캡슐화라고 할 수 있다.

  • public : 생략 시 기본적으로 public, 외부에서 데이터가 접근 가능
  • private : 지정하면 외부에서는 접근할 수 없다. 은닉화 할때 활용 할 수 있다.
  • protected : private가 외부에서는 접근할 수 없는 것과 달리 해당 클래스르 상속한 다른 클래스 내에서는 접근이 가능하다.
type CoffeeCup = {
  shots: number;
  hasMilk: boolean;
};

class CoffeeMaker {
  private static BEANS_GRAMM_PER_SHOT: number = 23; 
  private coffeeBeans: number = 0;

  // contructor를 private로 설정 하여 static 메소드를 사용하여 인스턴스를 만들어주도록 할 수 있다.
  private constructor(coffeeBeans: number) { 
    this.coffeeBeans = coffeeBeans;
  }
		
  //static를 통해 object를 만드는 메소드를 만든다면, 누군가가 생성자 함수를 통해 constructor로 접근하는 것을 막기 위해 주로 사용한다.
  // 이런 경우 private 로 constructor를 만든다.
  static makeMachine(coffeeBeans: number): CoffeeMaker { 
    return new CoffeeMaker(coffeeBeans);
  }

  fillCoffeeBeans(beans: number) {           
    // 함수를 통해 beans을 채우기 때문에, 그에 대한 에러 처리 핸들링을 해준다.
    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 = new CoffeeMaker(32);

// 그전 예시에서 아래와 같이 생성자를 만든 후 , 재 할당을 가정해보자.
//이 것은 외부에서 나의 오브젝트의 상태를 유효하지 않은 상태로 만들 수 있는 상황이다. => private돌려서 막을 수 있다.
const maker = new CoffeeMaker(32);
maker.coffeeBeans = -34; // invalid 외부에서 임의로 변경하게 됨..

// 멤버 변수가 private으로 설정됐기 때문에 접근이 불가능하다.

Getter & Setter

  • getter와 setter는 일반 멤버 변수처럼 사용이 가능하나 어떠한 계산을 해야 할때 유용하게 활용이 가능하다.
  • 멤버 변수의 값을 변경하거나 설정할 때 조금 더 유연하게 사용이 가능하다.
  • 유연하게 사용함과 동시에 프로퍼티 값을 원하는 대로 통제할 수 있다.
  • 유저의 firstName과 lastName을 받아서 fullName을 만들어주는 User라는 클래스가 있다.
//아래와 같은 유저 클래스를 만들고 이름을 출력하는 생성자 함수를 만들었다고 가정하자!
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
user.firstName = 'No War!'; 
console.log(user.fullName); // 우리의 의도는 성을 바꾸고 싶었으나, 동일하게 처음 생성된 Steve가 나오게 됨
=> 해결방법으로 getter가 있다.

Getter

  • Getter는 멤버 변수의 값을 받아서 새로운 값을 출력할 때 유용한다.
class User {
  firstName: string;
  lastName: string;
  get fullName(): string { //get이라는 키워드를 활용하면 함수형태처럼 얼핏 보이지만 
                           //접근할때는 멤버 변수에 접근하는 것처럼 아래에서 접근이 가능하다.
    return `${this.firstName} ${this.lastName}`;
  }
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}
const user = new User('Steve', 'Jobs');
console.log(user.fullName); // Steve Jobs
user.firstName = 'No War!'; 
console.log(user.fullName); // "No War Jobs"

멤버 변수를 private으로 만들면서 코드량 줄이기

  • 위의 코드를 더욱 간단하게 private를 사용해서 아래와 같이 만들 수 있음
class User {
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`; 
  }
  constructor(private firstName: string, private lastName: string) {} //lastname을 public으로 변경도 가능
}

getter와 setter을 활용한 예시

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) {
      throw new Error('age should be greater than 0');
    }
    this.internalAge = num;
  }
  constructor(private firstName: string, private lastName: string) {}
}
const user = new User('Steve', 'Jobs');
user.age = 6; //internalAge에서 4로 할당되어 4이지만 6으로 할당을 다시 재할당하여 user age를 업데이트를 할 수 있다.

2. Abstraction(추상화)

외부에서 어떤 형태로 공통적으로 이 클래스를 이용할 수 있도록 해줄 것인가를 고민하는 단계라고 볼 수 있다.

  • 객체지향에서의 추상화는 어떤 하위클래스들에 존재하는 공통적인 메소드를 인터페이스로 정의하는것을 예로 들 수 있다.

  • 접근 지정자(private) 사용하는 방법과 interfacef를 이용하여 추상화하는 방법이 있다.

  • 아래의 예시를 통해 알아보자.

2-1. 제어자를 통한 추상화 방법

  • 아래의 예시를 통해 알아보자 .
  • coffeMaker(커피머신)은 커피를 추출하는 과정이 있고 그 과정에는 여러 필요한 스텝이 있다.
  • 그러기 위해서 우리는 클래스 내부의 여러가지 메서드를 만들었다. 하지만 사용자는 makeCoffee만 사용하면 되고, 그 내부에 포함하고 있는 grindBeans(), preHeat(), extract()는 존재조차 모르게 하고 싶다면 어떻게 해야할까?
type CoffeeCup = {
  shots: number;
  hasMilk: boolean;
};

//coffeMaker(커피머신)은 커피를 추출하는 과정이 있고 그 과정에는 여러 필요한 스텝이 있다. 
 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;
    }

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

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

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

    // coffee 만드는 과정에는 여러 스탭이  필요 , 커피 콩을 분쇄하고 따듯하게 데운 물로 샷을 추출하는 과정이 있을 것이다.
   	// grindBeans , preHeat, extract 등의 내부 메서드가 필요하다. 
    makeCoffee(shots: number): CoffeeCup {
      this.grindBeans(shots);
      this.preheat();
      return this.extract(shots);
    }
  }

  const maker = CoffeeMaker.makeMachine(32);
  maker.fillCoffeeBeans(32);

const maker: CoffeeMachine = new CoffeeMachine(32); // (3)
maker.fillCoffeeBeans(10);
maker.makeCoffee(2);

const maker2: CoffeeMaker = new CoffeeMachine(32); // (4)
maker2.fillCoffeeBenas(32);
maker2.makeCoffee(2);

사용자가 커피를 추출할 때, interface를 통해 커피를 만드는 makeCoffee를 통해 바로 추출 하는 부분만 사용하게 끔 하고 싶다면, 아래처럼 할 수 있다. private를 해줌으로써 은닉화 할 수 있다. 사용자로 하여금 maker라는 클래스를 보면 , fillCoffeeBeans와 makeCoffee를 사용자에게 보여줄 수 있다.

2-2 Interface를 활용한 방법

  1. interface 의 정의 해준다. ( 나는 이러한 행동을 할 수 있어~ 라고 적혀있는 계약서 같은 아이라고 할 수 있다.)
  2. 클래스가 해당 interface를 수행한다는 의미의 implements와 원하는 interface를 입력한다.
type CoffeeCup = {
  shots: number;
  hasMilk: boolean;
};

interface CoffeeMaker { //interface 앞에  interface ICoffemaker라고 붙이며 사용하는 사람도 있고, class에 I를 붙이거나 혹은 구현하는 클래스에서 다른 이름을 가져가는 방법도 있다.
  // 1 
  makeCoffee(shots: number): CoffeeCup;
}

class CoffeeMachine implements CoffeeMaker {      // 2
  private static BEANS_GRAMM_PER_SHOT: number = 23;
  private coffeeBeans: number = 0;

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

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

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

  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.makerMachine(32) // (3)
maker.fillCoffeeBeans(10);
maker.makeCoffee(2);

const maker2: CoffeeMaker = new CoffeeMachine(32); // (4)
maker2.fillCoffeeBenas(32); // 이경우에는 interface에 없는 함수로 물결표시가 뜨며 사용할 수가 없다.
maker2.makeCoffee(2);

위에서 한 코드에 덧 붙여서 구체적으로 조금더 interface를 활용한 예시는 아래와 같다.

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

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

  // 상업적인 커피머신이라고 생각해서 만들어주면, 커피 기계청소과 커피 콩도채워주고 커피를 내려주는 기능이 있다고 해보자.
  interface CommercialCoffeeMaker {
    makeCoffee(shots: number): CoffeeCup;
    fillCoffeeBeans(beans: number): void;
    clean(): void;
  }

  // 2가지 interface를 따라가도록 implements를 통해 확장 할 수 있다.
  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);
    }
  }

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

 --------------------------------------------------------------------
  const maker: CoffeeMachine = CoffeeMachine.makeMachine(32); // coffeMachine내의 모든 public함수에 접근가능
  	maker.fillCoffeeBeans(10);
		maker.makeCoffee(2);
  
  // CommercailCoffeMaker의 interface의 public만 접근가능
  const maker2: CommercialCoffeeMaker = CoffeeMachine.makeMachine(32); 
  	maker2.fillCoffeeBeans(10);
		maker2.makeCoffee(2);
  	maker2.clean();
  ------------------------------------------------------------------
  //interface의 또 다른 예시 아마추어와 커피 클래스를 생성하여 그 메서드롤 할당해줄 수 있음
  //interface에서 규약된 좁은 범위의 함수만 접근이 가능하게 끔 해줄 수 있음 아마추어와 프로로 클래스를 생성.
  const amateur = new AmateurUser(maker);
  const pro = new ProBarista(maker);
  amateur.makeCoffee();
  pro.makeCoffee();   // 프로의 사용자는 기계도 청소하고 커피도 뽑아 낼수 잇는 것을 확인 가능
}
profile
문과생 개발자되다

0개의 댓글