728x90

Inhertiance(상속)의 문제를 해결하기위해 Composition(구성)을 사용한다.

상속은 다음과 같은 문제점이 있다.

  1. 상속의 깊이가 깊어어질수록 관계가 복잡해진다.
  2. 상속은 두 부모로부터 받을 수 없다.

Favor Composition Over Inheritance

상속이 나쁜것은 아니지만 확장이 가능하고 더 유연한 Composition을 적극적으로 사용해보도록 하자.

 


 

의존성(Dependency Injection) 주입

Composition에 대해 알아보기 이전에 옳은 Composition을 구현하기 위해 의존성의 문제에 대해서도 알아보자

의존성이란 어느 한 클래스가 다른 클래스를 참조하면서 생기는 두 클래스간의 연결이다.

의존성이 생긴 Class의 예시는 다음과 같다.

class PokemonMaster {
    private MonsterBall monsterBall; // MonsterBall 클래스에 의존성이 생긴다.

    public PokemonMaster() {
        this.monsterBall = new MonsterBall(); 
    }

    public throwMonsterBall() {
        this.monsterBall.gotcha(); //잡았다
        ...
    }
}

위의 코드는 PokemonMaster 클래스가 MonsterBall 클래스의 의존하고 있다.

만약 MonsterBall이 아닌 SuperBall이나 HyperBall을 던지고 싶다면 어떻게 해야할까?

PokemonMaster 클래스를 여러개 받거나 constructor 내부에서 몬스터볼의 instance를 종류대로 생성해야할까?

또한 MonsterBall 클래스의 함수가 변경되면 매번 PokemonMaster 클래스도 수정해 줘야하는걸까?

결합도가 너무 높아 이만저만 문제가 아니다

 

이 의존성 문제를 해결하기위해 Composition을 이용한 의존성 주입의 개념을 가져온다.

  • Unit Test가 용이해진다.
  • 코드의 재활용성을 높여준다.
  • 객체 간의 의존성(종속성)을 줄이거나 없엘 수 있다.
  • 객체 간의 결합도이 낮추면서 유연한 코드를 작성할 수 있다.

의존성 주입의 기본 개념은

constructor의 매개변수의 타입을 interface또는 최상단 class로 하여

instance 생성할때 다른 클래스와 결합이 이뤄지도록 한다는 것이다.

 

위의 예시를 의존성 주입으로 해결해보면 다음과 같다.

interface PokemonBall{
  gotcha():void;
}

class MonsterBall implements PokemonBall{
  gotcha(){
    console.log("monsterBall");
  }
};
class SuperBall implements PokemonBall {
  gotcha(){
    console.log("superBall");
  }
};

class PokemonMaster {
    private pokemonBall: PokemonBall; // MonsterBall 클래스에 의존성이 생긴다.

    constructor(pokemonBall:PokemonBall) {
        this.pokemonBall = pokemonBall; 
    }

    public throwMonsterBall() {
        this.pokemonBall.gotcha(); //잡았다
    }
}

const Red = new PokemonMaster(new MonsterBall());
const Blue = new PokemonMaster(new SuperBall());

Red.throwMonsterBall();
Blue.throwMonsterBall();

이제는 MonsterBall, SuperBall뿐만 아니라 MasterBall이 추가되어도 PokemonMaster가 사용할 수 있게 될 것이다.

728x90

Polymorphism(다형성)은 같은 데이터 타입의 함수에서 다른 기능이 발현되는 현상을 뜻한다.

polymorphism의 경우는 다음 2가지가 있다.

  1. static
  2. dynamic

먼저 static polymorphism의 경우는 method overloading에 있다.
하나의 클래스에서 같은 변수명의 함수를 return 타입 또는 input parameter 타입만 다르게하여 다른 기능을 만드는 것이다.
예시 코드는 다음과 같다.

cfunction add(a: string, b: string): string {
  console.log(a + b);
  return a + b;
}

function add(a: number, b: number): number {
  console.log(a + b);
  return a + b;
}

add("Hello ", "Steve"); // returns "Hello Steve"
add(10, 20); // returns 30

Function overloading with different number of parameters and types with same name is not supported.


dynamic polymorphism의 경우는 method overriding에 있다.
상속 관계에 있는 두개의 클래스에서 같은 변수명 같은 return 타입 같은 input parameter의 경우에도 다른 기능을 나타낼 수 있다.
같은 interface를 상속받는 다른 클래스의instance를 각각 대입받은 두 변수의 경우
이런 상황이 나타나게 된다.

