반디북
쏙쏙 들어오는 함수형 코딩
Ch10
우석

일급함수 1

리팩터링 기법 1: 암묵적 인자 드러내기

function setPriceByName(cart, name, price) {
  var item = cart[name];
  var newItem = objectSet(item, 'price', price);
  var newCart = objectSet(cart, name, newItem);
  return newCart;
}
 
function setQuantityByName(cart, name, quant) {
  var item = cart[name];
  var newItem = objectSet(item, 'quantity', quant);
  var newCart = objectSet(cart, name, newItem);
  return newCart;
}
 
function setShippingByName(cart, name, ship) {
  var item = cart[name];
  var newItem = objectSet(item, 'shipping', ship);
  var newCart = objectSet(cart, name, newItem);
  return newCart;
}
  • 책에서는 암묵적 인자 드러내기라는 리팩터링 기법을 소개한다.
  • 해당 기법을 적용할 수 있는 곳에서는 아래와 같은 특징이 있다.
    • 함수 구현의 거의 똑같다.
    • 함수 이름이 구현의 차이를 만든다.
  • 위 3가지 메서드를 보면 Price, Quantity, Shipping이라는 메서드 내의 단어가 구현의 차이를 만든다.
  • 이 단어들이 암묵적인 인자를 뜻한다.

function setFieldByName(cart, name, field, value) {
  var item = cart[name];
  var newItem = objectSet(item, field, value);
  var newCart = objectSet(cart, name, newItem);
  return newCart;
}
  • 인묵적 인자를 메서드 명에서 드러내고 메서드 파라미터로 바꾸면 3개의 개별적인 메서드는 하나의 메서드로 리팩터링이 가능하다.
  • 이는 메서드 명에 존재하던 필드명이라는 인자를 일급값으로 만든 것.
    • 일급값: 함수에 인자로 넘길 수 있고, 함수 리턴값으로 사용할 수 있고, 변수에 넣을 수 있는 값을 뜻한다.
  • 단, field 값의 정확성을 위해서는 컴파일 타임 혹은 런타임에 검사하는 로직을 추가하면 좋다.
    • 컴파일 타임: 정적 타입 시스템에서 Java의 Enum과 같은 타입을 이용하면 된다.
    • 런타임: Set 구조에 가능한 field를 담아두고 매 요청마다 검사를 진행한다.

리팩터링 기법 2: 본문을 콜백으로 바꾸기

function cookAndEatFoods() {
  for (var i = 0; i < foods.length; i++) {
    var food = foods[i];
    cook(food);
    eat(food);
  }
}
 
function cleanDishes() {
  for (var i = 0; i < dishes.length; i++) {
    var dish = dishes[i];
    wash(dish);
    dry(dish);
    putAway(dish);
  }
}
  • 2개의 메서드는 반복문을 포함한 비슷한 로직을 수행한다.
  • 우선 1차적으로 리팩터링 기법 1을 적용해 암묵적 인자를 드러내보자

function cookAndEatArray(array) {
  for (var i = 0; i < array.length; i++) {
    var item = array[i];
    cook(item);
    eat(item);
  }
}
 
function cleanArray(array) {
  for (var i = 0; i < array.length; i++) {
    var item = array[i];
    wash(item);
    dry(item);
    putAway(item);
  }
}
  • 암묵적 인자 드러내기를 통해 Foods와 Dishes를 일급값(array)로 변경했다.
  • 하지만 메서드 명에는 여전히 암묵적 인자가 존재해 보인다.
  • 이번 인자는 특정 데이터가 아닌 동작을 뜻하기 때문에 해당 동작을 드러내 변수로 변경해보자

function forEach(array, f) {
  for (var i = 0; i < array.length; i++) {
    var item = array[i];
    f(item);
  }
}
 
function cookAndEat(food) {
  cook(food);
  eat(food);
}
 
function clean(dish) {
  wash(dish);
  dry(dish);
  putAway(dish);
}
  • 이렇게 리팩터링이 마무리 됐다.
  • 결과적으로 forEach()는 고차 함수로 정의되었다.
    • 고차 함수: 인자로 함수를 받거나 함수를 리턴하는 것
  • 지금까지 진행된 본문을 콜백으로 바꾸기 리팩터링은 아래와 같은 단계를 거친다.
    • 본문과 본문의 앞/뒤를 구분한다.
    • 본문을 함수로 빼낸다.
    • 빼낸 본문을 함수 인자로 전달한다.
  • 이번 장에서는 2가지 리팩터링 방법과 일급 값, 일급 함수, 고차 함수라는 개념을 배웠다.
  • 앞으로 이 개념들을 활용하는 것의 강점에 대해 설명해줄듯
