refactoring 5: Basic refactoring way

가장 기본적이고 많이 사용해서 제일 먼저 배워야 하는 리팩터링
refactoring

가장 기본적이고 많이 사용해서 제일 먼저 배워야 하는 리팩터링

1. 함수 추출하기


가장 빈번하게 사용되는 리팩터링 중 하나로, 코드조각을 찾아 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙인다.

배경

절차

예시

function printOwing(invoice) {
	let outstanding = 0;

	console.log('**********');
	console.log('**고객채무**');
	console.log('**********');
...
}
function printOwing(invoice) {
	let outstanding = 0;

	printBanner(); => 배너 출력 로직을 함수로 추출
...
}

function printBanner() {
	console.log('**********');
	console.log('**고객채무**');
	console.log('**********');
};

예시: 지역변수의 값을 변경할 때*

function printOwing(invoice) {
  let outstanding = 0;

  // 미해결 채무(outstanding)를 계산한다
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }

  printBanner();
  recordDueDate(invoice);
  printDetails(invoice, outstanding);
}
function printOwing(invoice) {
  printBanner();

  const outstanding = calculateOutstanding(invoice); // 함수 추출 완료. 추출한 함수가 반환한 값을 원래 변수에 저장

  recordDueDate(invoice);
  printDetails(invoice, outstanding);
}

function calculateOutstanding(invoice) {
  return invoice.orders.map((o) => {
    result += o.amount;
  });
}

2. 함수 인라인 하기


6.1의 ‘함수 추출하기’의 반대 리팩터링으로, 함수 본문을 코드로 인라인하여 불필요한 함수들을 줄이는 리팩터링이다.

배경

절차

예시

function rating(aDriver) {
  return moreThanFiveLateDeliveries(aDriver) ? 2 : 1;
}

function moreThanFiveLateDeliveries(aDriver) {
  return aDriver.numberOfLateDeliveries > 5;
}
function rating(aDriver) {
  return aDriver.numberOfLateDeliveries > 5;
}

아주아주 조금 더 복잡한 예시

function reportLines(aCustomer) {
  const lines = [];
  gatherCustomerData(lines, aCustomer);
  return lines;
}

function gatherCustomerData(out, aCustomer) {
  out.push(["name", aCustomer.name]);
  out.push(["location", aCustomer.location]);
}
function reportLines(aCustomer) {
  const lines = [];

  lines.push(["name", aCustomer.name]);
  lines.push(["location", aCustomer.location]);

  return lines;
}

3. 변수 추출하기


지역변수를 활용하여 코드를 잘게 쪼개서 관리하면 복잡한 로직을 구성하는 단계마다 이름을 붙일 수 있어서 코드의 목적을 훨씬 명확하게 드러낼 수 있다.

배경

절차

예시*

function price(order) {
  // 가격(price) = 기본 가격 - 수량 할인 + 배송비
  return (
    order.quantity * order.itemPrice -
    Math.max(0, ordre.quantity - 500) * order.itemPrice * 0.05 +
    Math.min(order.quantity * order.itemPrice * 0.1, 100)
  );
}
function price(order) {
  const basePrice = order.quantity * order.itemPrice;
  const quantityDiscount =
    Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
  const shipping = Math.min(basePrice * 0.1, 100);

  // 가격(price) = 기본 가격 - 수량 할인 + 배송비
  return basePrice - quantityDiscount + shipping;
}

클래스 안에서의 예시

class Order {
  constructor(aRecord) {
    this._data = aRecord;
  }

  get itemPrice() {
    return this._data.itemPrice;
  }
  get price() {
    return this.basePrice - this.quantityDiscount + shipping;
  }
  get basePrice() {
    return this.quantity * this.itemPrice;
  }
  get quantityDiscount() {
    return Math.max(0, this.quantity - 500) * this.itemPrice * 0.05;
  }
  get shipping() {
    return Math.min(this.basePrice * 0.1, 100);
  }
}

4. 변수 인라인하기


6.3의 ‘변수 추출하기’의 반대 리팩터링으로, 별도의 변수로 두지 않고 인라인 하는 리팩터링

배경

절차

예시

function getBasePrice(anOrder) {
  let basePrice = anOrder.basePrice;

  return basePrice > 1000;
}
function getBasePrice(anOrder) {
  return anOrder.basePrice > 1000;
}

5. 함수 선언 바꾸기


함수 선언과 호출문들의 이름, 매개변수들을 추가/삭제 함으로써 코드를 개선하는 리팩터링

배경

간단한 절차

마이그레이션 절차(이름 변경 & 매개변수 추가를 모두 할 경우)

예시: 함수이름 바꾸기

cuircum(radius);

function circum(radius) {
  return 2 * Math.PI * redius;
}
circum(radius);

function circum(radius) {
  return circumference(radius);
}

