본문 바로가기

클린코드

[Refactoring] 7. 뒤엉킨 변경

 

 

코딩으로 학습하는 리팩토링 - 인프런 | 강의

리팩토링은 소프트웨어 엔지니어가 갖춰야 할 기본적인 소양 중 하나입니다. 이 강의는 인텔리J와 자바를 사용하여 보다 실용적인 방법으로 다양한 코드의 냄새와 리팩토링 기술을 설명하고 직

www.inflearn.com

| 인프런 - 백기선님의 코딩으로 학습하는 리팩토링 강의를 수강하며 정리한 글입니다.


뒤엉킨 변경
  • 소프트웨어는 변경에 유연하게 대처할 수 있어야 한다.
  • 서로 다른 문제는 다른 모듈에서 해결하는 것이 좋다.
    • 응집도를 높이고 결합도를 낮추자.

  • 모듈의 책임이 분리되어 있을 수록 해당 문맥을 더 잘 이해할 수 있으며 다른 문제는 신경쓰지 않아도 된다.
  • 관련 리팩토링 기술
    • 단계 쪼개기 -> 서로 다른 문맥의 코드를 분리할 수 있다.
    • 함수 옮기기 -> 적절한 모듈로 함수를 옮길 수 있다.
    • 함수 추출하기 -> 여러 가지 작업이 하나의 함수에 모여 있을 경우 적용할 수 있다.
    • 클래스 추출하기 -> 별도의 클래스로 분리할 수 있다.

Refactoring 1. 단계 쪼개기

  • 서로 다른 일을 하는 코드를 각기 다른 모듈로 분리한다. -> 그것과 관련있는 부분만 신경 쓸 수 있도록 하기 위해서
  • 여러 일을 하는 함수의 처리 과정을 각기 다른 단계로 구분할 수 있다.
    • ex) 전처리 -> 주요 작업 -> 후처리

  • 서로 다른 데이터를 사용한다면, 단계를 나누는데 있어 중요한 단서가 될 수 있다.
  • 중간 데이터를 사용하여 단계를 구분하고, 매개변수를 줄이는데 활용할 수 있다.
Before
public class PriceOrder {

    public double priceOrder(Product product, int quantity, ShippingMethod shippingMethod) {
        final double basePrice = product.basePrice() * quantity;
        final double discount = Math.max(quantity - product.discountThreshold(), 0)
                * product.basePrice() * product.discountRate();
        final double shippingPerCase = (basePrice > shippingMethod.discountThreshold()) ?
                shippingMethod.discountedFee() : shippingMethod.feePerCase();
        final double shippingCost = quantity * shippingPerCase;
        final double price = basePrice - discount + shippingCost;
        return price;
    }
}

public record Product(double basePrice, double discountThreshold, double discountRate) {
}

public record ShippingMethod(double discountThreshold, double discountedFee, double feePerCase) {
}
After
public class PriceOrder {

    public double priceOrder(Product product, int quantity, ShippingMethod shippingMethod) {
        final PriceData priceData = calculatePriceData(product, quantity);
        final double price = applyShipping(priceData, shippingMethod);
        return price;
    }

    private static PriceData calculatePriceData(Product product, int quantity) {
        final double basePrice = product.basePrice() * quantity;
        final double discount = Math.max(quantity - product.discountThreshold(), 0)
                * product.basePrice() * product.discountRate();

        final PriceData priceData = new PriceData(quantity, basePrice, discount);
        return priceData;
    }

    private static double applyShipping(PriceData priceData, ShippingMethod shippingMethod) {
        final double shippingPerCase = (priceData.basePrice() > shippingMethod.discountThreshold()) ?
                shippingMethod.discountedFee() : shippingMethod.feePerCase();
        final double shippingCost = priceData.quantity() * shippingPerCase;
        final double price = priceData.basePrice() - priceData.discount() + shippingCost;
        return price;
    }
}

public record PriceData(int quantity, double basePrice, double discount) {
}

public record Product(double basePrice, double discountThreshold, double discountRate) {
}

public record ShippingMethod(double discountThreshold, double discountedFee, double feePerCase) {
}

=> PriceData : 중간 데이터


