[개발 도서] 리팩터링 2판 chapter11. API 리팩터링

호박쿵야·2022년 2월 23일
0

1. 질의 함수와 변경 함수 분리하기

겉보기에 side effect 가 있는 함수와 없는 함수는 명확히 구분하는 것이 좋다 .이를 위한 한 가지 방법은 '질의 함수(읽기 함수)는 모두 부수효과가 없어야 한다'는 규칙을 따르는 것이다. 이를 명령-질의 분리라 한다. 값을 반환하면서도 부수효과도 있는 함수를 발견하면 상태를 변경하는 부분과 질의하는 부분을 분리하려 시도한다! (무조건)

  1. 기존 함수를 복제하고 질의 목적에 맞는 이름 짓기
  2. 새 질의 함수에서 부수효과 부분 삭제하기
function getTitalOutstandingAndSendBill(){
    const result = customer.invoices.reduce((total, each)=> each.amount + total, 0)
    sendBill();
    return result
}

//refactored
function totalOutstanding() {
   return customer.invoices.reduce((total, each)=> each.amount + total, 0)
}
function sendBill() {
    emailGeteway.send(formatBill(customer))
}

2. 함수 매개변수화하기

두 함수의 로직이 아주 비슷하고 단지 리터럴 값만 다르다면, 그 다른 값만 매개변수로 받아 처리하는 함수 하나로 함쳐서 중복을 없앨 수 있다.

function tenPercentRaise(aPerson) {
  aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
  aPerson.salary = aPerson.salary.multiply(1.05);
}
//refactored
function raise(aPerson, factor) {
  aPerson.salary = aPerson.salary.multiply(1 + factor);
}

3. 플래그 인수 제거하기

플래그 인수란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수다.

필자가 플래그 인수를 싫어하는 이유는 호출할 수 있는 함수들이 무엇이고 어떻게 호출해야 하는지를 이해하기가 어려워지기 때문이다. 플래그 인수가 있으면 함수들의 기능 차이가 잘 드러나지 않는다. boolean 플래그는 특히나 코드를 읽는 이에게 뜻을 온전히 전달하지 못하기 때문에 더욱 좋지 못하다. 그래서 다음과 같이 특정한 기능 하나만 수행하도록 명시적인 함수들 제공하는 편이 훨씬 깔끔하다.

함수 하나에서 플래그 인수를 2개 이상 사용하면 플래그 인수를 써야 하는 합당한 근거가 될 수 있다. 플래그 인수 없이 구현하려면 플래그 인수들의 가능한 조합만큼 함수를 만들어야 하기 때문이다.
다른 관점에서 봤을 때 플래그 인수가 둘 이상이면 함수 하나가 너무 많은 일을 처리하고 있다는 신호이기 때문에 더 간단한 함수를 만들 방법을 고민해봐야한다.

function setDimension(name, value){
    if(name === 'height'){
        this._height = value;
        return
    }
    if(name==='width'){
        this._width = value
        return
    }
}
//refactored
function setHeight(value){this._height=value}
function setWidth(value){this._width}

4. 객체 통째로 넘기기

하나의 레코드에서 값 두어 개를 가져와 인수로 넘기는 코드를 보면, 필자는 그 값들 대신 레코드를 통째로 넘기고 함수 본문에서 필요한 값들을 꺼내 쓰도록 수정하곤 한다.
레코드를 통째로 넘기면 변화에 대응하기 쉽다. 그 함수가 더 다양한 데이터를 사ㅏ용하도록 바뀌어도 매개변수 목록은 수정할 필요가 없다. 그리고 매개변수 목록이 짧아져서 일반적으로 함수 사용법을 이해하기 쉬워진다.

const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if(aPlan.withinRange(low, high))

//refactored

if(aPlan.withinRange(aRoom.daysTempRange))

5. 매개변수를 질의 함수로 바꾸기

매개변수 목록은 함수의 변동 요인을 모아놓은 곳이다. 즉, 함수의 동작에 변화를 줄 수 있는 일차적인 수단이다. 다른 코드와 마찬가지로 중복은 피하는 게 좋으며 짧을수록 이해하기 쉽다.