interface Calculate {
  add(a: number, b: number): void;
}

class CalculateImpl1 {
  add(a: number, b: number): void {
    console.log(a + b);
  }
}

class CalculateImpl2 {
  add(a: number, b: number): void {
    console.log(a + b + "!!!!!");
  }
}

const myCalculate: Calculate = new CalculateImpl1();
myCalculate.add(1, 2);

const yourCalculate: Calculate = new CalculateImpl2();
yourCalculate.add(1, 2);
728x90

interface와 유사하지만 클래스 기능의 제한의 목적보다는(할 수는 있지만)

불필요하게 중복되는 기능의 구현을 최소화하기 위해서 사용한다.

 

이번에는 파티를 만드는 예시보다 포켓몬의 진화의 예시가 좋을 것 같다.

포켓몬은 진화단계가 있다면 특정 레벨이상에서 레벨이 오를때 진화를 할 수 있으며 최종진화의 경우 더 이상 진화를 할 수 없다.


구현 예시

귀여운 치코리타의 진화로 예시를 들겠다. 치코리타의 진화 체계는 치코리타 -> 베이리프 -> 메가니움 순서로 이루어진다.

치코리타 -> 베이리프로는 레벨 16때 진화하며

베이리프 -> 메가니움으로는 레벨 32때 진화한다.

 

치코리타의 레벨을 쭉쭉 올려보자!

class Pokemon {
  constructor(
    protected name: string,
    protected level: number,
    protected levelToEvolve: number = 0
  ) {}

  protected isLevelToEvolve() {
    return this.levelToEvolve != 0 && this.level >= this.levelToEvolve;
  }
  private evolve(): void {
    console.log(`${this.name} 진화! 레벨: ${this.level}`);
  }
  levelup(): Pokemon {
    if (this.level <= 100) this.level++;
    if (this.isLevelToEvolve()) {
      this.evolve();
    }
    return new Pokemon(this.name, this.level, this.levelToEvolve);
  }
}

class Chikorita extends Pokemon {
  constructor(level: number) {
    console.log("치코리타 탄생");
    super("치코리타", level, 16);
  }

  levelup(): Pokemon {
    super.levelup();
    if (super.isLevelToEvolve()) {
      return new Bayleef(this.level);
    } else {
      return this;
    }
  }
}

class Bayleef extends Pokemon {
  constructor(level: number) {
    console.log("베이리프 탄생");
    super("베이리프", level, 32);
  }
  levelup(): Pokemon {
    super.levelup();
    if (super.isLevelToEvolve()) {
      return new Meganium(this.level); // super.level == undefined
    } else {
      return this;
    }
  }
}

class Meganium extends Pokemon {
  constructor(level: number) {
    console.log("메가니움 탄생");
    super("메가니움", level);
  }
}

let myChikorita: Chikorita = new Chikorita(15);
for (let i = 0; i < 50; i++) {
  myChikorita = myChikorita.levelup();
/* 
   치코리타 탄생
   치코리타 진화! 레벨:16
   베이리프 탄생
   베이리프 진화! 레벨:32
   메가니움 탄생
*/
}

super

  • 현재 instance의 부모 클래스를 의미한다
    • super.name을 출력시 부모 클래스에 정의된 name이 없으므로 undefined가 출력된다. 만약 super.name을 출력하고 싶다면 부모 클래스에서 name 정의시 static으로 정의하면 된다.
  • 부모 클래스의 constructor를 호출시 사용한다.

this

  • 현재 instance의 클래스를 의미한다
  • 현재 클래스의 constructor를 호출시 사용한다.
728x90

이전 코드에서 봤을때 외부에서 클래스를 봤을 때 사용할 수 있는 함수가 너무 많아 사용이 불편했다.

이를 해결해 줄 수 있는 개념이 바로 abstraction(추상화)이다.

추상화의 의의는 개발자가 코드를 보다 쉽게 이해할 수 있도록 돕는데 있다.

클래스 외부에서 사용할 함수에 네이밍과 매개변수 사용의 제한을 주어(다형성 적용시)

여러 클래스가 존재하더라도 같은 이름과 사용방법으로 같은 기능을 구현할 수 있다.

interface를 클래스에 implement하여 해당 클래스의 조상을 설정함과 동시에 사용할 함수, 변수를 제한할 수 있다.


이전코드

type Pokemon = {
  name:string;
  level: number;
};
const pokemonArr : Pokemon[] = [{name:'이상해꽃'
                                 level:50},
                                {name:'리자몽'
                                 level:50},
                                {name:'거북왕',
                                 level;50}
                               ];
