[리팩터링] 6. 조건부 로직 간소화

안광의·2022년 10월 18일
0
post-thumbnail

조건부 로직 간소화

조건문 분해하기

//리팩토링 전
function calculateCharge(date, quantity, plan) {
  let charge = 0;
  if (!date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd))
    charge = quantity * plan.summerRate;
  else charge = quantity * plan.regularRate + plan.regularServiceCharge;
  return charge;
}
//리팩토링 후
function calculateCharge(date, quantity, plan) {
  return isSummer() ? summerCharge() : regularCharge();

  function isSummer() {
    return !date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd)
  }

  function summerCharge() {
    return quantity * plan.summerRate;
  }

  function regularCharge() {
    return quantity * plan.regularRate + plan.regularServiceCharge;
  }
}

복잡한 조건부 로직은 프로그램을 복잡하게 만드는 가능 흔한 원흉에 속한다. 조건을 검사하고 그 결과에 따른 동작을 표현한 코드는 무슨 일이 일어나는지는 이야기해주지만 왜 일어나는지는 제대로 말해주지 않는다. 코드를 부위별로 분해한 다음 해체된 코드 덩어리들을 각 덩어리의 의도를 살린 이름의 함수 호출로 바꿔주면 전체적인 의도가 더 확실이 드러난다.



조건식 통합하기

//리팩토링 전
function disabilityAmount(employee) {
  if (employee.seniority < 2) return 0;
  if (employee.monthsDisabled > 12) return 0;
  if (employee.isPartTime) return 0;
  return 1;
}
//리팩토링 후
function disabilityAmount(employee) {
  return isNotEligibleForDisability(employee) ? 0 : 1;

}

function isNotEligibleForDisability(employee) {
  return employee.seniority < 2 || employee.monthsDisabled > 12 || employee.isPartTime;
}

같은 일을 하는 코드라면 조건 검사도 하나로 통합하는 게 낫다. 조건부 코드를 통합하는 중요하면 여러 조각으로 나뉜 조건들을 하나로 통합함으로써 내가 하려는 일이 더 명확해지고 함수 추출하기까지 이어질 가능성이 높다. 복잡한 조건식을 함수로 추출하면 코드의 의도가 훨씬 분명하게 드러나느 경우가 많다.



중첩 조건문을 보호 구문으로 바꾸기

//리팩토링 전
function adjustedCapital(instrument) {
  let result = 0;
  if (instrument.capital > 0) {
    if (instrument.interestRate > 0 && instrument.duration > 0) {
      result =
        (instrument.income / instrument.duration) *
        anInstrument.adjustmentFactor;
    }
  }
  return result;
}
//리팩토링 후
function adjustedCapital(instrument) {
  if (!isEligibleForAdjustedCapital()) {
      return 0
    }

  return (instrument.income / instrument.duration) * anInstrument.adjustmentFactor;
}

function isEligibleForAdjustedCapital(instrument) {
  return instrument.capital > 0 &&
    instrument.interestRate > 0 &&
    instrument.duration > 0;
}

조건문은 참인 경로와 거짓인 경로 모두 정상 동작으로 이어지느 형태와, 한쪽만 정상인 형태 두가지로 쓰인다. 두 형태는 의도하는 바가 서로 다르므로 그 의도가 코드에 드러나야 하느데 두 경로 모두 정상 동작이라면 ifd와 else절을, 한쪽만 정상이라면 비정상 조건을 if에서 검사하는 방식을 사용하는 것이 좋다. 두 번째 검사 형태를 흔히 보호 구문이라고 하는데 중첩 조건문을 보호 구문으로 바꾸기 리팩터링의 핵심은 의도를 부각하는데 있다.



조건부 로직을 다형성으로 바꾸기

//리팩토링 전
function plumages(birds) {
  let map = birds.map((b) => [b.name, plumage(b)]);
  let map1 = new Map(map);
  return map1;
}
function speeds(birds) {
  return new Map(birds.map((b) => [b.name, airSpeedVelocity(b)]));
}
function plumage(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return 'average';
    case 'AfricanSwallow':
      return bird.numberOfCoconuts > 2 ? 'tired' : 'average';
    case 'NorwegianBlueParrot':
      return bird.voltage > 100 ? 'scorched' : 'beautiful';
    default:
      return 'unknown';
  }
}
function airSpeedVelocity(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return 35;
    case 'AfricanSwallow':
      return 40 - 2 * bird.numberOfCoconuts;
    case 'NorwegianBlueParrot':
      return bird.isNailed ? 0 : 10 + bird.voltage / 10;
    default:
      return null;
  }
//리팩토링 후
class Bird {
  constructor(bird) {
    Object.assign(this, bird);
  }

  get name() {
    return this.name;
  }

  get plumage() {
    return 'unknown';
  }

  get airSpeedVelocity() {
    return null;
  }
}

class EuropeanSwallow extends Bird {
  get plumage() {
    return 'average';
  }

