코드가 오용하기 쉽게 작성된다면, 조만간 오용될 가능성이 있고 소프트웨어가 올바르게 작동하지 않을 것이다.
입력 매개변수를 변경 가능하게 하는 것은 바람직하지 않은 관행이다.
TextOptions 클래스는 가변적이라 해당 인스턴스를 전달받는 모든 코드는 이 객체를 변경할 수 있어 오용의 위험이 있다.
class TextOptions {
constructor(private font: Font, private fontSize: number) {}
setFont(font: Font) {
this.font = font;
}
setFontSize(fontSize: number) {
this.fontSize = fontSize;
}
}
class MessageBox {
//...
renderTitle(title: string, baseStyle: TextOptions) {
// TextOptions 인스턴스 객체 변경
baseStyle.setFontSize(25);
}
}
가장 쉽게 TextOptions 클래스를 불변 객체(클래스)로 만들기 위해서 setter를 모두 제거하고 getter만 남기는 것이다.
또는 불변성에 대한 디자인 패턴을 사용하는 것인데, 빌더 패턴
과 쓰기 시 복사 패턴
이 존재한다.
빌더 패턴은 값의 일부(또는 전체)가 선택 사항일 때 불변적 객체를 만드는 유용한 방법이다.
class TextOptions {
constructor(private font: Font, private fontSize: number) {}
getFont() {
return this.font;
}
getFontSize() {
return this.fontSize;
}
}
class TextOptionsBuilder {
constructor(private font: Font, private fontSize?: number) {} // 빌더는 생성자를 통해 필수값을 받는다.
setFontSize(fontSize: number) { // 필수적이지 않은 값을 받는다.
this.fontSize = fontSize;
return this; // setter 함수는 함수 호출을 연이어 할 수 있도록 this를 반환한다.
}
build() {
reutrn new TextOptions(this.font, this.fontSize); // 모든 값이 정해지고 나면 호출하는 쪽에서는 TextOptions 객체를 얻기 위한 build를 호출한다.
}
}
const getDefaultTextOptions = () => {
return new TextOptionsBuilder(FONT.ARIAL)
.setFontSize(25)
.build();
}
이렇게 작성하면 TextOptions 객체의 변경된 버전이 필요한 경우, 원본 객체에 영향을 미치지 않고도 변경된 복사본을 쉽게 얻을 수 있다.
class TextOptions {
constructor(private font: Font, private fontSize: number) {}
withFont(newFont: Font) {
return new TextOptions(newFont, this.fontSize); // 폰트만 변경된 TextOptions 객체 반환
}
withFontSize(newFontSize: number) {
return new TextOptions(this.font, newFontSize); // 폰트 크기만 변경된 TextOptions 객체 반환
}
//...
}
function getDefaultTextOptions() {
return new TextOptionsBuilder(FONT.ARIAL)
.withFontSize(12);
}
오용이 불가능한 클래스를 만드려면 setter 함수를 제거하고 인스턴스를 생성할 때만 값을 제공하면 된다.
지나치게 일반적인 유형은 오용될 수 있다.
만약 지도에서 위치를 처리하는 코드를 작성할 때 위도와 경도를 배열로 관리하면 오용하기 쉬워진다. 만약 위치 값의 타입을 number[]
로 나타낸다면 개발자는 타입 자체로 어떤 값을 나타내는지 알 수 없다. 또한 개발자는 배열에서 어떤 요소가 위도인지, 경도인지 혼동하기 쉽고, 타입 안정성이 없어 정상적으로 컴파일되지만 런타임에 문제가 발생할 수 있다.
즉, 설명이 부족하고 허용하는 범위가 넓을수록 코드 오용은 쉬워진다.
페어 유형은 동일하거나 다른 종류의 값을 두 개 저장하는 것을 말한다.
type Locations = [number, number][];
만약 [number, number][]
와 같은 타입을 사용한다면 위도와 경도의 순서를 혼동하기 쉬우며 무슨 의미인지 이해하기 어렵다.
무언가를 나타내기 위해 새로운 클래스(구조체, 타입)를 정의하는 것은 많은 노력이 들거나 불필요한 것처럼 보일 수 있지만, 대부분 보기보다 노력이 덜 들어가고 다른 개발자가 코드를 읽을 때 이해하기 쉽고 버그의 가능성도 줄여준다.
오해의 소지를 줄이는 방법은 위도와 경도를 나타내는 전용 타입(클래스)를 만들어 해결할 수 있다.
시간을 다룰 때 코드를 잘못 사용하고 혼동을 일으킬 여지가 굉장히 많다.
시간을 다룰 때 일반적으로 정수를 사용하는데 정수는 매우 일반적인 유형이므로 코드가 오용되기 쉽다.
그리고 가장 일반적인 단위는 ms 또는 s이지만 경우에 따라 μs(마이크로초)와 같은 단위도 사용하기에 함수 이름이나 매개변수 이름, 주석문으로 단위를 나타낼 수 있지만 여전히 개발자에게 혼동을 주기 쉽다.
또한 서버가 다른 위치에서 실행되고 시스템을 다른 표준 시간대로 설정한 경우 시간대에 대한 처리 오류가 발생할 수 있다. 예를 들어 유럽에 있는 서버가 처리한 날짜값이 캘리포니아에 있는 서버에 의해 저장될 수도 있다는 것이다.
날짜 값을 다룰 때, 여러 형식을 출력해야 할 때가 있다. 예를 들어, 23년 7월 16일, 16일 전, 16분 전과 같이 말이다. 이렇게 여러 형식으로 쉽게 제공할 수 있는 라이브러리이다.
데이터는 기본 데이터와 파생 데이터로 나뉜다. 파생 데이터는 주어진 기본 데이터에 기반해 계산된 데이터를 말한다.
// 계좌 잔액은 credit - debit이어야 함!
new UserAccount(credit, debit, debit - credit);
기본 데이터와 파생 데이터를 모두 처리하는 코드를 작성할 때 위와 같이 논리적으로 올바르지 않은 코드를 작성하기 쉽다.
만약 UserAccount 클래스를 생성할 때, 대변, 차변, 계좌 잔액(대변 - 차변)의 값으로 생성한다면 계좌 잔액에 대한 값은 대변과 차변의 값에서 파생될 수 있기에 중복 정보를 제공하는 셈이며 논리적으로 잘못된 인스턴스를 생성할 수 있다.
데이터 모델이 논리적으로 잘못된 상태를 허용하는지 항상 확인할 필요가 있다.
class UserAccount {
constructor(private credit: number, private debit: number) {}
getBalance() {
return this.credit - this.debit;
}
}