type Party= {
  pokemon : integer; //파티 포켓몬수
  hasLegendary:boolean; // 전설의 포켓몬 포함여부
};

class WeakParty{
  private static MINIMUM_LEVEL_REQUIRED : number = 50;
  private pokemon : Pokemon[];
  private constructor(pokemon:readonly Pokemon[]){
    this.pokemon = pokemon;
  }

  get pokemon(): Pokemon[]:{
      return pokemon;
  }

  set pokemon(pokemon : Pokemon[]){
    this.pokemon = pokemon;
  }

  public fillPokemon(pokemon : Pokemon){
    if(pokemon == null) throw new Error("no pokemon to fill");
    this.pokemon.push(pokemon);
  }

  public makeInstance(pokemon: Pokemon[]){
    return new WeakParty(pokemon);
  }

  public makeWeakParty():Party{
    if(pokemon.length()<1){
      throw new Error("no pokemon to participate");
    }
    pokemon.forEach((item)=>{
      if(item.level<WeakParty.MINIMUM_LEVEL_REQUIRED){
        throw new Error("too low level");
      }
    })
    return{
      pokemon.length(),
      hasLegendary:false
    }
  }
}


//const myParty = new WeakParty(pokemonArr); // 이제 작동하지 않음
const myParty = WeakParty.makeWeakParty(pokemonArr);
console.log(myParty.makeWeakParty()); // {3,false}

myParty.fillPokemon({name:'피카츄'
                     level:50})
console.log(myParty.makeWeakParty); // {4,false}

일반대회는 파티의 멤버를 추가하여 수정이 가능하지만

정식 대회는 이미 파티로 선정한 포켓몬에서 추가할 수 없다.

진짜 포켓몬을 대체하여 수정을 하려면 코드가 좀 복잡해지므로 파티를 추가하는 경우만 생각하자 (fillPokemon())


추상화를 적용한 코드

{
  type Pokemon = {
    name: string;
    level: number;
  };
  const pokemonArr: Pokemon[] = [
    { name: "이상해꽃", level: 50 },
    { name: "리자몽", level: 50 },
    { name: "거북왕", level: 50 }
  ];
  type Party = {
    pokemon: number; //파티 포켓몬수
    hasLegendary: boolean; // 전설의 포켓몬 포함여부
  };

  //interface 생성
  interface Rank {
    makeParty(): Party;
    fillPokemon(pokemon: Pokemon): void;
  }

  interface Competition {
    makeParty(): Party;
  }

  // 추상 클래스 생성한
  abstract class PartyImpl implements Rank, Competition {
    constructor(protected pokemon: Pokemon[]) {}

    public fillPokemon(pokemon: Pokemon) {
      if (pokemon == null) throw new Error("no pokemon to fill");
      this.pokemon.push(pokemon);
    }
    public abstract makeParty(): Party; // 자식 클래스마다 내용이 달라질 수 있다.
  }

  class WeakParty extends PartyImpl {
    private static MINIMUM_LEVEL_REQUIRED: number = 50;
    constructor(pokemon: Pokemon[]) {
      super(pokemon);
    }
    public makeInstance(pokemon: Pokemon[]) {
      return new WeakParty(pokemon);
    }
    public makeParty(): Party {
      if (this.pokemon.length < 1) {
        throw new Error("no pokemon to participate");
      }
      this.pokemon.forEach(item => {
        if (item.level < WeakParty.MINIMUM_LEVEL_REQUIRED) {
          throw new Error("too low level");
        }
      });
      return {
        pokemon: this.pokemon.length,
        hasLegendary: false
      };
    }
  }

  const partyForRank: Rank = new WeakParty(pokemonArr.slice());
  const partyForCompetition: Competition = new WeakParty(pokemonArr.slice());

  partyForRank.fillPokemon({ name: "피카츄", level: 50 });
  console.log(partyForRank.makeParty()); //{pokemon:4,legendary:false}

  //partyForRank.fillPokemon({name:'피카츄',level:50}); //불가능
  console.log(partyForCompetition.makeParty()); //{pokemon:3,legendary:false}
}

interface

  • 구현이 되지않은 class라 생각하면 편하다. 미처 구현이 되지 못한 부분은 상속받는 class에서의 구현 내용을 따라간다.
  • 생선된 변수의 타입을 interface로 지정하면 class의 구조에 상관없이 interface내의 함수,변수만 사용할 수 있다.