피호출 함수가 스스로 '쉽게' 결정할 수 있는 값을 매개변수로 건네는 것도 일종의 '중복'이다.

매개변수의 유무에 따라 값을 결정하는 책임 주체가 달라지는데, 매개변수가 있다면 결정 주체가 호출자가 되고, 매개변수가 없다면 피호출 함수가 된다. 때에 따라 매개변수를 제거하면 피호출 함수에 원치 않는 의존성이 생길 때 매개변수를 질의 함수로 바꾸지 말아야 할 상황이 생기기도 한다.

availableVacation(anEmployee, anEmployee.grade)
function availableVacation(anEmployee, grage){
    //연휴 계산
} 

//refactored
availableVacation(anEmployee)
function availableVacation(anEmployee){
    const grade = anEmployee.grade
   
} 

6. 질의 함수를 매개변수로 바꾸기

똑같은 값을 건네면 매번 똑같은 결과를 내는 함수는 다루기 쉽다. 이런 성질을 '참조 투명성'이라 하는데, 참조 투명하지 않은 원소에 접근하는 모든 함수는 참조 투명성을 잃게 된다. 이 문제는 해당 원소를 매개변수로 바꾸면 해결된다.

이 리팩터링을 통해 프로그램의 일부분을 순수함수로 바꿀 수 있으며, 그 부분은 테스트하거나 다루기 쉬워진다.
단점으로는 질의 함수를 매개변수로 바꾸면 어떤 값을 제공할지를 호출자가 알아내야 한다.

targetTemplature(aPlan)

function targetTemplature(aPlan){
    currentTemplature = thermostat.currentTemplature
    //...
}

//refactored

targetTemplature(aPlan, thermostat.currentTemplature)

function targetTemplature(aPlan,currentTemplature){

    //...
}

7. setter 제거하기

setter 메서드가 있다는 것은 필드가 수정될 수 있다는 뜻이다. 만약 객체 생성 후 수정되지 않길 원하는 필드라면 setter를 제공하지 않았을 것이다.

setter 제거하기 리팩터링이 필요한 상황은 주로 두가지이다. 첫 번째는 사람들이 무조건 접근자 메서드를 통해서만 필드를 다루려고 할 때다.
두 번째는 클라이언트에서 생성 스크립트를 사용해 객체를 생성할 때다. 생성 스크립트란 생성자를 호출한 후 일련의 setter를 호출하여 객체를 완성하는 형태의 코드를 말한다.

ex) Person이라는 클래스에 name, id, age 등의 속성이 있다. name과 age는 객체 생성 뒤에 바뀔 수 있지만 id는 그러면 안된다. -> id setter 없애기

class Person{
    get name(){}
    set name(aString){}
}

//refactored

class Person{
    get name(){}
}

8. 생성자를 팩터리 함수로 바꾸기

많은 객체 지향 언어에서 제공하는 생성자는 객체를 초기화하는 특별한 용도의 함수이다. 실제로 새로운 객체를 생성할 때면 주로 생성자를 호출한다. 하지만 생성자에는 일반 함수에는 없는 이상한 제약이 붙기도 한다.
팩터리 함수에는 그러한 제약이 없다. 팩터리 함수를 구현하는 과정에서 생성자를 호출할 수는 있지만, 원한다면 다른 무언가로 대체할 수 있다.

leadEngineer = new Employee(document.leadEngineer, 'E')
//refactored
leadEngineer = ceateEngineer(document.leadEngineer)

9. 함수를 명령으로 바꾸기

함수는 프로그래밍의 기본적인 빌딩 블록 중 하나다. 그런데 함수를 그 함수만을 위한 객체 안으로 캡슐화하면 더 유용해지는 상황이 있다. 이런 객체를 가리켜 '명령 객체' 또는 '명령'이라고 한다. 명령 객체 대부분은 메서드 하나로 구성되며, 이 메서드를 요청해 실행하는 것이 이 객체의 목적이다.

function score(candinate, medicalExam, scoringGuide){
    let result = 0; 
    let healthLevel = 0; 
    //...
}

//refactored