  get airSpeedVelocity() {
    return 35;
  }
}

class AfricanSwallow extends Bird {
  get plumage() {
    return this.numberOfCoconuts > 2 ? 'tired' : 'average';
  }

  get airSpeedVelocity() {
    return 40 - 2 * this.numberOfCoconuts;
  }
}

class NorwegianBlueParrot extends Bird {
  get plumage() {
    return this.voltage > 100 ? 'scorched' : 'beautiful'
  }

  get airSpeedVelocity() {
    return this.isNailed ? 0 : 10 + this.voltage / 10;
  }
}

const createBird = (bird) => {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return new EuropeanSwallow(bird);
    case 'AfricanSwallow':
      return new AfricanSwallow(bird);
    case 'NorwegianBlueParrot':
      return new NorwegianBlueParrot(bird);
    default:
      return new Bird(bird);
  }
}

function plumages(birds) {
  return new Map(
    birds.map((b) => createBird(b))
    .map((b) => [b.name, b.plumage])
  );
}
function speeds(birds) {
  return new Map(
    birds.map((b) => createBird(b))
    .map((b) => [b.name, b.airSpeedVelocity])
  );
}

조건부 로직을 직관적으로 구조화할 방법을 항상 고민해야 하는데 클래스와 다형성을 이용하면 더 확실하게 분리할 수 있다. 특수한 상황을 다루는 로직들을 기본 동작에서 분리하기 위해 상속과 다형성을 이용할 수 있다.



특이 케이스 추가하기

//리팩토링 전
export class Site {
  constructor(customer) {
    this._customer = customer;
  }

  get customer() {
    return this._customer;
  }
}

export class Customer {
  constructor(name) {
    this._name = name;
  }

  get name() {
    return this._name;
  }

  get billingPlan() {
    //
  }

  set billingPlan(arg) {
    //
  }

  get paymentHistory() {
    //
  }
}

export function customerName(site) {
  const aCustomer = site.customer;
  ...
  let customerName;
  if (aCustomer === 'unknown') customerName = 'occupant';
  else customerName = aCustomer.name;

  return customerName;
}
//리팩토링 후
export class Customer {
  constructor(name) {
    this._name = name; 
  }
  
  get name() {
    return this._name;
  }
  
  get billingPlan() {
    //
  }
  
  set billingPlan(arg) {
    //
  }
  
  get paymentHistory() {
    //
  }
}

class UnknownCustomer extends Customer {
  get name() {
    return 'occupant';
  }
}
export class Site {
  constructor(customer) {
    this._customer = customer;
  }

  get customer() {
    return this._customer === 'unknown' ? new UnknownCustomer() : new Customer(this._customer);
  }
}

export function customerName(site) {
  const aCustomer = site.customer;
  ...
  customerName = aCustomer.name;

  return customerName;
}

특정 값에 대해 똑같이 반응하는 코드가 여러 곳이라면 그 반응들을 한 데로 모으는게 효율적이다. 특수한 경우에 공통 동작을 요소 하나에 모아서 사용하는 특이 케이스 패턴이라는 것이 있는데, 단순히 데이터를 읽기만 한다면 반환할 값들ㅇ르 담은 리터럴 객체 형태롤 준비하면 되고 그 이상의 동작을 수행한다면 필요한 메서드를 담은 객체를 생성하면 된다.



어서션 추가하기

//리팩토링 전
class Customer {
  constructor() {
    this.discountRate = 0;
  }
  applyDiscount(number) {
    return this.discountRate ? number - this.discountRate * number : number;
  }
}
//리팩토링 후
import { strict as assert } from 'node:assert';

class Customer {
  constructor() {
    this.discountRate = 0;
  }
  applyDiscount(number) {
    assert(number >= 0);
    return this.discountRate ? number - this.discountRate * number : number;
  }
}

특정 조건이 참일 때만 제대로 동작하느 코드 영역이 있을 수 있는데 어셔션은 항상 참이라고 가정하는 조건부 문장으로, 어서션이 실패했다는 건 프로그래머가 잘 못했다는 뜻이다. 어서션은 프로그램이 어떤 상태임을 가정한 채 실행되는지를 다른 갭라자에게 알려주는 훌륭한 소통 도구이다.



제어 플래그를 탈출문으로 바꾸기

//리팩토링 전
for (const p of people) {
  if (!found) {
    if (p === 'Don') {
      sendAlert();
      found = true;
    }
  }
//리팩토링 후
for (const p of people) {
  if (p === 'Don') {
    sendAlert();
    break;
  }
}

if(people.includes('Don')) {
  sendAlert();
}

제어 플러그란 코드의 동작을 변경하는 데 사용되는 변수를 말하는데 제어 플러그보다는 함수에서 할 일을 마쳤다면 그 사실을 return 문으로 명확하게 알리는 편이 낫다.

profile
개발자로 성장하기

0개의 댓글