개발일지
[TS] 4. 객체지향 프로그래밍 본문
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 levelpublic, 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 |