기능 이동

함수 옮기기

//리팩토링 전
function trackSummary(points) {
  const totalTime = calculateTime();
  const totalDistance = calculateDistance();
  const pace = totalTime / 60 / totalDistance;
  return {
    time: totalTime,
    distance: totalDistance,
    pace: pace,
  };

  function calculateDistance() {
    let result = 0;
    for (let i = 1; i < points.length; i++) {
      result += distance(points[i - 1], points[i]);
    }
    return result;
  }

  function distance(p1, p2) {
    ...
  }

  function radians(degrees) {
    return (degrees * Math.PI) / 180;
  }

  function calculateTime() {
    return 10000;
  }
}
//리팩토링 후
function trackSummary(points) {
  const time = calculateTime();
  const distance = totalDistance(points)
  const pace = time / 60 / distance;
  return {
    time,
    distance,
    pace: pace,
  };
}

function calculateTime() {
  return 10000;
}

function totalDistance(points) {
  let result = 0;
  for (let i = 1; i < points.length; i++) {
    result += distance(points[i - 1], points[i]);
  }
  return result;
}

function distance(p1, p2) {
 ...
}

function radians(degrees) {
  return (degrees * Math.PI) / 180;
}

좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있느냐를 뜻하는 모듈성이다. 서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 하면 프로그램의 어딘가를 수정하려 할때 해당 기능과 깊이 관련된 작은 일부만 이해해도 가능하게 해준다. 다른 함수 안에서 도우미 역할로 정의된 함수 중 독립적으로도 고유한 가치가 있는 것은 접근하기 더 쉬운 장소로 옮기는 게 낫다.



필드 옮기기

//리팩토링 전
class Account {
  constructor(number, type, interestRate) {
    this._number = number;
    this._type = type;
    this._interestRate = interestRate;
  }

  get interestRate() {
    return this._interestRate;
  }
}

class AccountType {
  constructor(nameString) {
    this._name = nameString;
  }
}
//리팩토링 후
class Account {
  constructor(number, type) {
    this._number = number;
    this._type = type;
  }

  get interestRate() {
    return this._type.interestRate;
  }
}

class AccountType {
  constructor(nameString, interestRate) {
    this._name = nameString;
    this._interestRate = interestRate;
  }

  get interestRate() {
    return this._interestRate;
  }
}

주어진 문제에 적합한 데이터 구조를 활용하면 동작 코드는 자연스럽게 단순하고 직관적으로 짜여진다. 현재 데이터 구조가 적절치 않음을 깨닫게 되면 곧바로 수정해야 한다. 어떤 레코드를 넘길 때마다 또 다른 레코드의 필드도 함께 넘기고 있다면 데이터 위치를 옮겨야 한다.

한 레코드를 변경하려 할 때 다른 레코드의 필드까지 변경해야만 한다면 필드의 위치가 잘못되었다는 신호이다. 구조체 여러 개에 정의된 똑같은 필드들을 갱신해햐 한다면 한 번만 갱신해도 되는 다른 위치로 옮기는 것이 좋다.



문장을 함수로 옮기기

//리팩토링 전
function renderPerson(person) {
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(renderPhoto(person.photo));
  result.push(`<p>title: ${person.photo.title}</p>`);
  result.push(emitPhotoData(person.photo));
  return result.join('\n');
}

function photoDiv(p) {
  return ['<div>', `<p>title: ${p.title}</p>`, emitPhotoData(p), '</div>'].join(
    '\n'
  );
}

function emitPhotoData(aPhoto) {
  const result = [];
  result.push(`<p>location: ${aPhoto.location}</p>`);
  result.push(`<p>date: ${aPhoto.date.toDateString()}</p>`);
  return result.join('\n');
}

function renderPhoto(aPhoto) {
  return '';
}
//리팩토링 후
function renderPerson(person) {
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(renderPhoto(person.photo));
  result.push(emitPhotoData(person.photo));
  return result.join('\n');
}

function photoDiv(photo) {
  return ['<div>', emitPhotoData(photo), '</div>'].join('\n');
}

function emitPhotoData(aPhoto) {
  const result = [];
  result.push(`<p>title: ${person.photo.title}</p>`);
  result.push(`<p>location: ${aPhoto.location}</p>`);
  result.push(`<p>date: ${aPhoto.date.toDateString()}</p>`);
  return result.join('\n');
}

function renderPhoto(aPhoto) {
  return '';
}

특정 함수를 호출하는 코드가 나올 때마다 그 앞이나 뒤에서 똑같은 코드가 추가로 실행되는 모습을 보면, 그 반복되는 부분을 피호출 함수로 합치는 방법을 궁리해야 한다. 추후 반복되는 부분에서 무언가 수정할 일이 생겼을 때 단 한 곳만 수정하면 된다.



문장을 호출한 곳으로 옮기기

//리팩토링 전
function renderPerson(outStream, person) {
  outStream.write(`<p>${person.name}</p>\n`);
  renderPhoto(outStream, person.photo);
  emitPhotoData(outStream, person.photo);
}