만혁

일급 함수 1

코드의 냄새: 함수 이름에 있는 암묵적 인자

  • 특징
    • 거의 똑같이 구현된 함수가 있다.
    • 함수 이름이 구현에 있는 다른 부분을 가리킨다.

리팩터링: 암묵적 인자를 드러내기

  • 단계
    1. 함수 이름에 있는 암묵적 인자를 확인한다.
    2. 명시적인 인자를 추가한다.
    3. 함수 본문에 하드 코딩된 값을 새로운 인자로 바꾼다.
    4. 함수를 호출하는 곳을 고친다.

리팩터링: 함수 본문을 콜백으로 바꾸기

  • 단계
    1. 함수 본문에서 바꿀 부분의 앞부분과 뒷부분을 확인한다.
    2. 리팩터링 할 코드를 함수로 빼낸다.
    3. 빼낸 함수의 인자로 넘길 부분을 또 다른 함수로 빼낸다.

암묵적 인자 드러내기

// as-is
function setPriceByName(cart, name, price) {
    const item = cart[name];
    const newItem = objectSet(item, 'price', price);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}
 
function setQuantityByName(cart, name, quantity) {
    const item = cart[name];
    const newItem = objectSet(item, 'quantity', quantity);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}
 
cart = setPriceByName(cart, "shoe", 13);
cart = setQuantityByName(cart, "shoe", 3);
cart = setShippingByName(cart, "shoe", 0);
cart = setTaxByName(cart, "shoe", 2.34);
 
 
// to-be
function setFieldByName(cart, name, field, value) {
    const item = cart[name];
    const newItem = objectSet(item, field, value);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}
 
 
cart = setFieldByName(cart, "shoe", "price", 13);
cart = setFieldByName(cart, "shoe", "quantity", 3);
cart = setFieldByName(cart, "shoe", "shipping", 0);
cart = setFieldByName(cart, "shoe", "tax", 2.34);
연습 문제

함수 이름에 있는 암묵적 인자 드러내기

// as-is
function multiplyByFour(emplyoees) {
    return x * 4;
}
 
function multiplyByPir(emplyoees) {
    return x * 4;
}
 
// to-be
function multiply(x, y) {
    return x * y;
}
 
// as-is
function incrementQuantityByName(cart, name) {
    const item = cart[name];
    const quantty = item['quantity'];
    const newQuantity = quantity + 1;
    const newItem = objectSet(item, 'quantity', newQuantity);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}
 
function incrementSizeByName(cart, name) {
    const item = cart[name];
    const quantty = item['size'];
    const newSize = size + 1;
    const newItem = objectSet(item, 'size', newSize);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}
 
// to-be
function incrementFieldByName(cart, name, field) {
    const item = cart[name];
    const value = item[field];
    const newValue = value + 1;
    const newItem = objectSet(item, field, newValue);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}
// as-is
function incrementFieldByName(cart, name, field) {
    const item = cart[name];
    const value = item[field];
    const newValue = value + 1;
    const newItem = objectSet(item, field, newValue);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}
 
// to-be
function incrementFieldByName(cart, name, field) {
    const supports = ['size', 'quantity'];
    if (!supports.includes(field)) {
        throw "not supported field"
    }
    const item = cart[name];
    const value = item[field];
    const newValue = value + 1;
    const newItem = objectSet(item, field, newValue);
    const newCart = objectSet(cart, name, newItem);
    return newCart;
}

함수 본문을 콜백으로 바꾸기

 
// as-is
try { // 앞부분
    saveUserData(user); // 본문
} catch (error) { // 뒷부분
    logToSnapErrors(error); // 뒷부분
} // 뒷부분
 
try { // 앞부분
    fetchProduct(id); // 본문
} catch (error) { // 뒷부분
    logToSnapErrors(error); // 뒷부분
} // 뒷부분
 
 
// to-be
// 1. 함수로 빼내기
function withLogging() {
    try { 
        saveUserData(user); 
    } catch (error) { 
        logToSnapErrors(error);
    } 
}
 
// 2. 콜백으로 빼내기
function withLogging(f) {
    try {
        f();
    } catch (error) {
        logToSnapErrors(error);
    }
}
 
withLogging(function() { saveUserData(user) }) // 인라인으로 함수 선언
withLogging(() => saveUserData(user)) // 이것도 되겠지~

회독

  • 코드를 일단 작성하고 책에서 말하는 순서대로 리팩토링하는게
  • 최대한 안전하게 변경하는 방법인거 같다
  • 로깅 함수의 경우 활용도가 꽤 높아보임ㅎㅎ