객체지향의 원칙 4가지를 적용해보며 알아보자.
지난 oop-programing-1에서의 커피머신을 정보를 은닉화해서 캡슐화 하는 과정을 통해 encapsultation에 대해서 알아보자.
객체지향의 원칙 중 하나로 특정 키워드를 사용함으로서 정의한 클래스 외부에서 클래스 내부의 메서드나 멤버변수, 프로퍼티의 접근을 막기 위해 사용한다.
캡슐화라고도 부른다. 캡슐화를 위한 키워드는 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으로 설정됐기 때문에 접근이 불가능하다.
//아래와 같은 유저 클래스를 만들고 이름을 출력하는 생성자 함수를 만들었다고 가정하자!
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가 있다.
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"
class User {
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
constructor(private firstName: string, private lastName: string) {} //lastname을 public으로 변경도 가능
}
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를 업데이트를 할 수 있다.
외부에서 어떤 형태로 공통적으로 이 클래스를 이용할 수 있도록 해줄 것인가를 고민하는 단계라고 볼 수 있다.
객체지향에서의 추상화는 어떤 하위클래스들에 존재하는 공통적인 메소드를 인터페이스로 정의하는것을 예로 들 수 있다.
접근 지정자(private) 사용하는 방법과 interfacef를 이용하여 추상화하는 방법이 있다.
아래의 예시를 통해 알아보자.
- 아래의 예시를 통해 알아보자 .
- 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를 사용자에게 보여줄 수 있다.
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(); // 프로의 사용자는 기계도 청소하고 커피도 뽑아 낼수 잇는 것을 확인 가능
}