function listRecentPhotos(outStream, photos) {
  photos
    .filter((p) => p.date > recentDateCutoff())
    .forEach((p) => {
      outStream.write('<div>\n');
      emitPhotoData(outStream, p);
      outStream.write('</div>\n');
    });
}

function emitPhotoData(outStream, photo) {
  outStream.write(`<p>title: ${photo.title}</p>\n`);
  outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
  outStream.write(`<p>location: ${photo.location}</p>\n`);
}
//리팩토링 후
function renderPerson(outStream, person) {
  outStream.write(`<p>${person.name}</p>\n`);
  renderPhoto(outStream, person.photo);
  emitPhotoData(outStream, person.photo);
  outStream.write(`<p>location: ${photo.location}</p>\n`);
}

function listRecentPhotos(outStream, photos) {
  photos
    .filter((p) => p.date > recentDateCutoff())
    .forEach((p) => {
      outStream.write('<div>\n');
      emitPhotoData(outStream, p);
      outStream.write(`<p>다른 형식의 위치: ${photo.location}</p>\n`);
      outStream.write('</div>\n');
    });
}

function emitPhotoData(outStream, photo) {
  outStream.write(`<p>title: ${photo.title}</p>\n`);
  outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
}

함수는 프로그래머가 쌓아 올리는 추상화의 기본 빌딩 블록이다. 그런데 추상화라는 것이 그 경계를 항상 올바르게 긋기가 만만치 않다. 코드베이스의 기능 범위가 달라지면 추상화의 경계도 움직이게 되는데, 초기에는 응집도 높고 한 가지 일만 수행하던 함수가 어느새 둘 이상의 다른 일을 수행하게 바뀔 수 있다.



인라인 코드를 함수 호출로 바꾸기

//리팩토링 전
let appliesToMass = false;
for (const s of states) {
  if (s === 'MA') appliesToMass = true;
}
//리팩토링 후
const appliesToMass = states.includes('MA');

함수는 여러 동작을 하나로 묶어준다. 함수의 이름이 코드의 동작 방식보다는 목적을 말해주기 때문에 함수를 활용하면 코드를 이해하기 쉬워진다.



문장 슬라이드하기

//리팩토링 전
// 예제 1
const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;

// 예제 2
function someFunc() {
  let result;
  if (availableResources.length === 0) {
    result = createResource();
    allocatedResources.push(result);
  } else {
    result = availableResources.pop();
    allocatedResources.push(result);
  }
  return result;
}
//리팩토링 후
// 예제 1
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder();
let charge;

// 예제 2
function someFunc() {
  const result = availableResources.length === 0
    ? createResource()
    : availableResources.pop();
  allocatedResources.push(result);
  return result;
}

관련된 코드들이 가까이 모여 있다면 이해하기가 더 쉬워지는데, 이 작업은 다른 리팩터링의 준비 단계로 자주 행해진다. 관련있는 코드들을 명확히 구분되는 함수로 추출하는 게 그저 문장들을 한데로 모으는 것보다 나은 분리법이다.



반복문 쪼개기

//리팩토링 전
function reportYoungestAgeAndTotalSalary(people) {
  let youngest = people[0] ? people[0].age : Infinity;
  let totalSalary = 0;
  for (const p of people) {
    if (p.age < youngest) youngest = p.age;
    totalSalary += p.salary;
  }

  return `youngestAge: ${youngest}, totalSalary: ${totalSalary}`;
}
//리팩토링 후
function reportYoungestAgeAndTotalSalary(people) {
  return `youngestAge: ${youngestAge()}, totalSalary: ${totalSalary()}`;

  function youngestAge () {
    return Math.min(...people.map(person => person.age));
  }

  function totalSalary () {
    return people.reduce((total, person) => total + person.salary, 0);
  }
}

종종 반복문 하나에서 두 가지 일을 수행하는 모습을 보게 된다. 이 경우 가각의 반복문으로 분리해두면 수정할 동작 하나만 이해하면 된다. 반복문을 두 번 실행해야 하므로 이 리팩터링을 불편해할 수 도 있는데 최적화와 리팩터링을 분리하자. 오히려 반복문 쪼개기가 다른 더 강력한 최적화를 적용할 수 있는 경우도 있다.



반복문을 파이프라인으로 바꾸기

//리팩토링 전
function acquireData(input) {
  const lines = input.split('\n');
  let firstLine = true;
  const result = [];
  for (const line of lines) {
    if (firstLine) {
      firstLine = false;
      continue;
    }
    if (line.trim() === '') continue;
    const record = line.split(',');
    if (record[1].trim() === 'India') {
      result.push({ city: record[0].trim(), phone: record[2].trim() });
    }
  }
  return result;
}
//리팩토링 후
function acquireData(input) {
  return input
    .split('\n')
    .slice(1)
    .filter(line => line.trim() !== '')
    .map(line => line.split(','))
    .filter(records => records[1].trim() === 'India')
    .map(records => ({
      city: records[0].trim(),
      phone: records[2].trim(),
    }));
}

반복문 대신에 파이프라인을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다. 객체가 파이프라인을 따라 흐르며 어떻게 처리되는지를 읽을 수 있기 때문에 훨씬 이해하기 쉬워진다.

profile
개발자로 성장하기

0개의 댓글