배경

데이터 구조는 프로그램을 이해하는 역할을 한다.

데이터 구조는 깔끔하게 관리해야 한다. 개발을 이어갈 수록 데이터를 이해하게 것이다. 깊어진 이해를 프로그램에 반드시 반영해야 한다.

예시

const organization = {name: "애크미 구스베리",country:"GB"};
상수에 name title 변경해야 한다. 객체는 코드베이스 곳곳에서 사용된다.


const organization = {name: "애크미 구스베리",country:"GB"};
//organization 레코드 클래스로 캡슐화
class Organization{
    constructor(data){
        this._name = data.name;
        this._country = data.country;
    }
    get name(){return this._name;}
    set name(arg){ this._name = arg;}
    get country(){return this._country;}
    set country(arg){ this._country = arg;}
}
레코드를 클래스 캡슐화한 덕에 변경을 작은 단계로 나눠할 있게 됐다.
작은 단계라 함은 잘못될 일도 작아진다는 말과 같다.


const organization = {name: "애크미 구스베리",country:"GB"};
class Organization{
    constructor(data){
        //생성자와 접근자를 구분하여 점진적으로 변경
        this._title = data.name;
        this._country = data.country;
    }
    get name(){return this._title;}
    set name(arg){ this._title = arg;}
    get country(){return this._country;}
    set country(arg){ this._country = arg;}
}


const organization = {name: "애크미 구스베리",country:"GB"};
class Organization{
    constructor(data){
        //생성자 조정
        this._title = (data.title !== undefined) ? data.title : data.name;
        this._country = data.country;
    }
    get name(){return this._title;}
    set name(arg){ this._title = arg;}
    get country(){return this._country;}
    set country(arg){ this._country = arg;}
}


//호출하는 곳 수정
const organization = new Organization({title: "애크미 구스베리",country:"GB"});


class Organization{
    constructor(data){
        this._title =  data.title;
        this._country = data.country;
    }
    get name(){return this._title;}
    set name(arg){ this._title = arg;}
    get country(){return this._country;}
    set country(arg){ this._country = arg;}
}


const organization = new Organization({title: "애크미 구스베리",country:"GB"});
class Organization{
    constructor(data){
        this._title =  data.title;
        this._country = data.country;
    }
    get title(){return this._title;}
    set title(arg){ this._title = arg;}
    get country(){return this._country;}
    set country(arg){ this._country = arg;}
}

지금 과정은 데이터 구조가 여러 곳에서 참조되고 있을 리팩터링 과정이다.

만약 참조되는 적거나 곳이라면 캡슐화도 필요없었을 것이다.

복잡할 수록 이처럼 작게 나눠서 정복해야한다.

 

자바스크립트가 아닌 데이터 구조를 불변으로 만들 있는 언어는 캡슐화 대신 데이터 구조를 값을 복제해 새로운 이름으로 선언 복제한 값을 사용하게 바꾸는 식으로 진행한다.

 

개요

반복문 루프 변수, 수집 변수 같이 값을 여러 대입할 밖에 없는 경우를 제외하고, 변수는 번만 값을 대입해야 한다.

 

대입이 이상 이뤄진다면 여러 가지 역할을 수행한다는 신호다.

 

예시



function distanceTravelled(scenario, time){
    let result;
    let acc = scenario.primaryForce / scenario.mass;//가속도=/질량
    let primaryTime = Math.min(time, scenario.delay);
    result = 0.5 * acc * primaryTime * primaryTime; // 전파된 거리
    let secondaryTime = time - scenario.delay;
    if(secondaryTime > 0){
        let primaryVelocity = acc * scenario.delay;
        acc = (scenario.primaryForce + scenario.secondaryForce) /scenario.mass;
        result += primaryVelocity * secondaryTime + 0.5 * acc * secondaryTime *secondaryTime;
    }
    return result;
}
acc 변수가 가지 역할을 하고 있다.
하나는 번째 힘이 유발한 초기 가속도 저장
다른 하나는 번째 힘까지 반영된 가속도 저장


