Typescript로 Array-based ADT List 구현하기

Dasol Kim·2022년 3월 22일
0
post-thumbnail

기존에 Java로 작성한 array-based ADT List를 Typescript로 재작성해보았다.

interface List<T> {
		readonly size: number;
		isEmpty(): boolean;
		isFull(): boolean;
		add(item: T, index: number): ResultState<T>;
		remove(index: number): ResultState<T>;
		removeAll(): void;
		get(index: number): ResultState<T>;
		find(item: T): ResultState<T>;
	}

우선 인터페이스에는 operations들과 함께 size라는 데이터 프로퍼티를 정의하였다. 인터페이스 타입의 클래스 객체를 생성한 후 외부에서 접근할 때 변경되지 못하도록 해야하므로 readonly를 추가하였다. 또한 add, remove, get, find 메소드를 호출할 때 우리가 예상할 수 있는 에러에 대해 깔끔하게 처리하기 위해 성공 혹은 실패와 관련된 정보를 담은 ResultState를 반환하게끔 하였다.

type SuccessState<T> = {
		state: 'success';
		array: Array<T>;
		element?: T;
		index?: number;
	};

type FailureReason =
	| 'index out of range'
	| 'already full list'
	| 'empty list'
	| 'invalid value';

type FailureState = {
	state: 'fail';
	reason: FailureReason;
};

type ResultState<T> = SuccessState<T> | FailureState;

SuccessState와 FailureState는 state 프로퍼티로 구분이 가능한 Discriminated Unions로서 ResultState로 정의하였다. FailureReason은 String literal type들의 Union Type으로 정의하였다.

class ListImpl<T> implements List<T> {
	private _size: number = 0;
	private readonly MAX_LENGTH = 5;
	private array: Array<T> = new Array();
	private get RESULT_ARRAY(): SuccessState<T> {
		return {
			state: 'success',
			array: this.array,
		};
	}
	private readonly INDEX_OUT_OF_RANGE: FailureState = {
		state: 'fail',
		reason: 'index out of range',
	};
	private readonly FULL_LIST: FailureState = {
		state: 'fail',
		reason: 'already full list',
	};
	private readonly EMPTY_LIST: FailureState = {
		state: 'fail',
		reason: 'empty list',
	};
	private readonly INVALID_VALUE: FailureState = {
		state: 'fail',
		reason: 'invalid value',
	};
	get size() {
		return this._size;
	}
	isEmpty(): boolean {
		if (this._size === 0) return true;
		else return false;
	}
	isFull(): boolean {
		if (this._size === this.MAX_LENGTH) return true;
		return false;
	}
	add(item: T, index: number): ResultState<T> {
		if (this.isFull()) {
			return this.FULL_LIST;
		}
		if (index < 0 || index > this.size) {
			return this.INDEX_OUT_OF_RANGE;
		}
		for (let j = this.size; j > index; j--) {
			this.array[j] = this.array[j - 1];
		}
		this.array[index] = item;
		this._size++;
		return this.RESULT_ARRAY;
	}
	remove(index: number): ResultState<T> {
		if (this.isEmpty()) {
			return this.EMPTY_LIST;
		}
		if (index < 0 || index >= this.size) {
			return this.INDEX_OUT_OF_RANGE;
		}
		for (let j = index; j < this.size; j++) {
			this.array[j] = this.array[j + 1];
		}
		this._size--;
		return this.RESULT_ARRAY;
	}
	removeAll(): void {
		this.array = new Array();
		this._size = 0;
	}
	get(index: number): ResultState<T> {
		if (this.isEmpty()) {
			return this.EMPTY_LIST;
		}
		if (index < 0 || index >= this.size) {
			return this.INDEX_OUT_OF_RANGE;
		}
		return {
			...this.RESULT_ARRAY,
			element: this.array[index],
		};
	}
	find(item: T): ResultState<T> {
		if (this.isEmpty()) {
			return this.EMPTY_LIST;
		}
		for (let j = 0; j < this._size; j++) {
			if (this.array[j] === item) {
				return {
					...this.RESULT_ARRAY,
					index: j,
				};
			}
		}
		return this.INVALID_VALUE;
	}
}

인터페이스에서 readonly 프로퍼티로 정의한 size의 경우 클래스 내부에서 똑같이 readonly 프로퍼티로 정의하게 되면 클래스 내부에서 해당 프로퍼티를 변경하지 못하기 때문에, 변경이 가능한 private 프로퍼티인 underscore가 붙은 size 를 선언해주고 이를 반환하는 size의 custom getter를 정의해주었다. 그리고 각각의 메소드 내부에서 성공 혹은 실패(유효하지 않은 인덱스 범위, 꽉 찬 리스트, 빈 리스트, 유효하지 않은 값), 즉 위에서 정의한 각각의 타입을 반환하는 객체 프로퍼티들도 정의해주었다. 이렇게 함으로써 코드의 재사용성을 높여주었다.

const list: List<string> = new ListImpl<string>();
type Fruit = [name: string, index: number];

const fruits: Fruit[] = [['Apple', 0], ['Banana', 1], ['Orange', 2], ['Peach', 1]];
for (let i=0; i<fruits.length; i++) {
	const [name, index] = fruits[i];
	const result = list.add(name, index);
	if (result.state == "success") {
		console.log(result.array);
	} else {
		console.log(result.reason);
	}
}
const result2 = list.remove(4);
if (result2.state == "success") {
	console.log(result2.array);
} else {
	console.log(result2.reason);
}
const result3 = list.get(3);
if (result3.state == "success") {
	console.log(result3.element);
} else {
	console.log(result3.reason);
}
const result4 = list.find('Peach');
if (result4.state == "success") {
	console.log(result4.index);
} else {
	console.log(result4.reason);
}

하나의 튜플 타입을 Fruit으로 정의하였고 이러한 튜플의 배열을 담은 객체를 정의하였다. 이를 통해 add, remove, get, find가 각각 반환하는 객체의 state가 "success"냐 혹은 "fail"이냐에 따라서 그 결과를 달리 출력하게끔 해주었다.

0개의 댓글