DEV ℧ Developer Diary

[Refactoring] 냄새 11. 기본형 집착 (2)

해당 포스트는 inflearn의 백기선님의 강의인 리팩토링 을 듣고 정리한 글입니다.

냄새 11. 기본형 집착 (2)

리팩토링 32. 조건부 로직을 다형성으로 바꾸기

  • 복잡한 조건식을 상속과 다형성을 사용해 코드를 보다 명확하게 분리할 수 있다.
  • swich 문을 사용해서 타입에 따라 각기 다른 로직을 사용하는 코드.
  • 기본 동작과 (타입에 따른) 특수한 기능이 섞여있는 경우에 상속 구조를 만들어서 기본 동작을 상위 클래스에 두고 특수한 기능을 하위클래스로 옮겨서 각 타입에 따른 “차이점”을 강조할 수 있다.
  • 모든 조건문을 다형성으로 옮겨야 하는가? 단순한 조건문은 그대로 두어도 좋다. 오직 복잡한 조건문을 다형성을 활용해 좀 더 나은 코드로 만들 수 있는 경우에만 적용한다. (과용을 조심하자.)

아래의 ‘Employee’ 클래스는 각 직원의 타입별, 휴게 시간과 접근 권한을 가져오는 로직이다.

  • Employee
public class Employee {

    private String type;

    private 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;
        };
    }
}

먼저 테스트 코드를 실행해보자. 각 직원 구분에 따른 권한 및 시간이 옳게 반환되는지 검증한다.

class EmployeeTest {

    @Test
    void fulltime() {
        Employee employee = new Employee("full-time", List.of("spring", "jpa"));
        assertEquals(120, employee.vacationHours());
        assertTrue(employee.canAccessTo("new project"));
        assertTrue(employee.canAccessTo("spring"));
    }

    @Test
    void partime() {
        Employee employee = new Employee("part-time", List.of("spring", "jpa"));
        assertEquals(80, employee.vacationHours());
        assertFalse(employee.canAccessTo("new project"));
        assertTrue(employee.canAccessTo("spring"));
    }

    @Test
    void temporal() {
        Employee employee = new Employee("temporal", List.of("jpa"));
        assertEquals(32, employee.vacationHours());
        assertFalse(employee.canAccessTo("new project"));
        assertFalse(employee.canAccessTo("spring"));
        assertTrue(employee.canAccessTo("jpa"));
    }
}

성공하는 것을 확인 할 수 있다.

리팩토링1

이제 switch로 작동하는 로직을 각각 다형성을 이용해 클래스로 분리하고 Employee를 상속받아 보도록 하자

먼저 접근제어자를 protected 로 바꾸어 외부에서 호출할 수 없도록 한다. 이후 생성자를 통해 필요한 데이터를 부모객체에서 가져올수 있도록 추가해주자.

protected String type;

protected List<String> availableProjects;

public Employee(String type, List<String> availableProjects) {
    this.type = type;
    this.availableProjects = availableProjects;
}

public Employee(List<String> availableProjects) {
    this.availableProjects = availableProjects;
}

public Employee() {
}

그리고 switch에 해당하는 FULL-TIME, PART-TIME, TEMPORAL의 타입을 각각 객체로 만들어 준다.

먼저 Employee를 상속받고, vacationHours, canAccessTo 두 메소드를 @Override 하여 switch의 조건과 동일하게 반환하도록 구현해준다.

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;
    }
}

public class TemporalEmployee extends Employee{
    public TemporalEmployee(List<String> availableProjects) {
        super(availableProjects);
    }

    @Override
    public int vacationHours() {
        return 32;
    }
}

이후 ‘Employee’ 에서는 조건을 지워주고, 기본값을 반환하도록 수정한다.

vacationHours 메소드는 추상화를 해주어 자식객체에서 구현해주도록 변환한다.

public abstract class Employee {

  protected String type;

  protected List<String> availableProjects;

  public Employee(String type, List<String> availableProjects) {
    this.type = type;
    this.availableProjects = availableProjects;
  }

  public Employee(List<String> availableProjects) {
    this.availableProjects = availableProjects;
  }

  public Employee() {
  }

  public abstract int vacationHours();

  public boolean canAccessTo(String project) {
      return availableProjects.contains(project);
  }
}

Employee 에서만 데이터를 가져오는 테스트 코드도 수정해주도록 하자.

그렇게 된다면 호출부에서는 생성자로 타입을 넘겨줄 필요없이 알아서, 데이터를 검증해 반환해준다.

class EmployeeTest {

    @Test
    void fulltime() {
        Employee employee = new FullTimeEmployee();
        assertEquals(120, employee.vacationHours());
        assertTrue(employee.canAccessTo("new project"));
        assertTrue(employee.canAccessTo("spring"));
    }

    @Test
    void partime() {
        Employee employee = new PartTimeEmployee(List.of("spring", "jpa"));
        assertEquals(80, employee.vacationHours());
        assertFalse(employee.canAccessTo("new project"));
        assertTrue(employee.canAccessTo("spring"));
    }

    @Test
    void temporal() {
        Employee employee = new TemporalEmployee(List.of("jpa"));
        assertEquals(32, employee.vacationHours());
        assertFalse(employee.canAccessTo("new project"));
        assertFalse(employee.canAccessTo("spring"));
        assertTrue(employee.canAccessTo("jpa"));
    }
}

테스트 코드 또한 정상작동하는 것을 확인 할 수있다.

리팩토링2