function distanceTravelled(scenario, time){
    let result;
    //불변으로 정의
    const primaryAcceleration = scenario.primaryForce / scenario.mass;
    let primaryTime = Math.min(time, scenario.delay);
    result = 0.5 * primaryAcceleration * primaryTime * primaryTime;
    let secondaryTime = time - scenario.delay;
    if(secondaryTime > 0){
        let primaryVelocity = primaryAcceleration * scenario.delay;
        //별도 변수
        let acc = (scenario.primaryForce + scenario.secondaryForce) /scenario.mass;
        result += primaryVelocity * secondaryTime + 0.5 * acc * secondaryTime *secondaryTime;
    }
    return result;
}


function distanceTravelled(scenario, time){
    let result;
    const primaryAcceleration = scenario.primaryForce / scenario.mass;
    let primaryTime = Math.min(time, scenario.delay);
    result = 0.5 * primaryAcceleration * primaryTime * primaryTime;
    let secondaryTime = time - scenario.delay;
    if(secondaryTime > 0){
        let primaryVelocity = primaryAcceleration * scenario.delay;
        //불변으로 정의
        const secondaryAcceleration = (scenario.primaryForce + scenario.secondaryForce) /scenario.mass;
        result += primaryVelocity * secondaryTime + 0.5 * secondaryAcceleration * secondaryTime *secondaryTime;
    }
    return result;
}

 

예시: 입력 매개변수의 값을 수정할



function discount(inputValue, quantity){
    if(inputValue > 50) inputValue = inputValue - 2;
    if(quantity > 100) inputValue = inputValue - 1;
    return inputValue;
}


function discount(originalInputValue, quantity){
    //call by value 로 가정, 값을 복사해서 쓴다.
    let inputValue = originalInputValue;
    if(inputValue > 50) inputValue = inputValue - 2;
    if(quantity > 100) inputValue = inputValue - 1;
    return inputValue;
}


function discount(inputValue, quantity){
    //적절한 이름으로 변경
    let result = inputValue;
    if(inputValue > 50) result = result - 2;
    if(quantity > 100) result = result - 1;
    return result;
}

 

개요

소프트웨어 가격은 코드량에 비례하지 않는다.

사용하지 않는 코드가 있다고 시스템이 느려지는 것도 아니고, 런타임 메모리도 많이 잡아먹지 않는다.

최신 컴파일러들은 이런 코드를 알아서 제거해준다.

 

하지만 사용되지 않는 코드는 소프트웨어 동작을 이해하는 개발자에게 걸림돌이 있다.

이런 코드들이 스스로 나는 필요없지만 혹시해서 남겨놨어요 라고 알려주지 않기 때문이다.

이런 코드는 과감히 지워라. 현재 프로젝트들은 모두 버전 관리 시스템에 의해 관리되므로 정말 필요한 날이 올때를 위해 제거 커밋에 표기만 해두면 된다. 사실 심지어 커밋 메시지에 제거한 것을 따로 남겨놓지 않아서 후회한 적도 거의 없을 것이다.

 

예전 버전 관리 시스템이 없었을 적에 코드를 주석해서 사용했다.

개요

반복문 하나에 가지 일을 수행하는 모습을 자주보게 된다.

이렇게 되면 변경이 가해질 가지를 모두 고려해야 한다.

 

반면에 각각으로 분리되어 있다면, 수정할 곳만 확인하면 된다.

 

예시



function example(){
    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 `최연소:${youngest}, 총 급여: ${totalSalary}`;
}
하나의 반복문에 서로 다른 가지를 계산하고 있다.


function example(){
    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;
    }
    //먼저 단순한 복사
    for(const p of people){
        if(p.age<youngest) youngest = p.age;
        totalSalary += p.salary;
    }
    return `최연소:${youngest}, 총 급여: ${totalSalary}`;
}


