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 ]
- 함께 작업하고 싶지만 너무 다른 객체가 있을 때 사용한다.(느슨한 결합 촉진)
- 함께 작업하는 매우 동일한 객체가 있을 때 사용한다.(긴밀한 결합 촉진)
- 객체를 직접 생성하는 데 사용할 수 없음
- 부모 클래스로만 사용
- 일부 메서드에 대한 실제 구현을 포함할 수 있음(위에서 sort 메서드와 같이)
- 구현된 메서드는 아직 실제로 존재하지 않는 다른 메서드를 참조할 수 있음
(아직 구현되지 않은 메서드의 이름과 유형을 증명해야 한다.) - 자식 클래스가 다른 방법을 구현하도록 허용할 수 있다.
'Develop > Others' 카테고리의 다른 글
static 멤버 언제 어떻게 사용해야 좋을까? (0) | 2022.02.14 |
---|---|
[typescript] Composition vs Inheritance (0) | 2022.02.06 |
check code difference / git diff with vscode (0) | 2022.01.27 |
멀티 테넌시 란? (0) | 2022.01.22 |
wsl node version & nvm node version (0) | 2022.01.01 |
댓글