class Score{
    constructor(candinate, medicalExam, scoringGuide){
        this._candinate = candinate
        this._medicalExam = medicalExam
        this._scoringGuide = scoringGuide
    }

    execute(){
        this._result = 0;
        this._healthLevel = 0;
        //...
    }
}

10. 명령을 함수로 바꾸기

명령 객체는 복잡한 연산을 다룰 수 잇는 강력한 메커니즘을 제공한다. 큰 연산 하나를 여러 개의 작은 메서드로 쪼개고 필드를 이용해 쪼개짐 메서드들끼리 정보를 공유할 수 있다. 또한 어떤 메서드를 호출하냐에 따라 다른 효과를 줄 수 있고 단계를 거치며 데이터를 조금씩 완성해나갈 수 있다.

명령은 그저 함수를 하나 호출해 정해진 일을 수행하는 용도로 쓰인다. 이런 상황이고 로직이 크게 복잡하지 않다면 명령 객체는 장점보다 단점이 크니 평범한 함수로 바꿔주는게 낫다.

class ChargeCalculator{
  constructor(customer, usage){
    this._customer = customer;
    this._usage= usage;
  }
  execute(){
    return this._customer.rate*this._usage
  }
}

//refactored
function charge(customer, usage){
	return customer.rate *  usage
}

11. 수정된 값 반환하기

데이터가 어떻게 수정되는지를 추적하는 일은 코드에서 이해하기 가장 어려운 일 중 하나다. 특히 같은 데이터 블록을 읽고 수정하는 코드가 여러 곳이라면 데이터가 수정되는 흐름과 코드의 흐름을 일치시키기가 어렵다.

변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담아두도록 하는 것이 좋은 방법이 될 수 있다.

이 리팩터링은 값 하나를 계산한다는 분명한 목적이 있는 함수들에 가장 효과적이고, 반대로 값 여러 개를 갱신하는 함수에는 효과적이지 않다.

let totalAscent = 0;
calculateAscent();

function calculateAscent(){
    for(let i = 1; i<points.length; i++){
        const verticalCharge = points[i].elevation - points[i-1].elevation
        totalAscent += (verticalCharge > 0 ) ? verticalCharge : 0;
    }
}

//refactored

const totalAscent = calculateAscent();
function calculateAscent(){
    let result = 0;
    for(let i = 1; i<points.length; i++){
        const verticalCharge = points[i].elevation - points[i-1].elevation
        result += (verticalCharge > 0 ) ? verticalCharge : 0;
    }
    return result
}

12. 오류 코드를 예외로 바꾸기

예외는 독자적인 흐름이 있어서 프로그램의 나머지에서는 오류 방생에 따른 복잡한 상황에 대처하는 코드를 작성하거나 읽을 일이 없게 해준다.

예외는 정확히 예상 밖의 동작일때만 쓰여야 한다. 예외를 던지는 코드를 프로그램 종료 코드로 바꿔도 프로그램이 여전히 정상 동작할지를 따져보는 것이다. 정상 동작하지 않을 것 같다면 예외를 사용하지 말라는 신호다. 예외 대신 오류를 검출하여 프로그램을 정상 흐름으로 되돌리게끔 처리해야 한다.

if (data) {
  return new ShippingRules(data);
} else {
  return -23;
}

//refactored
if (data) {
  return new ShippingRules(data);
} else {
  throw new OrderProcessError(-23);
}

13. 예외를 사전확인으로 바꾸기

예외는 '뜻밖의 오류'라는, 말 그대로 예외적으로 동작할 때만 쓰여야 한다. 함수 수행 시 문제가 될 수 있는 조건을 함수 호출 전에 검사할 수 있다면, 예외를 던지는 대신 호출하는 곳에서 조건을 검사하도록 해야한다.

double getValueForPeriod(int periodNumber){
    try{
        return values[periodNumber]
    }catch(ArrayIndexOutOdBoundsExeption e){
        return 0;
    }
}

//refactored 
double getValueForPeriod(int periodNumber){ 
   return (periodNumber >= values.length ) ? 0 : values[periodNumber]
    
}

0개의 댓글