function circumference(radius) {
  return 2 * Math.PI * redius;
}
circumference(radius);

function circumference(radius) {
  return 2 * Math.PI * redius;
}

예시: 매개변수 추가하기(클래스에서)

...book 클래스
this.addReservation(customer);

addReservation(customer){
	this._reservation.push(customer);
}
...book 클래스
this.addReservation(customer);

addReservation(customer){
	this.zz_addReservation(customer, false);
}

zz_addReservation(customer, isPriority){
	assert(isPriority === true || isPriority === false);

	this._reservation.push(customer);
}
...book 클래스
this.zz_addReservation(customer, false);

zz_addReservation(customer, isPriority){
	assert(isPriority === true || isPriority === false);

	this._reservation.push(customer);
}

예시: 매개변수를 속성으로 바꾸기*

function inNewEngland(aCustomer) {
  return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(aCustomer.address.state);
}

const newEnglanders = someCustomers.filter((c) => inNewEngland(c));
function inNewEngland(stateCode) {
  return ["MA", "CT", "ME", "VT", "NH", "RI"].includes(stateCode);
}

const newEnglanders = someCustomers.filter((c) =>
  inNewEngland(c.address.stateCode)
);

6. 변수 캡슐화 하기


데이터 변수에 대한 접근 및 변경을 캡슐화하여 그 데이터로의 접근을 독점하고, 데이터를 변경하고 사용하는 코드를 감시할 수 있도록 하는 리팩터링

배경

절차

예시*

let defaultOwner = {firstName: "마틴", lastName: "파울러"};

spaceship.owner = defaultOwner;

defaultOwner = {firstName: "레베카", lastName: "파울러"};

복제본을 반환하도록 하여 캡슐화

let _defaultOwnerData = {firstName: "마틴", lastName: "파울러"};

export function defaultOwner() {
  return Object.assign({}, _defaultOwnerData);
}
export function setDefaultOwner(arg) {
  _defaultOwnerData = arg;
}

레코드 캡슐화하기

let defaultOwnerData = {firstName: "마틴", lastName: "파울러"};

function defaultOwner() {
  return new Person(defaultOwnerData);
}
function setDefaultOwner(arg) {
  defaultOwnerData = arg;
}

class Person {
  constructor(data) {
    this._lastName = data.lastName;
    this._firstName = data.firstName;
  }

  get lastName() {
    return this._lastName;
  }
  get firstName() {
    return this._firstName;
  }
}

7. 변수 이름 바꾸기


코드의 목적과 역할, 흐름에 어울리는 더욱 적절한 변수의 이름을 바꾸는 리팩토링

배경

절차

예시

let tpHd = untitled;

const result += '<h1>${tpHd}</h1>'; // tpHd 변수를 읽는데만 사용

tpHd = obj['articleTitle']; // tpHd값을 수정

변수 캡슐화 하기

const result += '<h1>${tpHd}</h1>';

setTitle(obj['articleTitle']);

function title() {return tpHd;} // tpHd 변수의 getter
function setTitle(arg) {tpHd = arg;} // tpHd 변수의 setter

8. 매개변수 객체 만들기


몰려다니는 여러개의 매개변수들을 하나의 데이터 구조로 변경하여 데이터 사이의 관계를 명확히 하고 코드를 근본적으로 관리하도록 하는 리팩터링

배경

절차

예시*

const station = {
  name: "ZB1",
  readings: [
    {temp: 47, time: "2016-11-10 09:10"},
    {temp: 53, time: "2016-11-10 09:10"},
    {temp: 58, time: "2016-11-10 09:10"},
    {temp: 53, time: "2016-11-10 09:10"},
    {temp: 51, time: "2016-11-10 09:10"},
  ],
};

function readingsOutsideRange(station, min, max) {
  return station.readings.filter((r) => r.temp < min || r.temp > max);
}

const alerts = readingsOutsideRange(
  staion,
  operatingPlan.temperatureFloor,
  operatingPlan.temperatureCeiling
);
const station = {
  name: "ZB1",
  readings: [
    {temp: 47, time: "2016-11-10 09:10"},
    {temp: 53, time: "2016-11-10 09:10"},
    {temp: 58, time: "2016-11-10 09:10"},
    {temp: 53, time: "2016-11-10 09:10"},
    {temp: 51, time: "2016-11-10 09:10"},
  ],
};

function readingsOutsideRange(station, range) {
  return station.readings.filter(
    (r) => r.temp < range.min || r.temp > range.max
  );
}

const range = new NumberRange(
  operatingPlan.temperatureFloor,
  operatingPlan.temperatureCeiling
);
const alerts = readingsOutsideRange(staion, range);

class NumberRange {
  constructor(min, max) {
    this._data = {min: min, max: max};
  }

  get min() {
    return this._data.min;
  }
  get max() {
    return this._data.max;
  }
}