implements

  • inherit과 유사한 개념으로 interface와 class를 상속관계로 맺어준다.
  • 한 class에 여러 interface를 상속시킬 수 있다.

abstract class

  • 자식 클래스에서 꼭 구현을 해야하는 함수가 있는데 사용법을 다르게 하거나 놓칠 수 있는 위험이 있는 경우 사용한다.
  • abstract class 자체로는 intance를 만들 수 없다.
  • 자식 클래스마다 달라질 수 있는 행동에 대해 abstract class 내 method 앞에 abstract를 붙인다.
    이 때 interface 처럼 해당 함수에는 구현 사항을 작성해서는 안되며 자식 클래스에서는 꼭 구현이 되어야한다.
728x90

이제 Encapsulation(캡슐화)에 대하여 알아보고 이전코드에 적용하도록 해보자.

캡슐화란 클래스 내에서 수정하거나 보여서는 안될 중요한 데이터를 보호할 수 있게 만드는 개념이다.

기본적으로 class에서 선언되는 변수는 모두 public 변수인데 이 옵션외에

protected,private를 적용하면 클래스 내 변수가 클래스 외부 코드에서 마음대로 변경되거나 참조되는 것을 막을 수 있다.

 


 

이전코드

type Pokemon = {
  name:string;
  level: number;
};
const pokemonArr : Pokemon[] = [{name:'이상해꽃'
                                 level:50},
                                {name:'리자몽'
                                 level:50},
                                {name:'거북왕',
                                 level;50}
                               ];
type Party= {
  pokemon : integer; //파티 포켓몬수
  hasLegendary:boolean; // 전설의 포켓몬 포함여부
};

class WeakParty{
  static MINIMUM_LEVEL_REQUIRED : number = 50;
  pokemon : Pokemon[];

  constructor(pokemon:readonly Pokemon[]){
    this.pokemon = pokemon;
  }

  makeWeakParty(pokemon:number):Party{
    if(pokemon.length()<1){
      throw new Error("no pokemon to participate");
    }
    pokemon.forEach((item)=>{
      if(item.level<WeakParty.MINIMUM_LEVEL_REQUIRED){
        throw new Error("too low level");
      }
    })
    return{
      pokemon,
      hasLegendary:false
    }
  }
}


const myParty = new WeakParty(pokemonArr);
console.log(myParty.makeWeakParty);

 


 

캡슐화를 적용한 코드

type Pokemon = {
  name:string;
  level: number;
};
const pokemonArr : Pokemon[] = [{name:'이상해꽃'
                                 level:50},
                                {name:'리자몽'
                                 level:50},
                                {name:'거북왕',
                                 level;50}
                               ];
type Party= {
  pokemon : integer; //파티 포켓몬수
  hasLegendary:boolean; // 전설의 포켓몬 포함여부
};

class WeakParty{
  private static MINIMUM_LEVEL_REQUIRED : number = 50; // class level에서 생성하여 instance마다 생성을 안하기때문에 메모리 소비 감소
  private pokemon : Pokemon[]; // instance level
  private constructor(pokemon:readonly Pokemon[]){
    this.pokemon = pokemon;
  }

  get pokemon(): Pokemon[]:{
      return pokemon;
  }

  set pokemon(pokemon : Pokemon[]){
    this.pokemon = pokemon;
  }

  public fillPokemon(pokemon : Pokemon){
    if(pokemon == null) throw new Error("no pokemon to fill");
    this.pokemon.push(pokemon);
  }

  public makeInstance(pokemon: Pokemon[]){
    return new WeakParty(pokemon);
  }

  public makeWeakParty(pokemon:number):Party{
    if(pokemon.length()<1){
      throw new Error("no pokemon to participate");
    }
    pokemon.forEach((item)=>{
      if(item.level<WeakParty.MINIMUM_LEVEL_REQUIRED){
        throw new Error("too low level");
      }
    })
    return{
      pokemon.length(),
      hasLegendary:false
    }
  }
}


//const myParty = new WeakParty(pokemonArr); // 이제 작동하지 않음
const myParty = WeakParty.makeWeakParty(pokemonArr);
console.log(myParty.makeWeakParty); // {3,false}

myParty.fillPokemon({name:'피카츄'
                     level:50})
console.log(myParty.makeWeakParty); // {4,false}
   

protected, private

  • protected : 상속관계에서 자식에게까지 공유된다.
    private : 해당 클래스내부까지만 공유된다
  • static 변수, 일반 변수, 함수에 적용할 수 있다.