function example(){
    let youngest = people[0] ? people[0].age : Infinity;
    let totalSalary = 0;
    //부수효과가 있는 코드는 한쪽만 남기고 제거
    for(const p of people) totalSalary += p.salary;
    for(const p of people) if(p.age<youngest) youngest = p.age;
   
    return `최연소:${youngest}, 총 급여: ${totalSalary}`;
}

 

가다듬기

반복문 쪼개기는 여기서 끝이다. 하지만 반복문 쪼개기의 진가는 리팩터링 자체가 아닌 다음 단계로 가는 디딤돌 역할에 있다.

function example(){
    let totalSalary = 0;
    for(const p of people) totalSalary += p.salary;
    //더 나아가 각 반복문을 각 함수로 만들기, 먼저 근처로 문장 슬라이드
    let youngest = people[0] ? people[0].age : Infinity;
    for(const p of people) if(p.age<youngest) youngest = p.age;
    return `최연소:${youngest}, 총 급여: ${totalSalary}`;
}


function example(){
    return `최연소:${youngestAge()}, 총 급여: ${totalSalary()}`;
    //각각 함수로 추출
    function youngestAge() {
        let youngest = people[0] ? people[0].age : Infinity;
        for (const p of people)
            if (p.age < youngest)
                youngest = p.age;
        return youngest;
    }
    function totalSalary() {
        let totalSalary = 0;
        for (const p of people)
            totalSalary += p.salary;
        return totalSalary;
    }
}


function example(){
    return `최연소:${youngestAge()}, 총 급여: ${totalSalary()}`;
    //알고리즘 교체하기
    function youngestAge() {
        return people.map(p=>p.age).reduce((a1,a2)=>a1<a2?a1:a2, Infinity);
    }
    //파이프라인으로 바꾸기
    function totalSalary() {
        return people.reduce((total, p)=> total+ p.salary,0);;
    }
}

 

개요

관련된 코드들이 가까이 뭉쳐 있으면 이해하기 쉽다.

관련 코드끼리 모으는 작업은 다른 리팩터링의 준비 단계로 자주 행해진다.

취향 차이지만 함수 모든 변수를 머리에 모으는 사람도 있는데 사용하는 근처에 선언하는 것도 나쁘진 않다.

 

예시

코드 슬라이드 가지를 고려

무엇을 슬라이드할지, 슬라이드 가능한지

무엇을 슬라이드할지는 맥락과 관련 있다.

 

const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
const baseCharge = pricingPlan.base;
let charge;
const chargePerUnit = pricingPlan.unit;
const units = order.units;
let discount;
charge = baseCharge + units * chargePerUnit;
let discountableUnits = Math.max(units - princingPlan * discountThreshold, 0);
discount = discountableUnits * pricingPlan.discountFactor;
if(order.isRepeat) discount += 20;
charge = charge - discount;
chargeOrder(charge);


//사이드 이펙트가 없는 코드끼리는 마음대로 재배치 할 수 있다.
//부수효과가 없는 프로그래밍을 하는 이유
const pricingPlan = retrievePricingPlan();
const baseCharge = pricingPlan.base;
let charge;
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder(); // 바로 근처로 옮김
const units = order.units;
charge = baseCharge + units * chargePerUnit;
let discountableUnits = Math.max(units - princingPlan * discountThreshold, 0);
let discount; //바로 근처로 옮김
discount = discountableUnits * pricingPlan.discountFactor;
if(order.isRepeat) discount += 20;
charge = charge - discount;
chargeOrder(charge);
retrieveOrder() 내부를 확인해야 부수효과가 있는지 확인할 있는데 여기선 자기가 작성한 함수고 명령-질의 분리 원칙을 지키고 있다고 가정하고 있다.
만약 남이 작성한 코드라면 살펴봐야한다.


const pricingPlan = retrievePricingPlan();
const baseCharge = pricingPlan.base;
let charge;
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder();
const units = order.units;
charge = baseCharge + units * chargePerUnit;
let discountableUnits = Math.max(units - princingPlan * discountThreshold, 0);
let discount;
discount = discountableUnits * pricingPlan.discountFactor;
if(order.isRepeat) discount += 20; //이 줄은 맨끝으로 슬라이드하고 싶어도 참조하는 라인이 있어 막힌다.
charge = charge - discount;
chargeOrder(charge);

 

 

