public Haggis(double primaryForce, double secondaryForce, double mass, int delay) {
...
}
public double distanceTravelled(int time) {
double result;
double acc = primaryForce / mass;
int primaryTime = Math.min(time, delay);
result = 0.5 acc primaryTime * primaryTime;
int secondaryTime = time - delay;
if (secondaryTime > 0) {
double primaryVelocity = acc * delay;
acc = (primaryForce + secondaryForce) / mass;
result += primaryVelocity * secondaryTime + 0.5 * acc * secondaryTime + secondaryTime;
}
return result;
}
Order
inputValue의 중복된 사용으로 가독성이 떨어져 새로운 변수 result를 추가하도록 하자.
public class Order {
public double discount(double inputValue, int quantity) {
if (inputValue > 50) inputValue = inputValue - 2;
if (quantity > 100) inputValue = inputValue - 1;
return inputValue;
}
}
리팩토링 19. 질의 함수와 변경 함수 분리하기 (Separate Query from Modifier)
“눈에 띌만한” 사이드 이팩트 없이 값을 조회할 수 있는 메소드는 테스트 하기도 쉽고, 메소드를 이동하기도 편하다.
명령-조회 분리 (command-query separation) 규칙:
어떤 값을 리턴하는 함수는 사이드 이팩트가 없어야 한다.
“눈에 띌만한 (observable) 사이드 이팩트”
가령, 캐시는 중요한 객체 상태 변화는 아니다. 따라서 어떤 메소드 호출로 인해, 캐시 데이터를 변경하더라도 분리할 필요는 없다.
예제
변경 전 (Billing)
조회와 변경을 getTotalOutstandingAndSendBill()에서 실행하고 있다.
public class Billing {
...
public Billing(Customer customer, EmailGateway emailGateway) {
this.customer = customer;
this.emailGateway = emailGateway;
}
public double totalOutstanding() {
return customer.getInvoices().stream()
.map(Invoice::getAmount)
.reduce((double) 0, Double::sum);
}
public void sendBill() {
emailGateway.send(formatBill(customer));
}
...
}
변경 전 (Criminal)
modifier와 query가 한 메소드에 있으면 테스트하기도 어렵다.
for문과 if문의 중첩을 분리해준다.
public class Criminal {
public String alertForMiscreant(List<Person> people) {
for (Person p : people) {
if (p.getName().equals("Don")) {
setOffAlarms();
return "Don";
}
if (p.getName().equals("John")) {
setOffAlarms();
return "John";
}
}
return "";
}
private void setOffAlarms() {
System.out.println("set off alarm");
}
}
변경 후 (Criminal)
public class Criminal {
public void alertForMiscreant(List<Person> people) {
if (!findMiscreant(people).isBlank())
setOffAlarms();
}
public String findMiscreant(List<Person> people) {
for (Person p : people) {
if (p.getName().equals("Don")) {
return "Don";
}
if (p.getName().equals("John")) {
return "John";
}
}
return "";
}
private void setOffAlarms() {
System.out.println("set off alarm");
}
}
리팩토링 20. 세터 제거하기 (Remove Setting Method)
세터를 제공한다는 것은 해당 필드가 변경될 수 있다는 것을 뜻한다. (세터를 쓸 수 있는 시점에는 언제든지)
객체 생성시 처음 설정된 값이 변경될 필요가 없다면 해당 값을 설정할 수 있는 생성자를 만들고 세터를 제거해서 변경될 수 있는 가능성을 제거해야 한다.
예제
변경 전
"id"는 초기에 설정한 값을 계속 유지하고 싶다고 가정해보자.
생성자 주입으로 해결할 수 있다.
public class Person {
private String name;
private int id;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
변경 후
public class Person {
private String name;
private int id;
public Person(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getId() {
return id;
}
}
냄새 6. 가변 데이터 (Mutable Data)
관련 리팩토링
변수 캡슐화하기 (Encapsulate Variable)
를 적용해 데이터를 변경할 수 있는 메소드를 제한하고 관리할 수 있다.변수 쪼개기 (Split Variable)
을 사용해 여러 데이터를 저장하는 변수를 나눌 수 있다.코드 정리하기 (Slide Statements)
를 사용해 데이터를 변경하는 코드를 분리하고 피할 수 있다.함수 추출하기 (Extract Function)
으로 데이터를 변경하는 코드로부터 사이드 이팩트가 없는 코드를 분리할 수 있다.질의 함수와 변경 함수 분리하기 (Separate Query from Modifier)
를 적용해서 클라이언트가 원하는 경우에만 사이드 이팩트가 있는 함수를 호출하도록 API를 개선할 수 있다.세터 제거하기 (Remove Setting Method)
를 적용한다.파생 변수를 질의 함수로 바꾸기 (Replace Derived Variable with Query)
를 적용할 수 있다.여러 함수를 클래스로 묶기 (Combine Functions into Class)
또는여러 함수를 변환 함수로 묶기 (Combine Functions into Transform)
을 적용할 수 있다.참조를 값으로 바꾸기 (Change Reference to Value)
를 적용해서 데이터 일부를 변경하기 보다는 데이터 전체를 교체할 수 있다.리팩토링 18. 변수 쪼개기 (Split Variable)
예제
Rectangle
temp
→perimeter
, 아래의temp
→area
변경해주면 의미해주는 바가 명확해져 가독성이 좋아진다.double final perimeter
로 final을 사용하면 의미가 조금 더 명확해진다.private double perimeter; private double area;
public void updateGeometry(double height, double width) { double temp = 2 * (height + width); System.out.println("Perimeter: " + temp); perimeter = temp;
} ... }
Haggis
acc
가 다른 계산에 두 번 사용되었기 때문에,primaryAcceleration
과secondaryAcceleration
으로 변수를 쪼개주면 변수의 의미가 조금 더 명확해진다.result
는 값이 추가되는 변수이기 때문에 사용이 옳다고 판단한다.private double primaryForce; private double secondaryForce; private double mass; private int delay;
public Haggis(double primaryForce, double secondaryForce, double mass, int delay) { ... }
public double distanceTravelled(int time) { double result; double acc = primaryForce / mass; int primaryTime = Math.min(time, delay); result = 0.5 acc primaryTime * primaryTime;
}
inputValue
의 중복된 사용으로 가독성이 떨어져 새로운 변수result
를 추가하도록 하자.리팩토링 19. 질의 함수와 변경 함수 분리하기 (Separate Query from Modifier)
예제
변경 전 (Billing)
getTotalOutstandingAndSendBill()
에서 실행하고 있다.public Billing(Customer customer, EmailGateway emailGateway) { this.customer = customer; this.emailGateway = emailGateway; }
public double getTotalOutstandingAndSendBill() { double result = customer.getInvoices().stream() .map(Invoice::getAmount) .reduce((double) 0, Double::sum); sendBill(); return result; }
private void sendBill() { emailGateway.send(formatBill(customer)); } ... }
리팩토링 20. 세터 제거하기 (Remove Setting Method)
예제
리팩토링 21. 파생 변수를 질의 함수로 바꾸기 (Replace Derived Variable with Query)
예제
discountedTotal
은discount
와baseTotal
로부터 derived된 값이다.setDiscount()
호출 전에getDiscount()
를 호출하면discountedTotal
의 값이 계산 전의 가격이 출력될 것이다.getDiscount()
의 첫째 줄에 "assert this.discountedTotal == this.baseTotal - thi.discount;" 를 입력한다.production
을 리팩토링할 여지가 있다.리팩토링 22. 여러 함수를 변환 함수로 묶기 (Combine Functions into Transform)
리팩토링 23. 참조를 값으로 바꾸기 (Change Reference to Value)
예제
Java 14
이후부터 제공하는 기능인 record를 사용하면 위와 같은 기능을 동일하게 제공할 수 있다.