리팩토링 - 냄새 11. 기본형 집착
들어가기
해당 포스팅은 인프런 백기선님의 '리팩토링'을 학습 후 정리한 내용입니다.
냄새 11. 기본형 집착
Primitive Obsession
- 애플리케이션이 다루고 있는 도메인에 필요한 기본 타입을 만들지 않고 프로그래밍 언어가 제공하는 기본 타입을 사용하는 경우가 많다.
- 예) 전화번호, 좌표, 돈, 범위, 수량 등
- 기본형으로는 단위 (인치 vs 미터) 또는 표기법을 표현하기 어렵다.
- 관련 리팩토링 기술
- “기본형을 객체로 바꾸기 (Replace Primitive with Object)”
- “타입 코드를 서브클래스로 바꾸기 (Replace Type Code with Subclasses)”
- “조건부 로직을 다형성으로 바꾸기 (Replace Conditional with Polymorphism)”
- “클래스 추출하기 (Extract Class)”
- “매개변수 객체 만들기 (Introduce Parameter Object)”
기본형을 객체로 바꾸기
Replace Primitive with Object
- 개발 초기에는 기본형 (숫자 또는 문자열)으로 표현한 데이터가 나중에는 해당 데이터와 관련있는 다양한 기능을 필요로 하는 경우가 발생한다.
- 예) 문자열로 표현하던 전화번호의 지역 코드가 필요하거나 다양한 포맷을 지원하는 경우.
- 예) 숫자로 표현하던 온도의 단위 (화씨, 섭씨)를 변환하는 경우.
- 기본형을 사용한 데이터를 감싸 줄 클래스를 만들면, 필요한 기능을 추가할 수 있다.
예제 코드
Order
public class Order {
private String priority;
public Order(String priority) {
this.priority = priority;
}
public String getPriority() {
return priority;
}
}
OrderProcessor
public class OrderProcessor {
public long numberOfHighPriorityOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.getPriority() == "high" || o.getPriority() == "rush")
.count();
}
}
예제 코드 설명
- Order 클래스에서 우선순위를 의미하는 prioirty 필드를 기본 자료형으로 사용하고 있으며
- OrderProcessor 클래스 numberOfHighPriorityOrders() 함수는 Order 클래스를 순회하며 priority 가 high 또는 rush 인 객체를 찾아 개수를 count 한다.
냄새
priority 필드는 다양한 기능을 필요로하기 때문에 단순 기본 자료형으로 사용하기보다는 객체로 사용하여야 한다.
해결
기본형을 사용한 데이터를 감싸 줄 클래스를 만들자.
리팩토링 후
Order
public class Order {
private Priority priority;
public Order(String priorityValue) {
this(new Priority(priorityValue));
}
public Order(Priority priority) {
this.priority = priority;
}
public Priority getPriority() {
return priority;
}
}
OrderProcessor
public class OrderProcessor {
public long numberOfHighPriorityOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.getPriority().higherThan(new Priority("normal")))
.count();
}
}
Priority
public class Priority {
private String value;
private List<String> legalValues = List.of("low", "normal", "high", "rush");
public Priority(String value) {
if (this.legalValues.contains(value))
this.value = value;
else
throw new IllegalArgumentException("Illegal value for priority " + value);
}
@Override
public String toString() {
return this.value;
}
private int index() {
return legalValues.indexOf(this.value);
}
public boolean higherThan(Priority other) {
return this.index() > other.index();
}
}
설명
priorty 필드를 String 타입이 아닌 Priority 객체로 하여 내부에서 생성 시 검증 가능함과 동시에 다양한 기능을 수행하는 함수를 만들었다.
타입 코드를 서브클래스로 바꾸기
Replace Type Code with Subclasses
- 비슷하지만 다른 것들을 표현해야 하는 경우, 문자열(String), 열거형 (enum), 숫자 (int) 등으로 표현하기도 한다.
- 예) 주문 타입, “일반 주문”, “빠른 주문”
- 예) 직원 타입, “엔지니어”, “매니저”, “세일즈”
- 타입을 서브클래스로 바꾸는 계기
- 조건문을 다형성으로 표현할 수 있을 때, 서브클래스를 만들고 “조건부 로직을 다형성으로 바꾸기”를 적용한다.
- 특정 타입에만 유효한 필드가 있을 때, 서브클래스를 만들고 “필드 내리기”를 적용한다.
예제 코드
Employee
public class Employee {
private String name;
private String type;
public Employee(String name, String type) {
this.validate(type);
this.name = name;
this.type = type;
}
private void validate(String type) {
List<String> legalTypes = List.of("engineer", "manager", "salesman");
if (!legalTypes.contains(type)) {
throw new IllegalArgumentException(type);
}
}
public String getType() {
return type;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
}
예제 코드 설명
직원의 타입을 type 이라는 필드에서 engineer, manager, salesman 등 3가지로 분류할 수 있다.
냄새
직원 타입을 객체 내부 type 이라는 필드로 분류를 하고 있다.
해결
다형성을 활용하여 Employee 라는 클래스를 상속받은 Engineer, Manager, salesman 이라는 자식 클래스를 통해 타입을 분류하자!
리팩토링 후
Employee
public abstract class Employee {
private String name;
protected Employee(String name) {
this.name = name;
}
public static Employee createEmployee(String name, String type) {
switch (type) {
case "engineer": return new Engineer(name);
case "manager": return new Manager(name);
case "salesman": return new SalesMan(name);
default: throw new IllegalArgumentException(type);
}
}
protected abstract String getType();
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", type='" + getType() + '\'' +
'}';
}
}
Engineer
public class Engineer extends Employee{
public Engineer(String name) {
super(name);
}
@Override
public String getType() {
return "engineer";
}
}
해당 포스팅은 인프런 백기선님의 '리팩토링'을 학습 후 정리한 내용입니다.
Primitive Obsession
- 애플리케이션이 다루고 있는 도메인에 필요한 기본 타입을 만들지 않고 프로그래밍 언어가 제공하는 기본 타입을 사용하는 경우가 많다.
- 예) 전화번호, 좌표, 돈, 범위, 수량 등
- 기본형으로는 단위 (인치 vs 미터) 또는 표기법을 표현하기 어렵다.
- 관련 리팩토링 기술
- “기본형을 객체로 바꾸기 (Replace Primitive with Object)”
- “타입 코드를 서브클래스로 바꾸기 (Replace Type Code with Subclasses)”
- “조건부 로직을 다형성으로 바꾸기 (Replace Conditional with Polymorphism)”
- “클래스 추출하기 (Extract Class)”
- “매개변수 객체 만들기 (Introduce Parameter Object)”
기본형을 객체로 바꾸기
Replace Primitive with Object
- 개발 초기에는 기본형 (숫자 또는 문자열)으로 표현한 데이터가 나중에는 해당 데이터와 관련있는 다양한 기능을 필요로 하는 경우가 발생한다.
- 예) 문자열로 표현하던 전화번호의 지역 코드가 필요하거나 다양한 포맷을 지원하는 경우.
- 예) 숫자로 표현하던 온도의 단위 (화씨, 섭씨)를 변환하는 경우.
- 기본형을 사용한 데이터를 감싸 줄 클래스를 만들면, 필요한 기능을 추가할 수 있다.
예제 코드
Order
public class Order {
private String priority;
public Order(String priority) {
this.priority = priority;
}
public String getPriority() {
return priority;
}
}
OrderProcessor
public class OrderProcessor {
public long numberOfHighPriorityOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.getPriority() == "high" || o.getPriority() == "rush")
.count();
}
}
예제 코드 설명
- Order 클래스에서 우선순위를 의미하는 prioirty 필드를 기본 자료형으로 사용하고 있으며
- OrderProcessor 클래스 numberOfHighPriorityOrders() 함수는 Order 클래스를 순회하며 priority 가 high 또는 rush 인 객체를 찾아 개수를 count 한다.
냄새
priority 필드는 다양한 기능을 필요로하기 때문에 단순 기본 자료형으로 사용하기보다는 객체로 사용하여야 한다.
해결
기본형을 사용한 데이터를 감싸 줄 클래스를 만들자.
리팩토링 후
Order
public class Order {
private Priority priority;
public Order(String priorityValue) {
this(new Priority(priorityValue));
}
public Order(Priority priority) {
this.priority = priority;
}
public Priority getPriority() {
return priority;
}
}
OrderProcessor
public class OrderProcessor {
public long numberOfHighPriorityOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.getPriority().higherThan(new Priority("normal")))
.count();
}
}
Priority
public class Priority {
private String value;
private List<String> legalValues = List.of("low", "normal", "high", "rush");
public Priority(String value) {
if (this.legalValues.contains(value))
this.value = value;
else
throw new IllegalArgumentException("Illegal value for priority " + value);
}
@Override
public String toString() {
return this.value;
}
private int index() {
return legalValues.indexOf(this.value);
}
public boolean higherThan(Priority other) {
return this.index() > other.index();
}
}
설명
priorty 필드를 String 타입이 아닌 Priority 객체로 하여 내부에서 생성 시 검증 가능함과 동시에 다양한 기능을 수행하는 함수를 만들었다.
타입 코드를 서브클래스로 바꾸기
Replace Type Code with Subclasses
- 비슷하지만 다른 것들을 표현해야 하는 경우, 문자열(String), 열거형 (enum), 숫자 (int) 등으로 표현하기도 한다.
- 예) 주문 타입, “일반 주문”, “빠른 주문”
- 예) 직원 타입, “엔지니어”, “매니저”, “세일즈”
- 타입을 서브클래스로 바꾸는 계기
- 조건문을 다형성으로 표현할 수 있을 때, 서브클래스를 만들고 “조건부 로직을 다형성으로 바꾸기”를 적용한다.
- 특정 타입에만 유효한 필드가 있을 때, 서브클래스를 만들고 “필드 내리기”를 적용한다.
예제 코드
Employee
public class Employee {
private String name;
private String type;
public Employee(String name, String type) {
this.validate(type);
this.name = name;
this.type = type;
}
private void validate(String type) {
List<String> legalTypes = List.of("engineer", "manager", "salesman");
if (!legalTypes.contains(type)) {
throw new IllegalArgumentException(type);
}
}
public String getType() {
return type;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
}
예제 코드 설명
직원의 타입을 type 이라는 필드에서 engineer, manager, salesman 등 3가지로 분류할 수 있다.
냄새
직원 타입을 객체 내부 type 이라는 필드로 분류를 하고 있다.
해결
다형성을 활용하여 Employee 라는 클래스를 상속받은 Engineer, Manager, salesman 이라는 자식 클래스를 통해 타입을 분류하자!
리팩토링 후
Employee
public abstract class Employee {
private String name;
protected Employee(String name) {
this.name = name;
}
public static Employee createEmployee(String name, String type) {
switch (type) {
case "engineer": return new Engineer(name);
case "manager": return new Manager(name);
case "salesman": return new SalesMan(name);
default: throw new IllegalArgumentException(type);
}
}
protected abstract String getType();
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", type='" + getType() + '\'' +
'}';
}
}
Engineer
public class Engineer extends Employee{
public Engineer(String name) {
super(name);
}
@Override
public String getType() {
return "engineer";
}
}
- 예) 문자열로 표현하던 전화번호의 지역 코드가 필요하거나 다양한 포맷을 지원하는 경우.
- 예) 숫자로 표현하던 온도의 단위 (화씨, 섭씨)를 변환하는 경우.
public class Order {
private String priority;
public Order(String priority) {
this.priority = priority;
}
public String getPriority() {
return priority;
}
}
public class OrderProcessor {
public long numberOfHighPriorityOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.getPriority() == "high" || o.getPriority() == "rush")
.count();
}
}
- Order 클래스에서 우선순위를 의미하는 prioirty 필드를 기본 자료형으로 사용하고 있으며
- OrderProcessor 클래스 numberOfHighPriorityOrders() 함수는 Order 클래스를 순회하며 priority 가 high 또는 rush 인 객체를 찾아 개수를 count 한다.
priority 필드는 다양한 기능을 필요로하기 때문에 단순 기본 자료형으로 사용하기보다는 객체로 사용하여야 한다.
기본형을 사용한 데이터를 감싸 줄 클래스를 만들자.
public class Order {
private Priority priority;
public Order(String priorityValue) {
this(new Priority(priorityValue));
}
public Order(Priority priority) {
this.priority = priority;
}
public Priority getPriority() {
return priority;
}
}
public class OrderProcessor {
public long numberOfHighPriorityOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.getPriority().higherThan(new Priority("normal")))
.count();
}
}
public class Priority {
private String value;
private List<String> legalValues = List.of("low", "normal", "high", "rush");
public Priority(String value) {
if (this.legalValues.contains(value))
this.value = value;
else
throw new IllegalArgumentException("Illegal value for priority " + value);
}
@Override
public String toString() {
return this.value;
}
private int index() {
return legalValues.indexOf(this.value);
}
public boolean higherThan(Priority other) {
return this.index() > other.index();
}
}
priorty 필드를 String 타입이 아닌 Priority 객체로 하여 내부에서 생성 시 검증 가능함과 동시에 다양한 기능을 수행하는 함수를 만들었다.
Replace Type Code with Subclasses
- 비슷하지만 다른 것들을 표현해야 하는 경우, 문자열(String), 열거형 (enum), 숫자 (int) 등으로 표현하기도 한다.
- 예) 주문 타입, “일반 주문”, “빠른 주문”
- 예) 직원 타입, “엔지니어”, “매니저”, “세일즈”
- 타입을 서브클래스로 바꾸는 계기
- 조건문을 다형성으로 표현할 수 있을 때, 서브클래스를 만들고 “조건부 로직을 다형성으로 바꾸기”를 적용한다.
- 특정 타입에만 유효한 필드가 있을 때, 서브클래스를 만들고 “필드 내리기”를 적용한다.
예제 코드
Employee
public class Employee {
private String name;
private String type;
public Employee(String name, String type) {
this.validate(type);
this.name = name;
this.type = type;
}
private void validate(String type) {
List<String> legalTypes = List.of("engineer", "manager", "salesman");
if (!legalTypes.contains(type)) {
throw new IllegalArgumentException(type);
}
}
public String getType() {
return type;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
}
예제 코드 설명
직원의 타입을 type 이라는 필드에서 engineer, manager, salesman 등 3가지로 분류할 수 있다.
냄새
직원 타입을 객체 내부 type 이라는 필드로 분류를 하고 있다.
해결
다형성을 활용하여 Employee 라는 클래스를 상속받은 Engineer, Manager, salesman 이라는 자식 클래스를 통해 타입을 분류하자!
리팩토링 후
Employee
public abstract class Employee {
private String name;
protected Employee(String name) {
this.name = name;
}
public static Employee createEmployee(String name, String type) {
switch (type) {
case "engineer": return new Engineer(name);
case "manager": return new Manager(name);
case "salesman": return new SalesMan(name);
default: throw new IllegalArgumentException(type);
}
}
protected abstract String getType();
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", type='" + getType() + '\'' +
'}';
}
}
Engineer
public class Engineer extends Employee{
public Engineer(String name) {
super(name);
}
@Override
public String getType() {
return "engineer";
}
}
Manager, Salesman 중복 생략
Manager, Salesman 클래스도 Engineer 클래스와 구조가 같다.
설명
- 리팩토링 이전 코드는 Employee 클래스에서 직원 타입을 분리하기 위해 내부 필드 type 의 문자열 값으로 구분하였다.
- 리팩토링 후 Employee 를 추상클래스로 수정 후 각 서브 클래스에서 Employee를 상속받아 Employee 클래스에서 팩토리 패턴으로 타입에 해당하는 서브 클래스를 생성한다.
조건부 로직을 다형성으로 바꾸기
Replace Conditional with Polymorphism
- 복잡한 조건식을 상속과 다형성을 사용해 코드를 보다 명확하게 분리할 수 있다.
- swich 문을 사용해서 타입에 따라 각기 다른 로직을 사용하는 코드.
- 기본 동작과 (타입에 따른) 특수한 기능이 섞여있는 경우에 상속 구조를 만들어서 기본 동작을 상위클래스에 두고 특수한 기능을 하위클래스로 옮겨서 각 타입에 따른 “차이점”을 강조할 수 있다.
- 모든 조건문을 다형성으로 옮겨야 하는가? 단순한 조건문은 그대로 두어도 좋다. 오직 복잡한 조건문을 다형성을 활용해 좀 더 나은 코드로 만들 수 있는 경우에만 적용한다. (과용을 조심하자.)
예제 코드
Employee
public class Employee {
protected String type;
protected List<String> availableProjects;
public Employee(String type, List<String> availableProjects) {
this.type = type;
this.availableProjects = availableProjects;
}
public int vacationHours() {
return switch (type) {
case "full-time" -> 120;
case "part-time" -> 80;
case "temporal" -> 32;
default -> 0;
};
}
public boolean canAccessTo(String project) {
return switch (type) {
case "full-time" -> true;
case "part-time", "temporal" -> this.availableProjects.contains(project);
default -> false;
};
}
}
예제 코드 설명
type 이 full-time, part-time, temporal 인지에 따라 canAccessTo() 함수의 결과 값이 달리지고, vactionHours() 함수의 결과 값도 달라진다.
냄새
Employee 클래스 vacationHours(), canAccessTo() 함수는 필드인 type 에 따라 각기 다른 동작을 수행한다. 다형성을 활용하여 기본 동작을 상위클래스에 두고 특수한 동작을 하위클래스로 옮길 필요가 있다.
해결
타입에 따른 Employee 클래스를 상속 받는 자식 클래스를 만들자!
리팩토링 후
Employee
public abstract class Employee {
protected List<String> availableProjects;
public Employee(List<String> availableProjects) {
this.availableProjects = availableProjects;
}
public Employee() {
}
protected abstract int vacationHours();
protected abstract boolean canAccessTo(String project);
}
FullTimeEmployee
public class FullTimeEmployee extends Employee {
@Override
public int vacationHours() {
return 120;
}
@Override
public boolean canAccessTo(String project) {
return true;
}
}
PartTimeEmployee
public class PartTimeEmployee extends Employee{
public PartTimeEmployee(List<String> availableProjects) {
super(availableProjects);
}
@Override
public int vacationHours() {
return 80;
}
@Override
public boolean canAccessTo(String project) {
return super.availableProjects.contains(project);
}
}
TemporalEmployee
public class TemporalEmployee extends Employee{
public TemporalEmployee(List<String> availableProjects) {
super(availableProjects);
}
@Override
public int vacationHours() {
return 32;
}
@Override
public boolean canAccessTo(String project) {
return super.availableProjects.contains(project);
}
}
설명
Employee 를 추상클래스로 하여 기본동작은 Employee 클래스에서 수행하고, FullTimeEmployee, PartTimeEmployee, TemporalEmployee 에서 각 타입별 특수한 동작을 수행한다.
Author And Source
이 문제에 관하여(리팩토링 - 냄새 11. 기본형 집착), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@coconenne/리팩토링-냄새-11.-기본형-집착
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
public class Employee {
protected String type;
protected List<String> availableProjects;
public Employee(String type, List<String> availableProjects) {
this.type = type;
this.availableProjects = availableProjects;
}
public int vacationHours() {
return switch (type) {
case "full-time" -> 120;
case "part-time" -> 80;
case "temporal" -> 32;
default -> 0;
};
}
public boolean canAccessTo(String project) {
return switch (type) {
case "full-time" -> true;
case "part-time", "temporal" -> this.availableProjects.contains(project);
default -> false;
};
}
}
type 이 full-time, part-time, temporal 인지에 따라 canAccessTo() 함수의 결과 값이 달리지고, vactionHours() 함수의 결과 값도 달라진다.
Employee 클래스 vacationHours(), canAccessTo() 함수는 필드인 type 에 따라 각기 다른 동작을 수행한다. 다형성을 활용하여 기본 동작을 상위클래스에 두고 특수한 동작을 하위클래스로 옮길 필요가 있다.
타입에 따른 Employee 클래스를 상속 받는 자식 클래스를 만들자!
public abstract class Employee {
protected List<String> availableProjects;
public Employee(List<String> availableProjects) {
this.availableProjects = availableProjects;
}
public Employee() {
}
protected abstract int vacationHours();
protected abstract boolean canAccessTo(String project);
}
public class FullTimeEmployee extends Employee {
@Override
public int vacationHours() {
return 120;
}
@Override
public boolean canAccessTo(String project) {
return true;
}
}
public class PartTimeEmployee extends Employee{
public PartTimeEmployee(List<String> availableProjects) {
super(availableProjects);
}
@Override
public int vacationHours() {
return 80;
}
@Override
public boolean canAccessTo(String project) {
return super.availableProjects.contains(project);
}
}
public class TemporalEmployee extends Employee{
public TemporalEmployee(List<String> availableProjects) {
super(availableProjects);
}
@Override
public int vacationHours() {
return 32;
}
@Override
public boolean canAccessTo(String project) {
return super.availableProjects.contains(project);
}
}
Employee 를 추상클래스로 하여 기본동작은 Employee 클래스에서 수행하고, FullTimeEmployee, PartTimeEmployee, TemporalEmployee 에서 각 타입별 특수한 동작을 수행한다.
Author And Source
이 문제에 관하여(리팩토링 - 냄새 11. 기본형 집착), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@coconenne/리팩토링-냄새-11.-기본형-집착저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)