소트웍스 앤솔로지 의 Object Calisthenics 내용을 TypeScript로 적용해봤습니다.
이전 두 개의 규칙도 간단했지만, 이번 규칙은 더 간단하다. 본래 이는 Java에서 byte
, short
, int
, long
, float
, double
, boolean
, char
그리고 String
을 포장하라는 의미다.
TypeScript의 경우에 Object, Array 등도 원시값으로 정의하므로 우리는 조금 이를 바꿔보자. TypeScript 작성 시 Number
과 String
은 포장하도록 하자. 값을 포장한다 는 것은 하드코딩하지 말자 는 이야기다. 두 가지 예시를 통해 이해해보자.
의미있는 값은 상수화하자.
Copy 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
을 포장하라 했으니, 이를 토대로 한 번 고쳐보자.
Copy 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
로 상수화하는 등 의미없는 상수명을 붙이는 것은 지양하자.
특정 기능이 있는 원시형은 값 객체로 만들자.
Copy 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
을 상수화해보자.
Copy 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) 다.
값 객체가 무조건 한 가지 필드를 가지고 있어야 하는 것은 아니다. 다만, 한 가지의 필드만 있더라도 그 값에 의미가 있다면 값 객체로 포장하는 습관을 들이면 좋다.
Copy 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
을 상수화하거나 값 객체로 포장하여 한 눈에 들어오는 가독성 좋은 코드를 만들자.