function createEmployee(name, type) {
return new Employee(name, type);
}
리팩터링 후
function createEmployee(name, type) {
switch (type) {
case "engineer": return new Engineer(name);
case "salesman": return new Salesman(name);
case "manager": return new Manager (name);
}
🧷 배경
소프트웨어 시스템에서는 비슷한 대상들을 특정 특성에 따라 구분해야 할 때가 자주 있는데 이를 다루는 수단으로 타입 코드 필드가 있다.
서브클래스는 두 가지 면에서 매력적이다.
조건에 따라 다르게 동작하도록 해주는 다형성을 제공한다.
특정 타입에서만 의미가 있는 값을 사용하는 필드나 메서드가 있을 때 좋다.
이 리팩터링은 대상 클래스에 직접 적용할지, 아니면 타입 코드 자체에 적용할지를 고민해야 한다.
전자) 직원의 하위 타입인 엔지니어 만들 것
후자) 직원에게 직원 유형 '속성'을 부여하고, 이 속성을 클래스로 정의해 엔지니어 속성과 관리자 속성 같은 서브클래스를 만드는 식
🧷 절차
타입 코드 필드를 자가 캡슐화한다.
타입 코드 값 하나를 선택하여 그 값에 해당하는 서브클래스를 만든다. 타입 코드 게터 메서드를 오버라이드하여 해당 타입 코드의 리터럴 값을 반환하게 한다.
매개변수로 받은 타입 코드와 방금 만든 서브클래스를 매핑하는 선택 로직을 만든다.
테스트한다.
타입 코드 값 각각에 대해 서브클래스 생성과 선택 로직 추가를 반복한다. 클래스 하나가 완성될 때마다 테스트한다.
타입 코드 필드를 제거한다.
테스트한다.
타입 코드 접근자를 이용하는 메서드 모두에 메서드 내리기와 조건부 로직을 다형성으로 바꾸기를 적용한다.
🧷 예시: 직접 상속하는 경우
🧷 리팩터링 전
class Employee {
constructor(name, type) {
this.validateType(type);
this._name = name;
this._type = type;
}
validateType(arg) {
if (!['engineer', 'manager', 'salesman'].includes(arg)) {
throw new Error(`Employee cannot be of type ${arg}`);
}
}
toString() {
return `${this._name} (${this._type})`;
}
}
🧷 리팩터링 후
class Employee {
constructor(name) {
this._name = name;
}
toString() {
return `${this._name} (${this.type})`;
}
}
class Engineer extends Employee {
get type() {
return 'engineer';
}
}
class Salesperson extends Employee {
get type() {
return 'salesperson';
}
}
class Manager extends Employee {
get type() {
return 'manager';
}
}
function createEmployee(name, type) {
switch (type) {
case 'engineer':
return new Engineer(name);
case 'salesperson':
return new Salesperson(name);
case 'manager':
return new Manager(name);
default: throw new Error(`Employee cannot be of type ${arg}`);
}
}
🧷 예시: 간접 상속하는 경우
🧷 리팩터링 전
class Employee {
constructor(name, type) {
this.validateType(type);
this._name = name;
this._type = type;
}
validateType(arg) {
if (!['engineer', 'manager', 'salesperson'].includes(arg)) {
throw new Error(`${arg}라는 직원 유형은 없습니다.`);
}
}
get type() {
return this._type;
}
set type(arg) {
this._type = arg;
}
get capitalizedType() {
return this._type.charAt(0).toUpperCase() + this._type.substr(1);
}
toString() {
return `${this._name} (${this.capitalizedType})`;
}
}
🧷 리팩터링 후
class EmployeeType {
constructor(aString) {
this._value = aString;
}
toString() {
return this._value;
}
get capitalizedType() {
return this.toString().charAt(0).toUpperCase() + this.toString().substr(1);
}
}
class Engineer extends EmployeeType {
toString() {
return 'engineer';
}
}
class Manager extends EmployeeType {
toString() {
return 'manager';
}
}
class Salesperson extends EmployeeType {
toString() {
return 'salesperson';
}
}
class Employee {
constructor(name, type) {
this.validateType(type);
this._name = name;
this._type = type;
}
validateType(arg) {
if (!['engineer', 'manager', 'salesperson'].includes(arg)) {
throw new Error(`${arg}라는 직원 유형은 없습니다.`);
}
}
get typeString() {
return this._type.toString();
}
get type() {
return this._type;
}
set type(arg) {
this._type = new EmployeeType(arg);
}
static createEmployeeType(aString) {
switch(aString) {
case 'engineer': return new Engineer();
case 'manager': return new Manager();
case 'salesperson': return new Salesperson();
default: throw new Error(`${aString}라는 직원 유형은 없습니다.`);
}
}
toString() {
return `${this._name} (${this.type.capitalizedType})`;
}
}