Refactoring 2. 함수 옮기기

  • 모듈화가 잘 되어 있는 소프트웨어는 최소한의 지식으로 프로그래밍이 가능하다.
  • 관련있는 함수나 필드가 모여있어야 더 쉽게 찾고 활용할 수 있다.
  • 함수를 옮겨야 하는 경우
    • 해당 함수가 다른 문맥(클래스)에 있는 데이터를 더 많이 참조하는 경우
    • 해당 함수를 다른 클라이언트(클래스)에서도 필요한 경우

Before
public class Account {

    private int daysOverdrawn;

    private AccountType type;

    public Account(int daysOverdrawn, AccountType type) {
        this.daysOverdrawn = daysOverdrawn;
        this.type = type;
    }

    public double getBankCharge() {
        double result = 4.5;
        if (this.daysOverdrawn() > 0) {
            result += this.overdraftCharge();
        }
        return result;
    }

    private int daysOverdrawn() {
        return this.daysOverdrawn;
    }

    private double overdraftCharge() {
        if (this.type.isPremium()) {
            final int baseCharge = 10;
            if (this.daysOverdrawn <= 7) {
                return baseCharge;
            } else {
                return baseCharge + (this.daysOverdrawn - 7) * 0.85;
            }
        } else {
            return this.daysOverdrawn * 1.75;
        }
    }
}

public class AccountType {
    private boolean premium;

    public AccountType(boolean premium) {
        this.premium = premium;
    }

    public boolean isPremium() {
        return this.premium;
    }
}
After
public class Account {

    private int daysOverdrawn;

    private AccountType type;

    public Account(int daysOverdrawn, AccountType type) {
        this.daysOverdrawn = daysOverdrawn;
        this.type = type;
    }

    public double getBankCharge() {
        double result = 4.5;
        if (this.daysOverdrawn() > 0) {
            result += this.type.overdraftCharge(this.daysOverdrawn);
        }
        return result;
    }

    private int daysOverdrawn() {
        return this.daysOverdrawn;
    }

}

public class AccountType {
    private boolean premium;

    public AccountType(boolean premium) {
        this.premium = premium;
    }

    public boolean isPremium() {
        return this.premium;
    }

    double overdraftCharge(int daysOverdrawn) {
        if (isPremium()) {
            final int baseCharge = 10;
            if (daysOverdrawn <= 7) {
                return baseCharge;
            } else {
                return baseCharge + (daysOverdrawn - 7) * 0.85;
            }
        } else {
            return daysOverdrawn * 1.75;
        }
    }
}

=> type의 isPremium()을 사용하므로 overdraftChange메서드를 Account Class 에서 AccountType Class로 이동


Refactoring 3. 클래스 추출하기

  • 클래스가 다루는 책임이 많아질수록 클래스가 커진다.
  • 클래스를 쪼개는 기준
    • 데이터나 메서드의 일부가 매우 밀접한 경우
    • 일부의 데이터, 메서드가 같이 바뀌는 경우
    • 하위 클래스로 만들어 책임을 분산시킬 수 있는 경우

Before
public class Person {

    private String name;

    private String officeAreaCode;

    private String officeNumber;

    public String telephoneNumber() {
        return this.officeAreaCode + " " + this.officeNumber;
    }

    public String name() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String officeAreaCode() {
        return officeAreaCode;
    }

    public void setOfficeAreaCode(String officeAreaCode) {
        this.officeAreaCode = officeAreaCode;
    }

    public String officeNumber() {
        return officeNumber;
    }

    public void setOfficeNumber(String officeNumber) {
        this.officeNumber = officeNumber;
    }
}
After
public class Person {

    private TelephoneNumber telephoneNumber;
    private String name;

    public Person(TelephoneNumber telephoneNumber, String name) {
        this.telephoneNumber = telephoneNumber;
        this.name = name;
    }

    public String name() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public TelephoneNumber getTelephoneNumber() {
        return telephoneNumber;
    }

}

public class TelephoneNumber {
    private String areaCode;
    private String number;

    public TelephoneNumber(String areaCode, String officeNumber) {
        this.areaCode = areaCode;
        this.number = officeNumber;
    }

    public String areaCode() {
        return areaCode;
    }

    public String number() {
        return number;
    }
}

'클린코드' 카테고리의 다른 글

[Refactoring] 9. 기능 편애  (0) 2022.09.12
[Refactoring] 8. 산탄총 수술  (0) 2022.09.11
[Refactoring] 6. 가변 데이터  (0) 2022.09.09
[Refactoring] 5. 전역 데이터  (0) 2022.09.08
[Refactoring] 4. 긴 매개변수 목록  (0) 2022.09.07