객체지향의 원칙 중 하나인 inheritance와 polymorphism에 대해 알아보자.
지난 oop-programing-2이어서 상속에 대해 예시와 함꼐 알아보자.
class caffeLatteMachine {
private static BEANS_GRAMM_PER_SHOT: number = 23;
private coffeeBeans: number = 0;
private constructor(coffeeBeans: number) {
this.coffeeBeans = coffeeBeans;
}
... makeMaching 메서드...
makeCoffee(shots: number): CoffeeCup {
this.grindBeans(shots);
this.preheat();
const coffee = this.extract(shots);
return {...coffee, hasMilk:true};
}
}
위에 코드를 보면 그전에 만든 머신에서 constructor 뿐만 아니라 makeMachine 등의 많은 부분이 중복이 되어 있다
상속을 이용하게 되면 아래처럼 간단하게 접근이 가능하다.
{
type CoffeeCup = {
shots: number;
hasMilk: boolean;
};
interface CoffeeMaker {
makeCoffee(shots: number): CoffeeCup;
}
class CoffeeMachine implements CoffeeMaker { //interface를 구현 할땐 implements를 사용
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);
}
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 CaffeLatteMachine extends CoffeeMachine { //extends 를 통해 다른 클래스를 상속할 수 있다.
//자식에서 따로 constructor를 따로 구현하는 경우 super를 꼭 호출애햐한다.
//부모 클래스에서 필요한 beans를 super를 통해 전달 해주고 그 뒤에 자식에서 사용하는 인자를 넣어서 만 들 수 있다.
constructor(beans: number, public readonly serialNumber: string) { //라떼기계의 경우에는 시리얼 넘버가 따로 존재하게됨
super(beans);
}
private steamMilk(): void {
console.log("Steaming some milk... 🥛");
}
makeCoffee(shots: number): CoffeeCup {
// super를 이용하면 부모 클래스의 함수를 호출하거나 접근 가능.(공통적인 기능은 제사용하면서 자식 클래스에 특화된 기능을 추가 할 수 있음)
// 이 경우 커피를 만드는 절차를 다 거치고, 그 뒤에 우유만 데워서 넣어서 주는 경우로 아래와 같이 활용이 가능하다.
const coffee = super.makeCoffee(shots);
this.steamMilk();
return {
...coffee,
hasMilk: true,
};
}
}
const machine = new CoffeeMachine(23); //상속이 없는 경우!
const latteMachine = new CaffeLatteMachine(23, "latte123");
const coffee = latteMachine.makeCoffee(1);
console.log(coffee);
console.log(latteMachine.serialNumber);
}
다형성을 이용하면 한 가지의 클래스나 인터페이스를 통해 다양한 방식으로 구현한 클래스를 만들 수 있다.
{
type CoffeeCup = {
shots: number;
hasMilk?: boolean;
hasSugar?: boolean;
};
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
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 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,
};
}
}
-------------------------
const machines = [ // - (3)
new CoffeeMachine(16),
new CaffeLatteMachine(16, '1'),
new SweetCoffeeMaker(16),
]
-------------------
// CoffeeMaker라는 공통적인 인터페이스의 배열로 만들 수 있음.
const machines: CoffeeMaker[] = [ // (2)
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); // (1) makeCoffee라는 공통된 api 메소드를 호출 할 수 있다.
// (2)의 경우에는 한가지 makeCoffee()라는 메소드만 활용이 가능하다. 인터페이스에 규약된 함수하나만 호출이 가능하다.
// (3)의 경우에는 clean, makecoffee, fillCoffeeBeans 메소드 등을 사용 할 수 있다.
});
}