[리팩토링] 악취 21 : 거대한 클래스

 

✍️ 악취 21 : 거대한 클래스

어떤 클래스가 하는 일이 너무 많아지면 필드도 많아지고 필드를 사용하는 메서드도 많아진다. 시간이 지날수록 내부 코드의 결합도는 증가하고 한 클래스에 너무 많은 일을 담당하니 코드를 이해하기도 수정하기도 어려워진다. 단일 책임 원칙을 고수하라는 이유도 바로 이런 문제점 때문이다.

 

여러 클라이언트가 공통적으로 호출하는 메서드가 있다면 '함수 추출하기'를 만약 여기에 항상 같이 사용되는 변수까지 있다면 '클래스 추출하기'로 리팩토링을 확장할 수 있다. 여러 클래스에 중복된 필드와 중복된 메서드가 있다면 상속을 사용한 '슈퍼 클래스 추출하기'를 적용할 수 있다.

 

여기 거대한 클래스를 해결하기 위한 두 가지 리팩토링 기법이 있다.

1. "함수 추출하기, 클래스 추출하기공통된 요소들이 존재한다면

2. "슈퍼 클래스 추출하기" 여러 클래스에 공통된 요소들이 존재한다면

 

본 포스팅에서 관심 있게 살펴볼 리팩토링은 '슈퍼 클래스 추출하기'이다.

 

🍊 슈퍼 클래스 추출하기

두 개 이상의 클래스에서 비슷한 요소(필드, 메서드)가 존재한다면 상속을 적용하고, 슈퍼 클래스로 '필드 올리기''메서드 올리기'를 적용할 수 있다. 

 

DepartmentEmployee 모두 'name' 필드와 '월간, 연간 비용' 계산 메서드라는 비슷한 요소가 존재한다. 

public class Department {

    private String name;

    private List<Employee> staff;

    public double totalMonthlyCost() {
        return staff.stream().mapToDouble(Employee::getMonthlyCost).sum();
    }

    public double totalAnnualCost() {
        return totalMonthlyCost() * 12;
    }
}

public class Employee {

    private Integer id;

    private String name;

    private double monthlyCost;

    public double annualCost() {
        return monthlyCost * 12;
    }
}

 

두 클래스의 슈퍼 클래스를 정의해서 중복되는 요소를 제거해 보자.
연간 비용을 구하는 로직은 두 클래스 동일하다. 다만 월간 비용을 구하는 로직이 서로 다르기에 그 구현을 서브 클래스에 위임했다. 일종의 팩토리 메서드 패턴이 적용된 케이스이다.

public abstract class Party {

    public String name;

    public abstract double getMonthlyCost();

    public double annualCost() {
        return getMonthlyCost() * 12;
    }
}

public class Department extends Party {

    private List<Employee> staff;

    public Department(String name, List<Employee> staff) {
        super(name);
        this.staff = staff;
    }

    @Override
    public double getMonthlyCost() {
        return staff.stream().mapToDouble(Employee::getMonthlyCost).sum();
    }
}

@Getter
public class Employee extends Party {

    private Integer id;

    private double monthlyCost;

    public Employee(String name, Integer id, double monthlyCost) {
        super(name);
        this.id = id;
        this.monthlyCost = monthlyCost;
    }
}