| 인프런 - 백기선님의 코딩으로 학습하는 리팩토링 강의를 수강하며 정리한 글입니다.
가변 데이터
- 데이터를 변경하다보면 예상치 못했던 결과나 해결하기 어려운 버그가 발생할 수 있다.
- 함수형 프로그래밍 언어에서는 데이터를 변경하지 않고 복사본을 전달하지만, 자바 같은 경우 데이터 변경을 허용하기 때문에 변경되는 데이터 사용 시 발생할 수 있는 리스크를 관리할 수 있는 방법을 적용하는 것이 좋다.
- 관련 리팩토링
- 변수 캡슐화하기 -> 데이터를 변경할 수 있는 메서드를 제한하고 관리할 수 있다.
- 변수 쪼개기 -> 여러 데이터를 저장하는 변수를 나눌 수 있다.
- 코드 정리하기 -> 데이터를 변경하는 코드를 분리하고 피할 수 있다.
- 함수 추출하기 -> 데이터를 변경하는 코드로부터 사이드 이팩트가 없는 코드를 분리할 수 있다.
- 질의 함수와 변경 함수 변경하기 -> 클라이언트가 원하는 경우에만 사이드 이팩트가 발생할 수 있는 함수를 호출하도록 개선할 수 있다.
- 세터 제거하기를 적용한다.
- 파생 함수를 질의함수로 변경하기를 적용한다.
- 여러 함수를 클래스로 묶기, 여러 함수를 변환 함수로 변경하기 -> 변수가 사용되는 범위를 제한한다.
- 참조를 값으로 바꾸기 -> 데이터의 일부를 변경하기 보다는 데이터 전체를 교체할 수 있다.
Refactoring 1. 변수 쪼개기
- 어떤 변수가 여러 번 재할당 되어도 적절한 경우
- 반복문을 순회하는데 사용되는 변수 또는 인덱스
- 값을 축적시키는데 사용하는 변수
- 그 밖의 경우에 재할당되는 변수가 존재한다면 해당 변수는 여러 용도로 사용되는 것이며 변수를 분리해야 더 이해하기 좋은 코드가 될 수 있다.
- 변수 하나 당 하나의 책임을 갖도록 한다.
- 상수를 활용한다. ex) const, final...
Before
public class Rectangle {
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;
temp = height * width;
System.out.println("Area: " + temp);
area = temp;
}
public double getPerimeter() {
return perimeter;
}
public double getArea() {
return area;
}
}
After
public class Rectangle {
private double perimeter;
private double area;
public void updateGeometry(double height, double width) {
final double perimeter = 2 * (height + width);
System.out.println("Perimeter: " + perimeter);
this.perimeter = perimeter;
final double area = height * width;
System.out.println("Area: " + area);
this.area = area;
}
public double getPerimeter() {
return perimeter;
}
public double getArea() {
return area;
}
}
Refactoring 2. 질의 함수와 변경 함수 분리하기
- 눈에 띌만한 사이드 이팩트 없이 값을 조회할 수 있는 메서드는 테스트 하기 쉽고, 메서드 이동도 편리하다.
- 명령-조회 분리
- 어떤 값을 리턴하는 함수는 사이드 이팩트가 없어야 한다.
- 어떤 값을 리턴하는 함수는 사이드 이팩트가 없어야 한다.
- 눈에 띌만한 사이드 이팩트
- 캐시는 중요한 객체 상태 변화는 아니므로, 어떤 메서드 호출로 인해 캐시 데이터를 변경하더라도 분리할 필요는 없다.
Before
public class Billing {
private Customer customer;
private EmailGateway emailGateway;
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));
}
private String formatBill(Customer customer) {
return "sending bill for " + customer.getName();
}
}
After
public class Billing {
private Customer customer;
private EmailGateway emailGateway;
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));
}
private String formatBill(Customer customer) {
return "sending bill for " + customer.getName();
}
}
=> 값 계산, Bill 전송 두 개의 역할을 분리
Refactoring 3. 세터 제거하기
- 세터가 존재하는 것은 해당 필드가 변경될 수 있다는 것을 의미한다.
- 객체 생성 시 처음 설정된 값이 변경될 필요가 없다면, 해당 값을 설정할 수 있는 생성자를 만들고 세터를 제거해서 필드가 변경될 가능성을 제거해야 한다.
Before
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;
}
}
After
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;
}
}
=> 처음 Id 가 주어지고 이 후 변경되지 않을 시 적용할 수 있다.
Refactoring 4. 파생 변수를 질의 함수로 바꾸기다
- 변경될 수 있는 데이터를 최대한 줄이도록 노력해야 한다.
- 계산해서 알아낼 수 있는 변수는 제거할 수 있다.
- 계산 자체가 데이터의 의미를 잘 표현한 것일 수 있다.
- 해당 변수가 어디선가 잘못된 값으로 수정될 수 있는 가능성을 제거할 수 있다.
- 계산에 필요한 데이터가 변하지 않는 값이라면, 계산의 결과에 해당하는 데이터 역시 불변하다.
Before
public class Discount {
private double discountedTotal;
private double discount;
private double baseTotal;
public Discount(double baseTotal) {
this.baseTotal = baseTotal;
}
public double getDiscountedTotal() {
return this.discountedTotal;
}
public void setDiscount(double number) {
this.discount = number;
this.discountedTotal = this.baseTotal - this.discount;
}
}
=> setDiscount 호출 전 getDiscountedTotal() 호출 시 0이 호출 된다.
After
public class Discount {
private double discount;
private double baseTotal;
public Discount(double baseTotal) {
this.baseTotal = baseTotal;
}
public double getDiscountedTotal() {
return this.baseTotal - this.discount;
}
public void setDiscount(double number) {
this.discount = number;
}
}
=> 수식 결과를 return 함으로써 불필요한 변수 제거
Refactoring 5. 여러 함수를 변환 함수로 묶기
- 관련있는 여러 파생 변수를 만들어내는 함수가 여러 곳에서 만들어지고 사용된다면 그러한 파생 변수를 변환 함수를 통해 한 곳으로 모아둘 수 있다.
- 소스 데이터가 변경될 수 있는 경우 여러 함수를 클래스로 묶기를 사용하는 것이 적절한다.
- 소스 데이터가 변경되지 않는 경우에는 두 가지 방법을 모두 사용할 수 있지만, 변환 함수를 사용해서 불변 데이터의 필드로 생성해 두고 재사용할 수 있다.
Before
public class Client1 {
double baseCharge;
public Client1(Reading reading) {
this.baseCharge = baseRate(reading.month(), reading.year()) * reading.quantity();
}
private double baseRate(Month month, Year year) {
return 10;
}
public double getBaseCharge() {
return baseCharge;
}
}
public class Client2 {
private double base;
private double taxableCharge;
public Client2(Reading reading) {
this.base = baseRate(reading.month(), reading.year()) * reading.quantity();
this.taxableCharge = Math.max(0, this.base - taxThreshold(reading.year()));
}
private double taxThreshold(Year year) {
return 5;
}
private double baseRate(Month month, Year year) {
return 10;
}
public double getBase() {
return base;
}
public double getTaxableCharge() {
return taxableCharge;
}
}
public record Reading(String customer, double quantity, Month month, Year year) {
}
After
public class Client {
protected double baseRate(Month month, Year year) {
return 10;
}
protected EnrichReading enrichReading(Reading reading) {
return new EnrichReading(reading, calculateVaseCharge(reading));
}
private double calculateVaseCharge(Reading reading) {
return baseRate(reading.month(), reading.year()) * reading.quantity();
}
}
public class Client1 extends Client {
double baseCharge;
public Client1(Reading reading) {
this.baseCharge = enrichReading(reading).baseCharge();
}
public double getBaseCharge() {
return baseCharge;
}
}
public class Client2 extends Client {
private double base;
private double taxableCharge;
public Client2(Reading reading) {
this.base = baseRate(reading.month(), reading.year()) * reading.quantity();
this.taxableCharge = enrichReading(reading).baseCharge();
}
private double taxThreshold(Year year) {
return 5;
}
public double getBase() {
return base;
}
public double getTaxableCharge() {
return taxableCharge;
}
}
public record EnrichReading(Reading reading, double baseCharge) {
}
public record Reading(String customer, double quantity, Month month, Year year) {
}
=> 공통으로 계산되는 baseCharge를 Client Class에서 계산한다.
Refactoring 6. 참조를 값으로 바꾸기
- 래퍼런스 객체 vs 값 객체
- 값 객체는 객체가 가진 필드의 값으로 동일성을 확인한다.
- 값 객체는 변하지 않는다.
- 어떤 객체의 변경 내역을 다른 곳으로 전파시키고 싶다면 레퍼런스, 아니면 값 객체를 사용한다.
Before
public class Person {
private TelephoneNumber officeTelephoneNumber;
public String officeAreaCode() {
return this.officeTelephoneNumber.areaCode();
}
public void officeAreaCode(String areaCode) {
this.officeTelephoneNumber.areaCode(areaCode);
}
public String officeNumber() {
return this.officeTelephoneNumber.number();
}
public void officeNumber(String number) {
this.officeTelephoneNumber.number(number);
}
}
public class TelephoneNumber {
private String areaCode;
private String number;
public String areaCode() {
return areaCode;
}
public void areaCode(String areaCode) {
this.areaCode = areaCode;
}
public String number() {
return number;
}
public void number(String number) {
this.number = number;
}
}
After
public class Person {
private TelephoneNumber officeTelephoneNumber;
public String officeAreaCode() {
return this.officeTelephoneNumber.areaCode();
}
public void officeAreaCode(String areaCode) {
this.officeTelephoneNumber = new TelephoneNumber(areaCode, this.officeNumber());
}
public String officeNumber() {
return this.officeTelephoneNumber.number();
}
public void officeNumber(String number) {
this.officeTelephoneNumber = new TelephoneNumber(this.officeAreaCode(), number);
}
}
public class TelephoneNumber {
private final String areaCode;
private final String number;
public TelephoneNumber(String areaCode, String number) {
this.areaCode = areaCode;
this.number = number;
}
public String areaCode() {
return areaCode;
}
public String number() {
return number;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
TelephoneNumber that = (TelephoneNumber) o;
return Objects.equals(areaCode, that.areaCode) && Objects.equals(number, that.number);
}
@Override
public int hashCode() {
return Objects.hash(areaCode, number);
}
}
public record TelephoneNumber(String areaCode, String number) {
}
'클린코드' 카테고리의 다른 글
[Refactoring] 8. 산탄총 수술 (0) | 2022.09.11 |
---|---|
[Refactoring] 7. 뒤엉킨 변경 (0) | 2022.09.10 |
[Refactoring] 5. 전역 데이터 (0) | 2022.09.08 |
[Refactoring] 4. 긴 매개변수 목록 (0) | 2022.09.07 |
[Refactoring] 3. 긴 함수 (0) | 2022.09.06 |