5. 한 줄에 점은 오직 하나만 사용하라

소트웍스 앤솔로지Object Calisthenics 내용을 TypeScript로 적용해봤습니다.

TypeScript에서는 어떤 객체 또는 배열 내 값에 접근할 때 점(.) 또는 인덱스 접근자([])을 사용한다. 이번 규칙은 무수히 많은 . 또는 []를 통해 객체나 배열 내부 깊게 들어가 속성값을 가져오는 것을 지양하라는 뜻이다. 규칙이 지켜지지 않은 예제를 살펴보자.

디미터의 법칙으로 일컬어지는 이 규칙은 너 자신 또는 1차 관계에 있는 친구에게만 이야기하라라고 비유한다.

class Car {
  constructor(public owner: string, public position: number) {}
}

const getMaxPosition = (cars: Car[]) => {
  let maxPosition = 0;
  for (let i = 0; i < cars.length; i++) {
    if (maxPosition < cars[i].position) {
      maxPosition = cars[i].position;
    }
  }
  return maxPosition;
};

const getTopRankedCar = (cars: Car[]) => {
  let topCarString = '';
  const maxPosition = getMaxPosition(cars);
  for (let i = 0; i < cars.length; i++) {
    if (maxPosition === cars[i].position) {
      topCarString += `${cars[i].owner}, `;
    }
  }
  return topCarString.substr(0, topCarString.length - 2);
};

cars[i].position을 살펴보자. 규칙에서 말하듯 .은 1개라서 문제가 없지 않냐 생각할 수 있지만 이 규칙의 의미는 그런 것이 아니다. cars라는 배열 객체의 인덱스에 접근하기 위해 1차적으로 [i]를 사용하고, 2차적으로 .position을 사용해 그 인덱스에 있는 Car 객체의 값에 접근하고 있기 때문에 문제시 될 수 있는 부분이다. 이렇게 한 줄에 객체 내부 값을 가져오기 위해 체이닝을 하는 것은 극단적으로는 이런 코드를 만들 수 있다.

const secondUncle = me.family.father.family.father.sons[1];

심지어 이와 유사한 코드가 소스 코드 여러 곳에 산재되어 있다면... 상상만 해도 끔찍하다. 이 무수히 많은 접근자들을 어떻게 더 쉽게 만들 수 있을까?

const secondUncle = me.secondUncle;

이런 느낌으로 바꾸면 어떨까? 훨씬 간결한 코드로 변한 것을 알 수 있다. 이렇게 한 줄에 점(.)(그리고 [])을 두 개 이상 찍지 말라는 규칙을 지키며 처음 코드를 리팩토링해보자.

단, class에서 this를 사용하는 경우와 .map, .reduce, .forEach 등을 체이닝 하는 배열 함수에 있어서는 이 규칙을 적용하지 않아도 된다고 생각하자. 중첩된 객체의 내부 속성에 직접 접근하는 것을 지양하는 것이 이 규칙의 핵심이다.

class Car {
  constructor(private _owner: string, private position: number) {}

  get owner() {
    return this._owner;
  }

  isOnSamePositionWith(other: Car) {
    return this.position == other.position;
  }

  defeats(other: Car) {
    return this.position > other.position;
  }
}

class Cars {
  static DELIMITER = ', ';

  constructor(private cars: Car[]) {}

  getTopRankedCars() {
    return this.filterTopRankedCars()
      .map((car) => car.owner)
      .join(Cars.DELIMITER);
  }

  private filterTopRankedCars() {
    return this.cars.filter((car) =>
      car.isOnSamePositionWith(this.getMaxPositionedCar())
    );
  }

  private getMaxPositionedCar() {
    return this.cars.reduce((previous, current) =>
      previous.defeats(current) ? previous : current
    );
  }
}

다음 방법들을 적용해 이전의 코드보다 가독성이 좋고 잘 추상화된 코드를 만들어봤다.

  • public 했던 객체의 속성을 private으로 바꿔서 직접 속성에 접근하는 것을 예방하기 (owner 같은 경우 set을 할 수 없게 get만 열어주었다.

  • 직접 속성에 접근해 연산하는 대신 다른 Car을 인자로 받는Car의 메소드를 만들기

  • Car[]에 일급 콜렉션을 적용해 Car들을 관리하는 메소드를 모으기

  • for문을 배열 함수로 바꾸어 체이닝하기

이제 객체나 배열의 구조가 변경되더라도 속성에 직접 접근했었던 모든 부분을 고치지 않아도 되기 때문에 확장에 대해 열려있고, 수정에 대해 닫혀 있는 개방-폐쇄 원칙을 지킬 수 있다.

이렇게 한 줄에 여러 개의 .[]를 두지 않으려 노력하는 것만으로도 코드의 품질을 많이 개선할 수 있다는 것을 알게 되었다.

Last updated