# 03-04. 긴 매개변수 목록


# ✋ Intro

  • 프로그래밍을 처음 배울 때는 함수에 필요한 것들을 모조리 매개변수로 전달하라고 배웠다.
  • 하지만 매개변수 목록이 길어지면 그 자체로 이해하기 어려울 때가 존재한다.
  • 이러한 경우에 적용할 수 있는 리팩토링 기법을 살펴보자.

# (1) 매개변수를 질의 함수로 바꾸기

  • 종종 다른 매개변수에서 값을 얻어올 수 있는 매개변수가 있을 때 이 리팩토링 기법을 이용해서 그 매개변수를 제거할 수 있다.
  • 매개변수 목록은 함수의 동작에 변화를 줄 수 있는 일차적인 수단으로 다른 코드와 마찬가지로 이 목록에서도 중복은 피하는 게 좋으며 짧은수록 이해하기 쉽다.
// before
availableVacation(anEmployee, anEmployee.grade);

function availableVacation(anEmployee, grade) {}
// after
availableVacation(anEmployee);

function availableVacation(anEmployee) {
  const grade = anEmployee.grade;
}
  • 특정 매개변수를 제거하면 값을 결정하는 책임 주체가 달라진다.
  • 반면 이 기법을 적용하면 안 되는 상황도 있다.
    • 매개변수를 제거하면 피호출 함수에 원치 않는 의존성이 생길 때다.
    • 해당 함수가 알지 못했으면 하는 프로그램 요소에 접근해야 하는 상황을 만들 때가 존재한다.
  • 주의해야할 점은 대상 함수가 참조 투명해야 한다는 것이다.
    • 참조 투명이란 '함수에 똑같은 값을 건네 호출하면 항상 똑같이 동작한다'는 뜻이다.
    • 이런 함수는 동작을 예측하고 테스트하기가 훨씬 쉬우니 이 특성이 사라지지 않도록 주의하자.
    • 따라서 매개변수를 없애는 대신 가변 전역 변수를 이용하는 일은 하면 안 된다. 변수 범위가 오염될 수 있는 위험이 있다.

  • 다른 리팩토링을 수행한 후 특정 매개변수가 더는 필요 없어졌을 때가 있는데, 바로 이 리팩토링을 적용하는 가장 흔한 사례다.
// before

// Order class
get finalPrice() {
  const basePrice = this.quantity * this.itemPrice;
  let discountLevel;
    
  if (this.quantity > 100) {
    discountLevel = 2;
  } else {
    discountLevel = 1;
  }
    
  return this.discountedPrice(basePrice, discountLevel);
}

discountPrice(basePrice, discountLevel) {
  switch (discountLevel) {
    case 1:
      return basePrice * 0.95;
    case 2:
      return basePrice * 0.9;
  }
}
get finalPrice() {
  const basePrice = this.quantity * this.itemPrice;
   
  return this.discountedPrice(basePrice);
}

// 임시 변수를 질의 함수로 바꾸기 리팩토링 적용
get discountLevel() {
  return this.quantity > 100 ? 2 : 1;
}

// 함수 선언 바꾸기로 해당 매개변수 제거
discountPrice(basePrice) {
  switch (this.discountLevel) {
    case 1:
      return basePrice * 0.95;
    case 2:
      return basePrice * 0.9;
  }
}

# (2) 플래그 인수 제거하기

  • 함수의 동작 방식을 정하는 플래그 역할의 매개변수는 이 리팩토링 기법을 이용해서 없애준다.
  • 여기서 플래그 인수(flag argument)란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수다.
  • 플래그 인수는 호출할 수 있는 함수들이 무엇이고 어떻게 호출해야 하는지 이해하기 어려울 수 있다.
    • 예를 들어 boolean flag는 코드를 읽는 이에게 뜻을 온전히 전달하지 못하기 때문에 더욱 좋지 못하다.
    • 함수에 전달한 true의 의미를 파악하는데 어려우므로 이보다는 특정한 기능 하나만 수행하는 명시적인 함수를 제공하는 편이 훨씬 깔끔하다.
  • 플래그 인수의 조건
    • 호출하는 쪽에서 boolean 값으로 (프로그램에서 사용되는 데이터가 아닌) 리터럴 값을 건네야 한다.
    • 호출되는 함수는 그 인수를 (다른 함수에 전달하는 데이터가 아닌) 제어 흐름을 결정하는 데 사용해야 한다.
  • 플래그 인수 없이 구현하려면 플래그 인수들의 가능한 조합 수만큼의 함수를 만들어야 한다.
    • 그런데 다른 관점에서 보면, 플래그 인수가 둘 이상이면 함수 하나가 너무 많은 일을 처리한다는 의미이기도 한다.
    • 그러니 같은 로직을 조합해내는 더 간단한 함수를 만들 방법을 고민해봐야 한다.

  • 리팩토링 절차
    • 매개변수로 주어질 수 있는 값 각각에 대응하는 명시적 함수들을 생성
    • 원래 함수를 호출하는 코드들을 모두 찾아서 각 리터럴 값에 대응하는 명시적 함수를 호출하도록 수정

  • 예시 - 배송일자 계산
// before

