본문 바로가기

SW Engineering

Refactoring - Encapsulation

Encapsulate Record

레코드 데이터를 클래스로 캡슐화하고 접근자를 만든다.

 

Before

organization = {name: "Acme Gooseberries", country: "GB"};

 

After

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;}
}

 

Motivation

Record structure 는 관련된 데이터들을 함께 모으는 직관적인 방법이다. 이를 통해 데이터들을 의미 있는 단위로 전달할 수 있도록 한다. 그러나 단순한 record sturcture는 단점이 있다.

단점 중 하나는 레코드에 저장된 값과 계산된 값을 명백히 분리해야 한다는 것이다. 범위를 가진 정수를 {start :1, end:5}와 같은 record structure로 만들 때 , 저장된 값은 1과 5이지만 원하는 범위를 계산하면 1,2,3,4,5의 값을 가진다.

하지만 나는 뭘 저장하더라도 시작과 끝이 무엇인지 알기를 원한다. 이것이 mutable data에 대해서 record보다 object를 선호 하는 이유다. 객체를 이용하여 저장된 것들이 무엇인지 감추고 값에 대한 메서드를 제공한다. 따라서 사용자는 객체 안에 어떤 것이 저장된 값이고 어떤 것이 계산된 것인지 알 필요가 없다.

레코드의 필드는 명시적이지 못하다는 단점도 있다. start/end 범위 정보를 알 수 있는 방법은 레코드 생성되거나 사용된 곳을 찾아봐야 한다. 이것은 작은 범위 내에서는 문제가 아니지만 사용범위가 커지면 문제가 생긴다.

 

 

Encapsulate Collection

클래스에서 콜렉션에 대한 수정 메서드를 제공하고 외부에서 콜렉션을 변경하지 못하도록 한다.

 

Before