예시:조건문이 있을 때의 슬라이드

 



function example(){
    let result;
    if(availableResources.length === 0){
        result = createResource();
        allocatedResources.push(result);
    }else{
        result = availableResources.pop();
        allocatedResources.push(result);
    }
    return result;
}


function example(){
    let result;
    if(availableResources.length === 0){
        result = createResource();
    }else{
        result = availableResources.pop();
    }
    allocatedResources.push(result);
    return result;
}

 

개요

함수는 여러 동작을 하나로 묶어준다.

함수 이름은 코드 동작 방식보다 목적을 말해주기에 함수를 활용하면 코드 이해가 쉬워진다.

 

함수는 중복 제거에도 효과적이다.

 

라이브러리가 제공하는 함수로 대체할 있다면 좋다. 함수 본문을 작성할 필요조차 없기 때문이다.

따라서 라이브러리가 제공하는 API 파악할 수록 이팩터링 활용 빈도가 높아진다.

 

 



let appliesToMass = false;
for(const s of states){
    if(s === "MA") appliesToMass = true;
}


let appliesToMass = states.includes("MA");

 

개요

함수는 추상화의 기본 빌딩 블록

추상화의 경계를 항상 올바르게 긋기는 쉽지 않다.

코드 베이스의 기능 범위가 달라지면 추상화의 경계도 움직인다.

초기에 응집도 높고 가지 일만 수행하던 함수가 이상 다른 일을 수행하게 바뀔 있다는

 