// 호출문
aShipment.deliveryDate = deliveryDate(anOrder, true);

aShipment.deliveryDate = deliveryDate(anOrder, false);

function deliveryDate(anOrder, isRush) {
  if (isRush) {
    let deliveryTime;
      
    if (['MA', 'CT'].includes(anOrder.deliveryState)) {
      deliveryTime = 1;
    } else if (['NY', 'NH'].includes(anOrder.deliveryState)) {
      deliveryTime = 2;
    } else {
      deliveryTime = 3;
    }
    
    return anOrder.placeOn.plusDays(1 + deliveryTime);
  } else {
    let deliveryTime;
      
    if (['MA', 'CT', 'NY'].includes(anOrder.deliveryState)) {
      deliveryTime = 2;
    } else if (['ME', 'NH'].includes(anOrder.deliveryState)) {
      deliveryTime = 3;
    } else {
      deliveryTime = 4;
    }
    
    return anOrder.placeOn.plusDays(2 + deliveryTime);
  }
}
// after

// 호출문
aShipment.deliveryDate = rushDeliveryDate(anOrder);

aShipment.deliveryDate = regularDeliveryDate(anOrder);

function rushDeliveryDate(anOrder) {
  let deliveryTime;
      
  if (['MA', 'CT'].includes(anOrder.deliveryState)) {
    deliveryTime = 1;
  } else if (['NY', 'NH'].includes(anOrder.deliveryState)) {
    deliveryTime = 2;
  } else {
    deliveryTime = 3;
  }
    
  return anOrder.placeOn.plusDays(1 + deliveryTime);
}

function regularDeliveryDate(anOrder) {
  let deliveryTime;
      
  if (['MA', 'CT', 'NY'].includes(anOrder.deliveryState)) {
    deliveryTime = 2;
  } else if (['ME', 'NH'].includes(anOrder.deliveryState)) {
    deliveryTime = 3;
  } else {
    deliveryTime = 4;
  }
    
  return anOrder.placeOn.plusDays(2 + deliveryTime);
}

# (3) 여러 함수를 클래스로 묶기

  • 클래스는 데이터와 함수를 하나의 공유 환경으로 묶은 후, 다른 프로그램 요소와 어우러질 수 있도록 그중 일부를 외부에 제공한다.
  • 클래스는 객체 지향 언어의 기본인 동시에 다른 패러다임 언어에도 유용하다.
  • 클래스로 묶으면 이 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있고, 각 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결하게 만들 수 있다.
  • 클래스로 묶을 때의 장점은 클라이언트가 객체의 핵심 데이터를 변경할 수 있고, 파싱 객체들을 일관되게 관리할 수 있다.

  • 예시) 매달 차 계량기를 읽어서 측정값 기록
const reading = {
  customer: 'ivan',
  quantity: 10,
  month: 5,
  year: 2017
};
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);
const taxableCharge = Math.max(0, basicChargeAmount - taxThreshold(aReading.year));

// 기본 요금 계산 함수
function calculateBaseCharge(aReading) {
  return baseRate(aReading.month, aReading.year) * aReading.quantity;
}
  • 절차1 - 먼저 레코드를 클래스로 변환하기 위해 레코드를 캡슐화한다.
class Reading {
  constructor(data) {
    this._customer = data.customer;
    this._quantity = data.quantity;
    this._month = data.month;
    this._year = data.year;
  }
    
  get customer() {
    return this._customer;
  }
    
  get quantity() {
    return this._quantity;
  }
    
  get month() {
    return this._month;
  }
    
  get year() {
    return this._year;
  }
}
  • 절차2 - 이미 만들어져 있는 calculateBaseCharge()부터 옮기자. 새 클래스를 사용하려면 데이터를 얻자마자 객체로 만들어야 한다.
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);
  • 그런 다음 calculateBaseCharge()를 새로 만든 클래스로 옮긴 후 적절한 이름(baseCharge)으로 바꾼다.
class Reading {
  // 생략
    
  get baseCharge() {
    return baseRate(this.month, this.year) * this.quantity;
  }
}

const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));
  • 절차3 - 이어서 세금을 부과할 소비량을 계산하는 코드를 함수로 추출하고 이를 Reading 클래스로 옮긴다.
// 완성본
class Reading {
  constructor(data) {
    this._customer = data.customer;
    this._quantity = data.quantity;
    this._month = data.month;
    this._year = data.year;
  }
    
  get customer() {
    return this._customer;
  }
    
  get quantity() {
    return this._quantity;
  }
    
  get month() {
    return this._month;
  }
    
  get year() {
    return this._year;
  }
    
  get baseCharge() {
    return baseRate(this.month, this.year) * this.quantity;
  }
    
  get taxableCharge() {
    return Math.max(0, this.baseCharge - taxThreshold(this.year));
  }
}

const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = aReading.taxableCharge;
  • 파생 데이터 모두를 필요한 시점에 계산되게 만들었기 때문에 저장된 데이터를 갱신하더라도 문제가 생길 일이 없다.
  • 프로그램의 다른 부분에서 데이터를 갱신할 가능성이 꽤 있을 때는 클래스로 묶어두면 큰 도움이 된다.