getter, setter

  • 클래스내에서 private,protected 변수의 반환과 변환을 위한 get,set 함수가 있다.
728x90

개요

typescript로 Object Orient Programing(OOP)을 공부해 보자.
OOP의 주제로는 크게

  • encapsulation (캡슐화)
  • abstraction (추상화)
  • inheritance (상속)
  • polymorphism (다형성)
  • composition (포함)

위의 5가지에 대해 각각이 언제 쓰이고 어떻게 쓰이는지 예시와 함께 알아볼 것이다.

예시는 포켓몬스터 소드실드의 랭크게임의 파티짜기를 가지고 코드를 작성해 보도록 하겠다.

포켓몬스터 소드실드의 랭크게임을 모르는 사람을 위해 간략히 설명하자면

랭크게임을 하기위해선 1마리 이상 6마리 이하의 포켓몬의 파티를 꾸리게 된다.

그리고 이 6마리 포켓몬안에는 최대 1마리의 전설의 포켓몬을 가질 수 있다.(1전설 + 5일반 or 6일반)




class 없이

객체 지향으로 들어가기전에 객체 지향이 얼마나 좋은 개념인지 비교하기 위해 class를 사용하지 않은 절차 지향으로 구현해보자.

type Pokemon = {
  name:string;
  level: number;
};
const MINIMUM_LEVEL_REQUIRED : number = 50;
const pokemonArr : Pokemon[] = [{name:'이상해꽃'
                                 level:50},
                                {name:'리자몽'
                                 level:50},
                                {name:'거북왕',
                                 level;50}
                               ];
type Party= {
  pokemon : integer; //파티 포켓몬수
  hasLegendary:boolean; // 전설의 포켓몬 포함여부
};

funcion makeWeakParty(pokemon:readonly Pokemon[]): Party{
  if(pokemon.length()<1){
    throw new Error("no pokemon to participate");
  }
  pokemon.forEach((item)=>{
      if(item.level<MINIMUM_LEVEL_REQUIRED){
      throw new Error("too low level");
    }
  })
  return{
    pokemon,
    hasLegendary:false
  }
}

const myParty = makeWeakParty(pokemonArr);
console.log(myParty);



class 있이

이제 class 개념을 도입하여 위의 코드를 바꿔보도록 하자

type Pokemon = {
  name:string;
  level: number;
};
const pokemonArr : Pokemon[] = [{name:'이상해꽃'
                                 level:50},
                                {name:'리자몽'
                                 level:50},
                                {name:'거북왕',
                                 level;50}
                               ];
type Party= {
  pokemon : integer; //파티 포켓몬수
  hasLegendary:boolean; // 전설의 포켓몬 포함여부
};

class WeakParty{
  static MINIMUM_LEVEL_REQUIRED : number = 50; // class level에서 생성하여 instance마다 생성을 안하기때문에 메모리 소비 감소
  pokemon : number; // instance level
  constructor(pokemon:readonly Pokemon[]){
    this.pokemon = pokemon;
  }
  makeWeakParty(pokemon:number):Party{
    if(pokemon.length()<1){
      throw new Error("no pokemon to participate");
    }
    pokemon.forEach((item)=>{
      if(item.level<WeakParty.MINIMUM_LEVEL_REQUIRED){
        throw new Error("too low level");
      }
    })
    return{
      pokemon,
      hasLegendary:false
    }
  }
}


const myParty = new WeakParty(pokemonArr);
console.log(myParty.makeWeakParty);

readonly

  • const,let을 선언할 수 없는 매개 변수, class 내 변수, type내 변수를 읽기 전용으로 만들어준다.

  • 사용방법

    1. parameter

      function func(arr : readonly string[]){ 
         console.log(arr); 
      }
    2. class variable && type variable

      class myClass{ 
         readonly name : string = "bill"; // 외부에서 참조 가능하지만 수정불가능상태 
      }
      
      type myType{ 
         readonly name : string; // myType으로 정의된 데이터의 name은 참조는 가능하지만 변경이 불가능하다. 
      }

static

  • class level에서 생성되는 변수. 인스턴스가 아무리 많이 생성되어도 메모리는 한곳에만 저장되어 메모리 낭비를 줄일 수 있다.

아직 class를 도입한 코드가 더 좋다고 딱히 말할 수는 없다.

하지만 이후에 나오는 encapsulation, abstraction, inheritance, polymorphism, composition을 도입하면 확장성이 얼마나 좋아지는지 알 수 있을것이다.

+ Recent posts