class Person {              
  get courses() {return this._courses;}
  set courses(aList) {this._courses = aList;}

 

After

class Person {
  get courses() {return this._courses.slice();}
  addCourse(aCourse)    { ... }
  removeCourse(aCourse) { ... }

 

Motivation

Collection 변수에 대한 접근을 캡슐화 할 때, getter 메서드가 콜렉션 자체를 반환하면 콜렉션을 가진 클래스의 개입 없이 콜렉션의 멤버쉽이 변경 될 수 있다. 이를 막기 위해 클래스를 통해 콜렉션을 수정하도록 add와 remove 같은 메서드를 제공하도록 한다. 이를 통해 콜렉션에 대한 변경은 소유 클래스를 통과하므로 변경사항이 있을 때 클래스에서 수정 기회를 제공합니다.

클래스가 콜렉션을 그대로 반환하면 클래스 밖에서 콜렉션 값이 변경될 수 있고, 이는 버그를 만들면 변경을 추적하기 힘들게 한다. 이를 막기 위한 한가지 방법은 collection이 반환하지 않도록 하는 것이다. aCustomer.orders.size 대신 aCustomer.numberOfOrderers와 같이 필요한 정보에 대한 특정 메서드를 제공하는 것이다. 하지만 이러한 방법은 언어가 제공하는 풍부한 콜렉션 기능(e.g map과 filter같은 pipline)을 못쓰게 한다.

다른 방법은 콜렉션을 읽기만 가능하도록 하는 것이다. 예를 들어 자바는 콜렉션에 대한 read-only 프록시를 쉽게 반환할 수 있다.

콜렉션의 getter 메서드 구현할 때 가장 흔한 방법은 콜렉션의 복사본을 반환하는 것이다. 이를 통해 반환된 콜렉션의 변경이 원래 콜렉션에 영향을 끼치지 않도록 한다. 복사는 성능에 대한 이슈가 있지만 대부분의 경우 큰 문제 아니다.

코드베이스에서 중요한 것은 프로그램 내에서 collection에 대한 처리를 일관성 있게 유지해야 한다.

 

 

Replace Primitive with Object

원시타입을 가진 데이터의 특정한 동작이 필요할 때, 클래스로 변경하고 동작에 대한 메서드를 추가한다.

 

Before

orders.filter(o => "high" === o.priority
                || "rush" === o.priority);

 

After

orders.filter(o => o.priority.higherThan(new Priority("normal")))

 

Motivation

개발할 때 단순한 항목의 경우 string이나 number와 같은 원시 타입으로 표현할 수 있다. 초기에 전화번호를 string으로 표현하였으나, 나중에 포맷팅이나 지역번호 추출과 같은 특별한 동작이 필요해질 수 있다. 이러한 종류의 로직은 코드의 중복을 발생시켜 필요할때마다 노력을 증가시킵니다.

단순한 프린팅 이상의 무언가가 필요하다고 느끼면 데이터를 위한 새로운 클래스를 만드는 것이 좋다. 처음에는 원시 타입을 래핑한 것보다 조금 더 무언가를 하는 클래스지만 나중에 기능을 추가하는 노력을 덜여준다.

 

Replace Temp with Query

 

Before

const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000)
  return basePrice * 0.95;
else
  return basePrice * 0.98

 

After

get basePrice() {this._quantity * this._itemPrice;}

...

if (this.basePrice > 1000)
  return this.basePrice * 0.95;
else
  return this.basePrice * 0.98;

 

Motivation

임시 변수는 어떤 코드에 의해 계산된 값을 가지고 나중에 사용 될 수 있다. 임시 변수는 의미를 설명하면서 계산 코드의 반복 없이 참조 될 수 있다. 임시변수는 유용하지만 때때로 함수를 사용하는 것이 더 나을 수 있다.

큰 함수를 쪼갤 때, 변수들을 함수 자신의 것으로 바꾸면 추출된 함수쪽으로 매개변수를 전달하지 않아도 되기 때문에 추출이 쉬워진다. 이러한 로직을 함수에 넣으면 원래 함수와 추출된 로직 사이에 경계가 강해져서 어색한 종속성과 사이드 이펙트를 발견하고 피할 수 있다.

이 리팩토링은 클래스 내부인 경우에 좋다. 왜냐하면 추출하는 메서드에 대해 클래스가 공유된 컨텍스트를 제공하기 때문이다. 클래스 외부의 경우 많은 파라미터를 처리해줘야 하기 때문에 함수를 사용하는 장점이 반감된다.

한번만 계산되고 뒤에서 참조만 되는 임수변수의 경우 Replace Temp with Query에 적합할 수 있다.

 

Extract Class

클래스의 책임이 많은 경우에 책임을 분리하고 자식 클래스를 만들어 양도한다.

 

Before

class Person {
  get officeAreaCode() {return this._officeAreaCode;}
  get officeNumber()   {return this._officeNumber;}

 

After

class Person {
  get officeAreaCode() {return this._telephoneNumber.areaCode;}
  get officeNumber()   {return this._telephoneNumber.number;}
}
class TelephoneNumber {
  get areaCode() {return this._areaCode;}
  get number()   {return this._number;}
}

 

 

Motivation

클래스는 선명하게 추상화되어야하고 몇가지 분명한 책임만을 처리해야 한다. 실무에서 클래스는 계속해서 커진다. 데이터와 오퍼레이션들이 조금씩 클래스에 추가되지만, 별도의 클래스로 분리할 필요는 없다고 느낄 수 있다. 이렇게 시간이 지나면 클래스는 커지고 복잡해질 것이다.

클래스가 많은 메서드와 데이터를 가질 때 분리하는 것을 고려해야 한다. 분리할 때 좋은 징후는 데이터의 subset과 메서드의 subset이 함께 움직이는 것처럼 보일 때다. 다른 좋은 징후는 데이터의 subset이 함께 변하거나 서로에게 특히 의존하는 경우다.

 

Inline Class

하는 일이 별로 없으 별도의 클래스로 유지할 필요가 없을 때, 모든 변수와 메서드를 다른 클래스로 옮긴다.

 

After

class Person {
  get officeAreaCode() {return this._telephoneNumber.areaCode;}
  get officeNumber()   {return this._telephoneNumber.number;}
}
class TelephoneNumber {
  get areaCode() {return this._areaCode;}
  get number()   {return this._number;}
}

 

Before

class Person {
  get officeAreaCode() {return this._officeAreaCode;}
  get officeNumber()   {return this._officeNumber;

 

Motivation

Extract Class의 반대이다. 클래스의 중요도가 떨어지거나 더이상 사용되지 않을 때 Inline Class를 적용한다. 종종 리팩토링의 결과로 클래스의 책임이 빠져나가 남은게 얼마 없는 경우에 다른 클래스에 흡수 될 수 있다. 이 때 책임이 적은 클래스에 합치는 것을 고려해보면 좋다.

 

Hide Delegate

위임 객체를 직접 호출하면 메서드를 만들어 위임 객체를 감춘다.

 

After

manager = aPerson.department.manager;

 

Before

manager = aPerson.manager;

class Person {
  get manager() {return this.department.manager;}

 

Motivation

캡슐화는 모듈들이 시스템의 다른 부분에 덜 알아도 되도록 한다. 따라서 변경이 발생할 때 변경에 대해 알려야할 정보가 줄어들어 변경이 쉬워진다.

클라이언트에서 서버 객체의 필드로 존재하는 위임 객체의 메서드를 호출 할 때, 클라이언트는 이 위임 객체를 알야야 한다. 만약 위임 객체의 인터페이스가 변경되면, 이 변경사항은 서버의 클라이언트들에도 전파된다. 서버에 위임 객체를 숨기는 위임 메서드를 둠으로써 변경사항이 서버까지만 전파되도록 한다.

 

Remove Middle Man

위임 메서드가 많아지면 위임 객체를 직접 참조하도록 변경한다.

 

Before

manager = aPerson.manager;

class Person {
  get manager() {return this.department.manager;}

 

After

manager = aPerson.department.manager;

 

Motivation

앞서 Hide Delegate에서 위임 객체 캡슐화의 장점을 말하였다. 하지만 위임 메서드에는 비용이 있다. 클라이언트에서 위임 객체에 새로운 기능을 원할 때마다 서버에 메서드를 추가해야 한다. 서버는 단순히 중간자며 클라이언트가 위임 객제를 직접 호출하는 것이 나을 수 있다. 종종 데미테르 법칙을 열성적으로 따르면 이러한 smell이 나타난다.

 

Substitude Algoritm

알고리즘을 더욱 단순한 방법으로 바꾼다.

 

Before

function foundPerson(people) {
  for(let i = 0; i < people.length; i++) {
    if (people[i] === "Don") {
      return "Don";
    }
    if (people[i] === "John") {
      return "John";
    }
    if (people[i] === "Kent") {
      return "Kent";
    }
  }
  return "";
}

 

After

function foundPerson(people) {
  const candidates = ["Don", "John", "Kent"];
  return people.find(p => candidates.includes(p)) || '';
}

 

Motivation

리팩토링은 복잡한 부분을 단순한 조각들로 나눌 수도 있지만, 알고리즘 전체를 지우고 더욱 단순한 방법으로 대체할 수도 있다. 이는 문제에 대한 이해가 높아져서 쉬운 방법을 깨닫거나 라이브러리에서 코드에 중복된 기능을 제공할 때 발생할 수 있다.

'SW Engineering' 카테고리의 다른 글

Refactoring 예제  (0) 2019.12.07
Shotgun surgery vs. Divergent Change  (0) 2019.09.08