예시

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>제목: ${photo.title}</p>\n`);
    outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
    outStream.write(`<p>위치: ${photo.location}</p>\n`);
}
위치 정보만 다르게 해야 한다.


function emitPhotoData(outStream, photo){
    zztmp(outStream, photo);
    outStream.write(`<p>위치: ${photo.location}</p>\n`);
}
function zztmp(outStream, photo) {//이동하지 않을 코드
    outStream.write(`<p>제목: ${photo.title}</p>\n`);
    outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
}


function renderPerson(outStream, person){
    outStream.write(`<p>${person.name}</p>\n`);
    renderPhoto(outStream, person.photo);
    zztmp(outStream, person.photo);
    outStream.write(`<p>위치: ${photo.location}</p>\n`);
}


function listRecentPhotos(outStream, photos){
    photos
        .filter(p=>p.date > recentDateCutoff())
        .forEach(p=>{
            outStream.write("<div>\n");
            zztmp(outStream, p)
            outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
            outStream.write("</div>\n");
        });
}
function renderPerson(outStream, person){
    outStream.write(`<p>${person.name}</p>\n`);
    renderPhoto(outStream, person.photo);
    emitPhotoData(outStream, person.photo);
    outStream.write(`<p>위치: ${photo.location}</p>\n`);
}
//기존 emitPhotoData제거 후 새 함수 이름 emitPhotoData로 변경
function listRecentPhotos(outStream, photos){
    photos
        .filter(p=>p.date > recentDateCutoff())
        .forEach(p=>{
            outStream.write("<div>\n");
            emitPhotoData(outStream, p)
            outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
            outStream.write("</div>\n");
        });
}
function emitPhotoData(outStream, photo) {
    outStream.write(`<p>제목: ${photo.title}</p>\n`);
    outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
}

 

 

개요

중복 제거는 건강한 코드를 위해 필요하다.

중복되는 코드를 발견하면 반복 부분을 함수로 합치는 방법을 강구한다.

합쳐놓으면 변경이 발생 한곳만 수정하면 된다.

 

코드를 여러 변형으로 나눠야 하는 순간이 오면 문장을 호출한 곳으로 옮기기를 적용하면 된다.

 

예시

사진 관련 데이트럴 HTML 내보내는 코드



function renderPerson(outStream, person){
    const result = [];
    result.push(`<p>${person.name}</p>`);
    result.push(renderPhoto(person.photo));
    result.push(`<p>제목: ${person.photo.title}</p>`);
    result.push(emitPhotoData(person.photo));
    return result.join("\n");
}
function photoDiv(p){
    return [
        "<div>",
        `<p>제목: ${p.title}</p>`,
        emitPhotoData(p),
        "</div>",
    ].join("\n");
}
function emitPhotoData(aPhoto){
    const result = [];
    result.push(`<p>위치: ${aPhoto.location}</p>`)
    result.push(`<p>날짜: ${aPhoto.date.toDateString()}</p>`)
    return result.join("\n");
}


function photoDiv(p){
    return [
        "<div>",
        zznew(p),
        "</div>",
    ].join("\n");
}
//중복 제거를 위해 함수 추출
function zznew(p){
    return [
        `<p>제목: ${p.title}</p>`,
        emitPhotoData(p) ,
    ].join("\n");
}


function renderPerson(outStream, person){
    const result = [];
    result.push(`<p>${person.name}</p>`);
    result.push(renderPhoto(person.photo));
    //추가 중복 제거
    result.push(zznew(person.photo));
    return result.join("\n");
}


function renderPerson(outStream, person){
    const result = [];
    result.push(`<p>${person.name}</p>`);
    result.push(renderPhoto(person.photo));
    //추가 중복 제거
    result.push(zznew(person.photo));
    return result.join("\n");
}
function photoDiv(p){
    return [
        "<div>",
        zznew(p),
        "</div>",
    ].join("\n");
}
//emitPhotoData()인라인하기
function zznew(p){
    return [
        `<p>제목: ${p.title}</p>`,
        `<p>위치: ${p.location}</p>` ,
        `<p>날짜: ${p.date.toDateString()}</p>`,
    ].join("\n");
}


function renderPerson(outStream, 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(p){
    return [
        "<div>",
        emitPhotoData(p),
        "</div>",
    ].join("\n");
}
//함수 이름 바꾸기로 마무리
function emitPhotoData(p){
    return [
        `<p>제목: ${p.title}</p>`,
        `<p>위치: ${p.location}</p>` ,
        `<p>날짜: ${p.date.toDateString()}</p>`,
    ].join("\n");
}
function renderPerson(outStream, 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(aPhoto){
    return [
        "<div>",
        emitPhotoData(aPhoto),
        "</div>",
    ].join("\n");
}
function emitPhotoData(aPhoto){
    return [
        `<p>제목: ${aPhoto.title}</p>`,
        `<p>위치: ${aPhoto.location}</p>` ,
        `<p>날짜: ${aPhoto.date.toDateString()}</p>`,
    ].join("\n");
}

 

 

 

 

 

 

 

 

개요

적합한 데이터 구조를 활용하면 동작 코드는 자연스럽게 단순하고 직관적으로 짜여진다.

데이터 구조를 잘못 선택하면 데이터를 다루기 위한 코드로 범벅이 된다. 결국 이해하기 힘든 코드가 된다.

 

현재 데이터 구조가 적절치 않으면 바로 수정해야 한다.

 

함수에 어떤 레코드를 넘길 때마다 다른 레코드의 필드도 함께 넘기고 있다면, 필드 옮기기가 필요하다.

레코드의 변경을 때마다 다른 레코드의 필드까지 변경해야 한다면, 필드 옮기기가 필요하다.

여러 레코드에 정의된 똑같은 필드가 있을 레코드의 필드가 갱신될 다른 레코드 필드도 갱신해야 한다면 필드 옮기기가 필요하다.

 

필드 옮기기는 대체로 리펙터링을 위한 사전 작업인 경우가 많다.

 

레코드 뿐만 아니라 클래스도 똑같다. 데이터에 함수가 해진 레코드가 클래스이기 때문이다.

 

클래스는 접근자 메서드로 캡슐화가 되어 있어 비교적 리팩터링이 쉽다.

캡슐화되자 않은 날레코드는 훨씬 까다롭다.



//날레코드 예시,게터세터 없이 데이터를 직접 접근
public class RawRecord{
    public String data;
    public Integer data2;
}

예시



class Customer{
    constructor(name, discountRate){
        this._name = name;
        this._discountRate = discountRate;
        this._contract = new CustomerContract(dateToday());
    }
    get discountRate(){return this._discountRate;}
    becomePreferred(){
        this._discountRate += 0.03;
        //코드들 ...
    }
    applyDiscount(amount){
        return amount.subtract(amount.multiply(this._discountRate));
    }
}
class CustomerContract{
    constructor(startDate){
        this._startDate = startDate;
    }
}
Customer 클래스에 discountRate 변수를 CustomerContract 옮기고 싶다.
세터는 만들고 싶이 않아 메서드를 사용
class Customer{
    constructor(name, discountRate){
        this._name = name;
        this._setDiscountRate(discountRate);
        this._contract = new CustomerContract(dateToday());
    }
    get discountRate(){return this._discountRate;}
    //먼저 변수 캡슐화부터
    _setDiscountRate(aNumber){this._discountRate = aNumber;}
    becomePreferred(){
        this._setDiscountRate(this._discountRate +0.03);
        //코드들 ...
    }
    applyDiscount(amount){
        return amount.subtract(amount.multiply(this._discountRate));
    }
}


}
//필드와 접근자 추가
class CustomerContract{
    constructor(startDate,discountRate){
        this._startDate = startDate;
        this._discountRate = discountRate;
    }
    get discountRate(){return this._discountRate;}
    set discountRate(arg){this._discountRate = arg;}
}


class Customer{
    //계약 인스턴스를 사용하도록 수정
    constructor(name, discountRate){
        this._name = name;
        this._contract = new CustomerContract(dateToday());
        this._setDiscountRate(discountRate);
    }
    get discountRate(){return this._contract.discountRate;}
    _setDiscountRate(aNumber){this._contract.discountRate = aNumber;}
    becomePreferred(){
        this._setDiscountRate(this._discountRate +0.03);
        //코드들 ...
    }
    applyDiscount(amount){
        return amount.subtract(amount.multiply(this._discountRate));
    }
}
class CustomerContract{
    constructor(startDate,discountRate){
        this._startDate = startDate;
        this._discountRate = discountRate;
    }
    get discountRate(){return this._discountRate;}
    set discountRate(arg){this._discountRate = arg;}
}

리팩터링은 객체를 활용할 수월하다.

캡슐화 덕분에 데이터 접근이 메서드로도 메끄럽다.

함수로는 까다롭다.

 

예시: 공유 객체로 이동하기

이자율을 계좌 별로 설정하는 예시



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;
    }
}
계좌 종류에 따라 정해지도록 수정


//interestRate 이자율 필드 생성 및 접근자 추가
class AccountType{
    constructor(nameString, interestRate){
        this._name = nameString;
        this._interestRate = interestRate;
    }
    get interestRate(){return this._interestRate;}
}
Acount AcountType 이자율을 가져오도록 수정하면 문제가 생길 있다.
전엔 Acount별로 자신만의 이자율을 가졌다. 지금은 같은 종류 계좌끼리 이자율을 공유하려 한다.
정말로 같은 종류 계좌는 이자율이 같은지가 관건이다. 반약 다르다면 리팩터링의 핵심인 "수정 전과 수정 겉보기 동작이 같아야 한다" 깨져 이상 리팩터링이 아니게 된다.
이를 위해 먼저 DB에서 같은 종류 계좌가 같은 이자율 같은지 검증하는 것이 필요하다. 그리고 어셔션을 추가하면 도움된다.


class Account{
    constructor(number, type, interestRate){
        this._number = number;
        this._type = type;
        //어셔션 추가한 상태로 당분간 운영하기
        assert(interestRate === this._type.interestRate);
        this._interestRate = interestRate;
    }
    get interestRate(){return this._interestRate;}
}


class Account{
    constructor(number, type, interestRate){
        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;}
}

 

 

 

 

 

 

개요

좋은 소프트웨어 설계 핵심은 모듈화 수준이다.

모듈성이란 프로그램 어딘가를 수정 해당 기능과 관련된 극히 작은 부분만 이해 해도되는 능력이다.

 

모듈성이 높으려면 연관된 요소를 하나의 요소로 묶고, 요소 사이 연결 관계를 쉽게 이해할 있어야 한다.

 

객체 지향 프로그래밍의 핵심 모듈화 컨텍스트는 클래스다.

 

A 함수가 자신이 속한 모듈보다 다른 모듈을 참조한다면 옮겨야한다.

 

예시: 중첩 함수를 최상위로 옮기기

 



/* GPS 추적 기록의 총 거리를 계산하는 함수 */
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) {/* 라디안값으로 변환 */}
    function calculateTime() {/* 총 시간 계산 */}
}
중첩 함수 calculateDistance() 최상위로 옮겨서 다른 정보와 독립적으로 계산하고 싶다.


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) {/* 라디안값으로 변환 */}
    function calculateTime() {/* 총 시간 계산 */}
}
function top_calculateDistance() { //최상위로 복사,임시이름
    let result = 0;
    for(let i = 1; i < points.length ; i++){
        result += distance(points[i-1], points[i]);
    }
    return result;
}


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;
        //distance, radians 는 현재 문맥에서 calculateDistance에서만 사용중
        //따라서 calculateDistance()함수 안으로 옮기는게 적절하다.
        function distance(p1, p2) {
            const EARTH_RADIUS = 3959; // 단위 마일
            const dLat = radians(p2.lat) - radians(p1.lat);
            const dLon = radians(p2.lon) - radians(p1.lon);
            const a = Math.pow(Math.sin(dLat / 2), 2)
                + Math.cos(radians(p2.lat))
                * Math.cos(radians(p1.lat))
                * Math.pow(Math.sin(dLon /2), 2);
            const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
            return EARTH_RADIUS * c;
        }
        function radians(degrees) {
            return degrees * Math.PI / 180;
        }
    }
    function calculateTime() {/* 총 시간 계산 */}
}
function top_calculateDistance(points) {
    let result = 0;
    for(let i = 1; i < points.length ; i++){
        result += distance(points[i-1], points[i]);
    }
    return result;
}


function top_calculateDistance(points) {
    let result = 0;
    for(let i = 1; i < points.length ; i++){
        result += distance(points[i-1], points[i]);
    }
    return result;
    //top_calculateDistance()함수도 똑같이 옮겨준다.
    function distance(p1, p2) {
        const EARTH_RADIUS = 3959; // 단위 마일
        const dLat = radians(p2.lat) - radians(p1.lat);
        const dLon = radians(p2.lon) - radians(p1.lon);
        const a = Math.pow(Math.sin(dLat / 2), 2)
            + Math.cos(radians(p2.lat))
            * Math.cos(radians(p1.lat))
            * Math.pow(Math.sin(dLon /2), 2);
        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
        return EARTH_RADIUS * c;
    }
    function radians(degrees) {
        return degrees * Math.PI / 180;
    }
}


function trackSummary(points){
    const totalTime = calculateTime();
    const totalDistance = calculateDistance();
    const pace = totalTime / 60 / totalDistance;
    return {
        time : totalTime,
        distance : totalDistance,
        pace : pace,
    };
    //복사 함수를 호출하도록 수정한다.
    function calculateDistance() { //총 거리 계산
        return top_calculateDistance(points);
    }
    function calculateTime() {/* 총 시간 계산 */}
}
시점에서 반드시 테스트를 정상 동작을 확인한다.
정상 동작한다면 기존 함수 제거 복사 함수를 직접 호출하도록 수정한다.
function trackSummary(points){
    const totalTime = calculateTime();
    const totalDistance = top_calculateDistance();
    const pace = totalTime / 60 / totalDistance;
    return {
        time : totalTime,
        distance : totalDistance,
        pace : pace,
    };
    function calculateTime() {/* 총 시간 계산 */}
}


function trackSummary(points){
    const totalTime = calculateTime();
    const pace = totalTime / 60 / totalDistance();
    return {//적절한 이름으로 변경 후 인라인
        time : totalTime,
        distance : totalDistance(),
        pace : pace,
    };
    function calculateTime() {/* 총 시간 계산 */}
}


function trackSummary(points){
    const totalTime = calculateTime();
    const pace = totalTime / 60 / totalDistance();
    return {
        time : totalTime,
        distance : totalDistance(),
        pace : pace,
    };
    function calculateTime() {/* 총 시간 계산 */}
}
function totalDistance(points) {
    let result = 0;
    for(let i = 1; i < points.length ; i++){
        result += distance(points[i-1], points[i]);
    }
    return result;
}
//distance, radians totalDistance안에 어떤 것도 의존하지 않으니
//최상단으로 옮겼다.
function distance(p1, p2) {
    const EARTH_RADIUS = 3959; // 단위 마일
    const dLat = radians(p2.lat) - radians(p1.lat);
    const dLon = radians(p2.lon) - radians(p1.lon);
    const a = Math.pow(Math.sin(dLat / 2), 2)
        + Math.cos(radians(p2.lat))
        * Math.cos(radians(p1.lat))
        * Math.pow(Math.sin(dLon /2), 2);
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
    return EARTH_RADIUS * c;
}
function radians(degrees) {
    return degrees * Math.PI / 180;
}

 

예시: 다른 클래스로 옮기기



class Account{
    get bankCharge(){ //은행이자
        let result = 4.5;
        if(this._daysOverdrawn > 0) result += this.overdraftCharge;
        return result
    }
    get overdraftCharge(){
        if(this.type.isPremium){//초과 인출 이자 계산
            const baseCharge = 10;
            if(this.daysOverdrawn <= 7){
                return baseCharge;
            }else{
                return baseCharge + (this.daysOverdrawn - 7) * 0.85;
            }
        }else{
            return this.daysOverdrawn * 1.75;
        }
    }
}
계좌 종류에 따라 이자 책정 알고리즘이 달라지도록 수정


class AccountType{
    //계좌 타입으로 복사 후 적절하게 수정
    //daysOverdrawn는 계좌별로 달라지므로 원래 클래스에 남겨두었다.
    overdraftCharge(daysOverdrawn){
        if(this.isPremium){
            const baseCharge = 10;
            if(this.daysOverdrawn <= 7){
                return baseCharge;
            }else{
                return baseCharge + (daysOverdrawn - 7) * 0.85;
            }
        }else{
            return daysOverdrawn * 1.75;
        }
    }
}


class Account{
    get bankCharge(){
        let result = 4.5;
        if(this._daysOverdrawn > 0) result += this.overdraftCharge;
        return result
    }
    get overdraftCharge(){//위임 메서드
        return this.type.overdraftCharge(this.daysOverdrawn);
    }
}
class AccountType{
    overdraftCharge(daysOverdrawn){
        if(this.isPremium){
            const baseCharge = 10;
            if(this.daysOverdrawn <= 7){
                return baseCharge;
            }else{
                return baseCharge + (daysOverdrawn - 7) * 0.85;
            }
        }else{
            return daysOverdrawn * 1.75;
        }
    }
}


class Account{
    get bankCharge(){
        let result = 4.5;
        if(this._daysOverdrawn > 0)
            //인라인할지 고민한 결과 인라인하기로 결정
            result += this.type.overdraftCharge(this.daysOverdrawn);
        return result
    }
}
class AccountType{
    overdraftCharge(daysOverdrawn){
        if(this.isPremium){
            const baseCharge = 10;
            if(this.daysOverdrawn <= 7){
                return baseCharge;
            }else{
                return baseCharge + (daysOverdrawn - 7) * 0.85;
            }
        }else{
            return daysOverdrawn * 1.75;
        }
    }
}

 

 

 

+ Recent posts