본문 바로가기
Develop/Others

[typescript] Interface VS Abstract Class

by 3-stack 2022. 2. 4.

typescript를 사용하면서

인터페이스와 추상클래스는 어떤 경우에 사용하는지 알아봅니다.

Interface vs Abstract Class

정렬 기능을 갖는  Sorter 클래스를 구현하면서 실습합니다.

 

 

1-1. 사용할 프로젝트를 생성합니다.

# 기본적인 package.json 파일과 함께 프로젝트 생성
npm init -y
# typescript 및 테스트에 사용될 npm 설치
npm i typescript nodemon concurrently
# source 디렉토리 생성
mkdir src
# main file 생성
touch index.ts
# 기본 typescript 설정파일 생성
npx tsc --init

- 아래와 같이 프로젝트 구조가 갖춰집니다.

 

1-2. 다음과 같이 typescript config를 설정합니다.

- tsconfig.json 파일을 열어서 rootDir, outDir을 수정합니다.

{
  "compilerOptions": {
   ...
   /*소스파일 루트 경로를 설정합니다.*/,
    "rootDir": "./src"
   /*타입스크립트 빌드 결과물 경로를 설정합니다.*/
    "outDir": "./build" 
    ...
  }
}

 

1-3. 학습에 용이하게 npm script를 설정합니다. package.json 파일을 수정합니다.

- build 용도의 scripts를 설정해주세요.

- tsconfig.json > rootDir에 설정한 경로의 소스파일을 빌드합니다. 

"scripts": {
  ...
  "start:build": "tsc --watch"
}

- 아래와 같이 build 디렉터리가 생성되는 것을 확인할 수 있습니다.

- 앞서 생성한 index.ts 파일에 간단한 코드를 입력해서 테스트해보세요.

- run 용도의 scripts를 설정해주세요. 빌드 결과물(build > index.js)이 정상적으로 실행됩니다.

"scripts": {
  ...
  "start:run": "nodemon build/index.js"
}

- 테스트를 편하게 하고자 build script와 run script를 한 번에 실행할 수 있습니다.

"scripts": {
  ...
  "start": "concurrently npm:start:*"
}

 

 

2-1. Sorter 클래스 만들기

- 숫자 배열을 받아서 collection이라는 이름의 속성을 갖는 클래스입니다.

- collection을 정렬하는 sort 함수가 있습니다. (버블정렬을 사용해서 직접 구현해주세요.)

export class Sorter {
  constructor(public collection: number[]) {}

  sort(): void {
    const { length } = this.collection;
    for (let i = 0; i < length; i++) {
      for (let j = 0; j < length - i - 1; j++) {
        if (this.collection[j] > this.collection[j + 1]) {
          const leftHand = this.collection[j];
          this.collection[j] = this.collection[j + 1];
          this.collection[j + 1] = leftHand;
        }
      }
    }
  }
}

 

2-2. Sorter 클래스 리팩터링

- sort 함수에 1) 두 아이템을 비교하는 기능(compare)2) 두 아이템의 위치를 바꾸는 기능(swap)이 함께 혼재되어있습니다. refactoring 해서 분리해줍니다.

export class Sorter {
  constructor(public collection: number[]) {}

  compare(leftIndex: number, rightIndex: number): boolean {
    return this.collection[leftIndex] > this.collection[rightIndex];
  }

  swap(leftIndex: number, rightIndex: number): void {
    const leftHand = this.collection[leftIndex];
    this.collection[leftIndex] = this.collection[rightIndex];
    this.collection[rightIndex] = leftHand;
  }

  sort(): void {
    const { length } = this.collection;
    for (let i = 0; i < length; i++) {
      for (let j = 0; j < length - i - 1; j++) {
        if (this.compare(j, j + 1)) {
          this.swap(j, j + 1);
        }
      }
    }
  }
}

