소트웍스 앤솔로지의 Object Calisthenics 내용을 TypeScript로 적용해봤습니다.
이전 두 개의 규칙도 간단했지만, 이번 규칙은 더 간단하다. 본래 이는 Java에서 byte
, short
, int
, long
, float
, double
, boolean
, char
그리고 String
을 포장하라는 의미다.
TypeScript의 경우에 Object, Array 등도 원시값으로 정의하므로 우리는 조금 이를 바꿔보자. TypeScript 작성 시 Number
과 String
은 포장하도록 하자. 값을 포장한다는 것은 하드코딩하지 말자는 이야기다. 두 가지 예시를 통해 이해해보자.
const generateRandomLottoNumber = () => {
return Math.round(Math.random() * 45) + 1;
};
const lottoTicket: number[] = [];
for (let i = 0; i < 6; i++) {
lottoTicket.push(generateRandomLottoNumber());
}
console.log(lottoTicket);
로또 티켓을 만드는 로직이다. 모든 Number
과 String
을 포장하라 했으니, 이를 토대로 한 번 고쳐보자.
const LOTTO_NUMBERS_COUNT = 6;
const LOTTO_MINIMUM_NUMBER = 1;
const LOTTO_MAXIMUM_NUMBER = 45;
const generateRandomLottoNumber = () => {
return (
Math.round(Math.random() * LOTTO_MAXIMUM_NUMBER) + LOTTO_MINIMUM_NUMBER
);
};
const lottoTicket: number[] = [];
for (let i = 0; i < LOTTO_NUMBERS_COUNT; i++) {
lottoTicket.push(generateRandomLottoNumber());
}
console.log(lottoTicket);
6
, 1
, 45
를 상수화 했는데, 그 이유는 각각의 숫자들이 특별한 의미를 가지기 때문이다. 이렇게 의미를 지닌 원시값들을 명시적으로 상수화한다면 읽는 사람으로 하여금 코드를 더 이해하기 쉽게 만들어준다.
단,
0
을ZERO
로,2
를TWO
로 상수화하는 등 의미없는 상수명을 붙이는 것은 지양하자.
const store = {
dietCoke: 1800,
coke: 1600,
snack: 1800,
tofu: 1500,
milk: 3800,
};
let money = 12000;
console.log('잔액: ', money);
money = money - store.dietCoke;
money = money - store.snack;
money = money - store.milk;
console.log('잔액: ', money);
우선 이전에 했던 규칙에 따라 의미있는 Number
과 String
을 상수화해보자.
const INITIAL_MONEY_AMOUNT = 12000;
const BALANCE_PREFIX = '잔액: ';
const store = {
dietCoke: 1800,
coke: 1600,
snack: 1800,
tofu: 1500,
milk: 3800,
};
let money = INITIAL_MONEY_AMOUNT;
console.log(BALANCE_PREFIX, money);
money = money - store.dietCoke;
money = money - store.snack;
money = money - store.milk;
console.log(BALANCE_PREFIX, money);
우리는 돈을 표현하기 위해 Number
를 사용하고 있는데, 한 가지만 생각해보자. 돈은 일반 숫자와는 다르게 0인 경우에 사용할 수 없다. 그리고 초기값이 음수일 수도 없다. 특별한 의미를 가지고 있는 이 숫자를 어떻게 의미있게 포장할 수 있을까? 이 때 나오는 것이 **값 객체(Value Object)**다.
값 객체가 무조건 한 가지 필드를 가지고 있어야 하는 것은 아니다. 다만, 한 가지의 필드만 있더라도 그 값에 의미가 있다면 값 객체로 포장하는 습관을 들이면 좋다.
const NEGATIVE_AMOUNT_ERROR_MESSAGE = '초기값은 음수가 될 수 없습니다.';
const LOWER_LIMIT_AMOUNT = 0;
class Money {
constructor(private _amount: number) {
if (this._amount < 0) {
throw Error(NEGATIVE_AMOUNT_ERROR_MESSAGE);
}
}
get amount() {
return this._amount;
}
add(other: Money): void {
this._amount += other._amount;
}
reduce(other: Money): void {
if (this._amount - other._amount < LOWER_LIMIT_AMOUNT) {
throw Error(NEGATIVE_AMOUNT_ERROR_MESSAGE);
}
this._amount -= other._amount;
}
}
const INITIAL_MONEY_AMOUNT = 12000;
const BALANCE_PREFIX = '잔액: ';
const store = {
dietCoke: new Money(1800),
coke: new Money(1600),
snack: new Money(1800),
tofu: new Money(1500),
milk: new Money(3800),
};
let money = new Money(INITIAL_MONEY_AMOUNT);
console.log(BALANCE_PREFIX, money.amount);
money.reduce(store.dietCoke);
money.reduce(store.snack);
money.reduce(store.milk);
console.log(BALANCE_PREFIX, money.amount);
의미 있는 값 포장을 통해 Primitive Obsession Anti Pattern을 피할 수 있다. Number
나 String
을 상수화하거나 값 객체로 포장하여 한 눈에 들어오는 가독성 좋은 코드를 만들자.
잘 보고 갑니다~~~ :)