5장 클래스와 인터페이스

2021. 07. 22

5.1 클래스와 상속

  • 타입 스크립트는 클래스의 프로퍼티와 메서드에 세 가지 접근 한정자를 제공한다

    • public
    • 어디에서나 접근 가능
    • protected
    • protected 클래스와 서브클래스의 인스턴스 내부 에서만 접근 가능
    • private
    • priavte을 사용한 클래스의 인스턴스 내부 에서만 접근 가능
  • abstract

    • 어떤 클래스가 인스턴스를 직접 생성하지 못하게 막고, 상속 받은 클래스를 통해서만 인스턴스화 할 수 있도록 허용한 것
    • 직접 인스턴스화 시도시 에러 발생
type Color = "Black" | "White"

type Tile = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H"
type Rank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8

class Position {
  constructor(private tile: Tile, private rank: Rank) {}
}

abstract class Piece {
  protected position: Position
  constructor(
    private readonly color: Color,
    private tile: Tile,
    private rank: Rank
  ) {
    this.position = new Position(tile, rank)
  }
}

new Piece("Black", "A", 1) // TS2511
  • abstract 내부에서 메서드를 구현 할 수도 있으며, 필요한 메서드를 추상 메서드로 구현 할 수 있다.
abstract class Piece {
  // ....
  moveTo(position: Position) {
    this.position = position
  }

  abstract canMoveTo(position: Position): boolean
}
  • abstract 를 사용함으로써, canMoveTo 메서드를 주어진 시그니처와 호환되도록 구현함을 알린다. 그래서 이 Pieceextends 해서 사용하게 될 경우 canMoveTo 메서드를 구현하도록 강제한다.
  • moveTo 의 경우 기본 구현을 포함하며, 필요시 서브 클래스에서 오버라이드 할 수 있다.

5.2 super

  • 해당 키워드를 사용해서 자식 클래스에서 부모 클래스의 메서드를 호출 할 수 있다.
  • 생성자 함수에서 사용하는 super() 의 경우 생성자를 호출한다.
  • super 키워드로 부모의 프로퍼티에는 접근 할 수 없다. 메서드만 접근 가능하다.

5.3 Interface

  • interfacetype 키워드의 차이

    1. type alias는 더 일반적이므로, type alias 의 오른편에는 타입 표현식(type, |, &) 을 포함한 모든 타입이 등장 할 수 있으나, interface 의 오른쪽에는 형태가 나와야 한다.
    type A = number
    type B = A | string
    interface A {
    good(x: number): string
    bad(x: number): string
    }
    
    interface B extends A {
    good(x: number | string): string
    bad(x: string): string // error TS2430
    }
    1. interface를 상속할 때 tsc는 상속받는 interface의 타입에 상위 interface를 할당할 수 있는지를 확인한다.
    2. 이름과 범위가 같은 인터페이스가 여러개 있다면 이는 합쳐지게 된다. 그러나 type alias는 컴파일 에러가 난다.
  • Implements

    • 클래스 선언 시 implements 키워드를 사용하여 특정 인터페이스를 만족 시키는 것을 표현할 수 있다.
interface Animals {
  readonly name: string
  eat(food: string): void
  sleep(hours: number): void
}

class Cat implements Animals {
  readonly name: string
  constructor(name: string) {
    this.name = name
  }
  eat(food: string) {
    console.info("ate", food)
  }
  sleep(hours: number) {
    console.info("sleep", hours)
  }
}
  • Interface를 Implement한 class는 Interface에서 선언한 것들을 모두 구현해야 한다.
  • Interface로 instance property를 정의할 수 있으나, 접근 제어자는 선언할 수 없으며, static 키워드도 사용할 수 없다.

5.5 Structure base type

class Zebra {
  trot() {}
}

class Poodle {
  trot() {}
}

function ambleAround(animal: Zebra) {
  animal.trot()
}

ambleAround(new Poodle()) // OK
  • 함수의 관점에서 두 클래스 모두 .trot 을 구현하며 서로 호환되므로 아무 문제가 되지 않는다. 즉, 함수의 관점에서 구조적으로 동일한 클래스의 경우 같은 타입으로 인식하고 동작된다.

5.6 Class declares value and type

값과 타입은 타입스크립트에서 별도의 네임스페이스에 존재하게 된다. 그러나 classenumtype namespacetype 을, value namespacevalue 를 동시에 생성한다는 점을 기억해야 한다.

type State = {
  [key: string]: string
}

class StringDatabase {
  state: State = {}

  constructor(public state: State = {}) {}

  get(key: string): string | null {
    return key in this.state ? this.state[key] : null
  }

  set(key: string, value: string): void {
    this.state[key] = value
  }

  static from(state: State) {
    const db = new StringDatabase()
    for (let key in state) {
      db.set(key, state[key])
    }
    return db
  }
}

이 클래스 선언 코드는 Stringdatabase인스턴스 타입StringDatabase생성자 타입 이 두가지 타입을 만들게 된다.

interface StringDatabase {
  state: State
  get(key: string): string | null
  set(key: string, value: string): void
}

interface StringDatabaseConstructor {
  new (state?: State): StringDatabase
  from(state: State): StringDatabase
}