- 다음과 같이 index.ts 에서 Sorter 클래스가 잘 작동함을 확인할 수 있습니다.

import { Sorter } from "./Sorter";

const sorter = new Sorter([3, 0, 100, -2]);
sorter.sort();
console.log(sorter.collection); // [ -2, 0, 3, 100 ]

 

2-3. Sorter로 string도 정렬하기

- string을 주면서 생성하면 아래와 같이 에러가 나타난다. 지금 구현한 swap 함수로는 string을 swap 시킬 수 없습니다. 직접 테스트해보면 swap 되지 않고 그대로인 것을 확인할 수 있습니다.

- 문자열은 대소문자 구분하지 않고, 정렬되게 해주세요.

 

2-4. number[]와 string 인 경우를 나눠서 swap을 다른 방식으로 구현합니다.

- 아래와 같이 type guard 개념을 사용해서 나눌 수 있습니다. 그러나 Sorter로 정렬하는 collection 종류가 많아질수록 (예를 들면 LInkedList, Stack 등) swap은 물론 다른 함수의 복잡도가 올라가고 테스트하기 어려워집니다.

- collection의 타입에 따라 분리해줘야 합니다.

swap(leftIndex: number, rightIndex: number): void {
    if (this.collection instanceof Array) {
      const leftHand = this.collection[leftIndex];
      this.collection[leftIndex] = this.collection[rightIndex];
      this.collection[rightIndex] = leftHand;
    } else {
      const characters = this.collection.split("");
      const leftHand = characters[leftIndex];
      characters[leftIndex] = characters[rightIndex];
      characters[rightIndex] = leftHand;
      this.collection = characters.join("");
    }
}

- NumbersCollection, CharactersCollection 으로 분리

export class NumbersCollection {
  constructor(public collection: number[]) {}

  get length(): number {
    return this.collection.length;
  }

  swap(leftIndex: number, rightIndex: number): void {
    const leftHand = this.collection[leftIndex];
    this.collection[leftIndex] = this.collection[rightIndex];
    this.collection[rightIndex] = leftHand;
  }

  compare(leftIndex: number, rightIndex: number): boolean {
    return this.collection[leftIndex] > this.collection[rightIndex];
  }
}
export class CharactersCollection {
  constructor(public collection: string) {}

  get length(): number {
    return this.collection.length;
  }

  swap(leftIndex: number, rightIndex: number): void {
    const characters = this.collection.split("");
    const leftHand = characters[leftIndex];
    characters[leftIndex] = characters[rightIndex];
    characters[rightIndex] = leftHand;
    this.collection = characters.join("");
  }

  compare(leftIndex: number, rightIndex: number): boolean {
    return (
      this.collection[leftIndex].toLowerCase() >
      this.collection[rightIndex].toLowerCase()
    );
  }
}

- Sorter 클래스는 interface Sortable를 정의하고 정렬 가능한 collection만 받아서 생성합니다.

interface Sortable {
  swap(leftIndex: number, rightIndex: number): void;
  compare(leftIndex: number, rightIndex: number): boolean;
  length: number;
}

export class Sorter {
  constructor(public collection: Sortable) {}

  sort(): void {
    const { length } = this.collection;
    for (let i = 0; i < length; i++) {
      for (let j = 0; j < length - i - 1; j++) {
        if (this.collection.compare(j, j + 1)) {
          this.collection.swap(j, j + 1);
        }
      }
    }
  }
}

- 아래와 같이 작동을 확입합니다.

import { CharactersCollection } from "./CharactersCollection";
import { NumbersCollection } from "./NumbersCollection";
import { Sorter } from "./Sorter";

const charactersCollection = new CharactersCollection("eDcBa");
const sorter1 = new Sorter(charactersCollection);
sorter1.sort();
console.log(charactersCollection.collection); // aBcDe

const numbersCollection = new NumbersCollection([3, 0, 100, -2]);
const sorter2 = new Sorter(numbersCollection);
sorter2.sort();
console.log(numbersCollection.collection); // [ -2, 0, 3, 100 ]

 