정리하면, 클래스 정의는 value와 type 둘다 생성하며, type 수준에서는 두 개의 interface 를 생성한다. 하나는 클래스의 인스턴스를 가리키며, 다른 하나는 typeof 로 얻을 수 있는 클래스 생성자 자체를 가리킨다.

5.7 Polymorphism

  • class도 generic을 지원한다. generic type의 범위는 classinterface 전체가 될 수도 있고, 특정 메서드로 한정할 수도 있다.
class MyMap<K, V> {
  constructor(initKey: K, initV: V) {
    // ...
  }
  get(key: K): V {
    // ..
  }
  set(key: K, value: V): void {
    // ..
  }
  merge<K1, V1>(map: MyMap<K1, V1>): MyMap<K | K1, V | V1> {
    // ..
  }
  static of<K, V>(k: K, v: V): MyMap<K, V> {
    // ..
  }
}
  • interface에서도 제네릭을 사용할 수 있다.
interface MyMap<K, V> {
  get(key: K): V
  set(key: K, value: V): void
}

5.8 Design Pattern

5.8.1 Mixin

function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach(baseCtor => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
      const baseCtorsName = Object.getOwnPropertyDescriptor(
        baseCtor.prototype,
        name
      )
      if (!baseCtorsName) return
      Object.defineProperty(derivedCtor.prototype, name, baseCtorsName)
    })
  })
}

class Disposable {
  isDisposed: boolean
  constructor(disposed: boolean = false) {
    this.isDisposed = disposed
  }
  dispose() {
    this.isDisposed = true
  }
}

// Activatable Mixin
class Actiavatable {
  isActive: boolean = false
  activate() {
    this.isActive = true
  }
  deactivate() {
    this.isActive = false
  }
}

class SmartObject {
  constructor() {
    setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500)
  }

  interact() {
    this.activate()
  }
}

interface SmartObject extends Disposable, Actiavatable {}
applyMixins(SmartObject, [Disposable, Actiavatable])

let smartObj = new SmartObject()
smartObj.dispose()
setTimeout(() => smartObj.interact(), 1000)

5.8.2 Factory

const BALLET = "BALLET"
const BOOT = "BOOT"
const SNEAKER = "SNEAKER"

type Shoes = "BALLET" | "BOOT" | "SNEAKER"

type Shoe = {
  purpose: string
}

class BalletFlat implements Shoe {
  purpose = "ballet"
}

class Boot implements Shoe {
  purpose = "woodcutting"
}

class Sneaker implements Shoe {
  purpose = "walking"
}

const ShoesFactory = (type: Shoes): Shoe => {
  switch (type) {
    case BALLET:
      return new BalletFlat()
    case BOOT:
      return new Boot()
    case SNEAKER:
      return new Sneaker()
  }
}

const boots = ShoesFactory(BALLET)

5.8.3 Builder

class RequestBuilder {
  private url: string | null = null
  private method: "get" | "post" | null = null

  setUrl(url: string): this {
    this.url = url
    return this
  }

  setMethod(method: "get" | "post"): this {
    this.method = method
    return this
  }
}
import { BULLET_STATE } from "../configs"
import Bullet from "./bullet"

export default class BulletFactory {
  constructor(scene) {
    this.scene = scene
    this.init()
  }
  init() {
    this.activeBulletArray = []
    this.inactiveBulletArray = []
  }

  getBullet() {
    let bullet = this.inactiveBulletArray.pop()
    if (bullet) {
      return bullet
    } else {
      this.createBullet()
      return this.getBullet()
    }
  }
  createBullet() {
    const bullet = new Bullet(this.scene)
    this.inactiveBulletArray.push(bullet)
  }
  moveBulletArray(bullet, _to) {
    if (_to === BULLET_STATE.ATTACK) {
      const idx = this.inactiveBulletArray.indexOf(bullet)
      if (idx !== -1) {
        this.inactiveBulletArray.splice(idx, 1)
        this.activeBulletArray.push(bullet)
      }
    } else if (_to === BULLET_STATE.IDLE) {
      const idx = this.activeBulletArray.indexOf(bullet)
      if (idx !== -1) {
        this.activeBulletArray.splice(idx, 1)
        this.inactiveBulletArray.push(bullet)
      }
    }
  }
}
import { BULLET_STATE } from '../configs'
import Bullet from './bullet'

export default class BulletFactory {
  constructor (scene) {
    this.scene = scene
    this.init()
  }

  init () {
    this.activeBulletArray = []
    this.inactiveBulletArray = []
    this.bulletArray = []
  }

  getBullet () {
    for (let i = 0; i < this.bulletArray.length; i++) {
      if (this.bulletArray[i].state === BULLET_STATE.IDLE) {
        this.bulletArray[i].state = BULLET_STATE.ATTACK
        return this.bulletArray[i]
      }
    }
    const bullet = this.createBullet()
    bullet.state = BULLET_STATE.ATTACK
    return bullet
  }

  createBullet () {
    const bullet = new Bullet(this.scene)
    this.bulletArray.push(bullet)
    return bullet
  }

© 2024 Doe의 devlog, Built with Vapor blog Theme Gatsby