3-1. 정렬이 가능한 클래스에 sort 함수 통합 

- 지금도 크게 나빠보이지 않지만 정렬이 필요할 때마다 별도로 Sorter 인스턴스를 생성해서 사용하니 불편하고 직관적이지 않습니다.

- 정렬이 가능한 클래스에 sort 함수를 통합시켜 아래와 같이 사용할 수 있게 해주세요.

import { CharactersCollection } from "./CharactersCollection";
import { NumbersCollection } from "./NumbersCollection";

const charactersCollection = new CharactersCollection("eDcBa");
charactersCollection.sort();
console.log(charactersCollection.collection);

const numbersCollection = new NumbersCollection([3, 0, 100, -2]);
numbersCollection.sort();
console.log(numbersCollection.collection);

 

3-2. Sorter를 상속시킵니다.

- Sorter 클래스의 sort 함수를 사용하기 위해 NumbersCollection에 Sorter를 상속시키면,

- Sorter의 constructor에서 Sortable 데이터를 받고 있기 때문에 에러가 나타납니다. CharactersCollection도 동일한 에러가 나타납니다.

 

3-3. Sorter를 추상 클래스로 변경

- 앞으로는 Sorter 클래스로 직접 객체를 생성하지 않고, 부모 클래스 역할로만 사용할 것이므로 constructor를 삭제하고 abstract class 로 만들어줍니다.

- constructor를 삭제하고 더 이상 사용하지 않을 interface Sortable도 같이 지웁니다.

export abstract class Sorter {
  abstract swap(leftIndex: number, rightIndex: number): void;
  abstract compare(leftIndex: number, rightIndex: number): boolean;
  abstract length: number;

  sort(): void {
    const { length } = this;
    for (let i = 0; i < length; i++) {
      for (let j = 0; j < length - i - 1; j++) {
        if (this.compare(j, j + 1)) {
          this.swap(j, j + 1);
        }
      }
    }
  }
}

- constructor 를 지우면 constructor에서 만들어주던 collection이 사라져 아래와 같이 에러가 발생합니다.

- length는 물론 compare, swap 도 동일합니다.

- length, compare, swap 은 모두 Sorter를 상속받는 자식 클래스에서 구현하여 사용합니다.

- 아직 실제로 존재하지 않지만 자식 클래스에서 구현할 것을 타입스크립트에게 알려줘야 합니다.

- 아래와 같이 abstract를 사용하여 구현은 자식클래스에서 한다는 것을 타입스크립트에게 가르쳐 줄 수 있습니다.

export abstract class Sorter {
  abstract swap(leftIndex: number, rightIndex: number): void;
  abstract compare(leftIndex: number, rightIndex: number): boolean;
  abstract length: number;

  sort(): void {
    ...
  }
}

 

# Interface와 Abstract Class

- Interfcae와 Abstract Class는 서로 다른 클래스 간에 관계를 설정하는 공통점이 있으며 아래와 같은 차이점이 있다.

[ Interfcae ]

  • 함께 작업하고 싶지만 너무 다른 객체가 있을 때 사용한다.(느슨한 결합 촉진)
[ Abstract Class ]
  • 함께 작업하는 매우 동일한 객체가 있을 때 사용한다.(긴밀한 결합 촉진)
  • 객체를 직접 생성하는 데 사용할 수 없음
  • 부모 클래스로만 사용
  • 일부 메서드에 대한 실제 구현을 포함할 수 있음(위에서 sort 메서드와 같이)
  • 구현된 메서드는 아직 실제로 존재하지 않는 다른 메서드를 참조할 수 있음
    (아직 구현되지 않은 메서드의 이름과 유형을 증명해야 한다.)
  • 자식 클래스가 다른 방법을 구현하도록 허용할 수 있다.

댓글