본문 바로가기
Study/Refactoring

리팩터링 12

by irerin07 2023. 8. 30.
728x90

12장

상속 다루기

12.1 메서드 올리기

  • 반대 리팩터링 : 메서드 내리기

배경

  • 여러 메서드들의 본문 코드가 똑같을 때 적용하기 가장 쉽다.
  • 여러 클래스의 두 메서드를 각각 매개변수화 하면 궁극적으로 같은 메소드가 되기도 하는데, 이런 경우 각각의 함수를 매개변수화 한 다음 메서드를 상속 계층의 위로 올린다.
  • 만약 해당 메서드의 본문에서 참조하는 필드들이 서브 클래스에만 있는 경우에는 해당 필드들을 먼저 슈퍼 클래스로 올린 후에 메서드를 올린다.

절차

  1. 똑같이 동작하는 메서드인지 면밀히 살핀다.
    1. 하는 일은 같지만 코드가 다른 경우 본문 코드가 똑같아질 때까지 리팩터링 한다.
  2. 메서드 안에서 호출하는 다른 메서드와 참조하는 필드들을 슈퍼클래스에서도 호출하고 참조할 수 있는지 확인한다.
  3. 메서드 시그니처가 다르다면 함수 선언 바꾸기로 슈퍼클래스에서 사용하고 싶은 형태로 통일한다.
  4. 슈퍼클래스에 새로운 메서드를 생성하고, 대상 메서드의 코드를 복사한다.
  5. 정적 검사를 수행한다.
  6. 서브클래스 중 하나의 메서드를 제거한다.
  7. 테스트한다.
  8. 모든 서브클래스의 메서드가 없어질 때까지 다른 서브클래스의 메서드를 하나씩 제거한다.

예시

1 - 두 클래스에서 같은 일을 수행하는 메서드를 찾는다.

public class Employee extends Party{

  private int monthlyCost;

  public Employee(int monthlyCost) {
    this.monthlyCost = monthlyCost;
  }

  public int getAnnualCost() {
    return this.monthlyCost * 12;
  }

}

public class Department extends Party{

  private int monthlyCost;

  public Department(int monthlyCost) {
    this.monthlyCost = monthlyCost;
  }

  public int getTotalAnnualCost() {
    return this.monthlyCost * 12;
  }

}

2- 두 메서드에서 참조하는 monthlyCost()속성은 슈퍼클래스에는 정의되어 있지 않지만 두 서브클래스 모두에 존재한다. 정적언어라면 슈퍼클래스인 Party에 추상 메서드를 정의해야한다.

3- 두 메서드의 이름이 다르므로 함수 선언 바꾸기로 이름을 통일한다.

public class Employee extends Party{

  private int monthlyCost;

  public Employee(int monthlyCost) {
    this.monthlyCost = monthlyCost;
  }

  public int getAnnualCost() {
    return this.monthlyCost * 12;
  }

}

public class Department extends Party{

  private int monthlyCost;

  public Department(int monthlyCost) {
    this.monthlyCost = monthlyCost;
  }

  public int getAnnualCost() {
    return this.monthlyCost * 12;
  }

}

4- 서브클래스 중 하나의 메서드를 복사해 슈퍼클래스에 붙여 넣는다. 정적언어라면 슈퍼클래스인 Party에 추상 메서드를 정의해야한다.

public abstract class Party {

  public abstract int getTotalAnnualCost();

}

정적 언어라면 이 시점에서 컴파일하여 모든 참조가 올바른지 확인한다.

12.2 필드 올리기

  • 반대 리팩터링 : 필드 내리기

배경

  • 필드들이 어떻게 이용되는지 분석하여 비슷한 방식으로 쓰인다 판단되면 슈퍼 클래스로 끌어 올린다.
    • 데이터 중복 선언을 줄일 수 있다.
    • 해당 필드를 사용하는 동작을 슈퍼클래스로 옮길 수 있다.

절차

  1. 후보 필드들을 사용하는 곳 모두가 그 필드들을 똑같은 방식으로 사용하는지 면밀히 살핀다.
  2. 필드들의 이름이 각기 다르다면 똑같은 이름으로 바꾼다.(필드 이름 바꾸기)
  3. 슈퍼 클래스에 새로운 필드를 생성한다.
    1. 서브클래스에서 이 필드에 접근할 수 있어야 한다.
  4. 서브클래스의 필드들을 제거한다.
  5. 테스트한다.

예시

public class Employee{

}
public class Engineer extends Employee{
  private String name;

  public Engineer(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

}
public class Manager extends Employee{
  private String fullName;

  public Manager(String fullName) {
    this.fullName = fullName;
  }

  public String getFullName() {
    return fullName;
  }
}
public class Manager extends Employee{
  **private String name;

  public Manager(String name) {
    this.name= name;
  }

  public String getName() {
    return name;
  }**
}
public class Employee{
  protected String name;

  public Employee(String name) {
    this.name = name;
  }

}
public class Engineer extends Employee{

  public Engineer(String name) {
    super(name);
  }

  public String getName() {
    return name;
  }

}
public class Manager extends Employee{

  public Manager(String name) {
    super(name);
  }

  public String getName() {
    return name;
  }

}

12.3 생성자 본문 올리기

배경

  • 메서드 올리기의 조금 특별한 케이스
  • 다만 생성자는 할 수 있는 일과 호출 순서에 제약이 있으므로 조금 다른 식으로 접근해야 한다.

절차

  1. 슈퍼클래스에 생서자가 없다면 하나 정의한다. 서브클래스의 생성자들에서 이 생성자가 호출되는지 확인한다.
  2. 문장 슬라이드하기로 공통 문장 모두를 super() 호출 직후로 옮긴다.
  3. 공통 코드를 슈퍼클래스에 추가하고 서브클래스들에서는 제거한다. 생성자 매개변수 중 공통 코드에서 참조하는 값들을 모두 super()로 건넨다.
  4. 테스트한다.
  5. 생성자 시작 부분으로 옮길 수 없는 공통 코드에는 함수 추출하기와 메서드 올리기를 차례로 적용한다.

예시

public class Party {

}

public class Employee extends Party{
  private String id;
  private String name;
  private int monthlyCost;

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

}

public class Department extends Party{
  private String name;
  private String staff;

  public Department(String name, String staff) {
    super();
    this.name = name;
    this.staff = staff;
  }

}
// 공통 코드 this.name = name; Employee에서 이 대입문을 슬라이드하여 super() 호출 바로 아래로 옮긴다.
public class Party {

}

public class Employee extends Party{
  private String id;
  private String name;
  private int monthlyCost;

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

}

public class Department extends Party{
  private String name;
  private String staff;

  public Department(String name, String staff) {
    super();
    this.name = name;
    this.staff = staff;
  }

}
// 테스트 성공시 이 공통 코드를 슈퍼클래스로 옮긴다. 참조하는 인수를 슈퍼클래스 생성자에 매개변수로 건넨다.
public class Party {
  private String name;

  public Party(String name) {
    this.name = name;
  }

}

public class Employee extends Party{
  private String id;
  private String name;
  private int monthlyCost;

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

}

public class Department extends Party{
  private String name;
  private String staff;

  public Department(String name, String staff) {
    super(name);
    this.staff = staff;
  }

}

공통 코드가 나중에 올 때

public class Employee extends Party{
  private String id;
  private String name;
  private int monthlyCost;

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

  public boolean isPrivileged() {
    return true;
  }

  public int assignCar() {
    return 1;
  }

}

public class Manager extends Employee{
  private int grade;

  public Manager(String name, String id, int monthlyCost, int grade) {
    super(name, id, monthlyCost);
    this.grade = grade;
    if (this.isPrivileged()) {
      this.assignCar();
    }
  }

  @Override
  public boolean isPrivileged() {
    return this.grade > 4;
  }

}

isPrivileged()는 grade 필드에 값이 대입된 후에야 호출될 수 있고, 서브클래스만이 이 필드에 값을 대입할 수 있다.

// 이런 경우라면 먼저 공통 코드를 함수로 추출한다.
public class Manager extends Employee{
  private int grade;

  public Manager(String name, String id, int monthlyCost, int grade) {
    super(name, id, monthlyCost);
    this.grade = grade;
    this.finishConstruction();
  }

  public void finishConstruction() {
    if (this.isPrivileged()) {
      this.assignCar();
    }
  }

  @Override
  public boolean isPrivileged() {
    return this.grade > 4;
  }

}
// 그런 다음 추출한 메서드를 슈퍼클래스로 옮긴다.
public class Employee extends Party{
  private String id;
  private String name;
  private int monthlyCost;

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

  public boolean isPrivileged() {
    return true;
  }

  public int assignCar() {
    return 1;
  }

  public void finishConstruction() {
    if (this.isPrivileged()) {
      this.assignCar();
    }
  }

}

12.4 메서드 내리기

  • 반대 리팩터링 : 메서드 올리기

배경

  • 특정 서브클래스와만 관련된 메서드는 슈퍼클래스에서 제거하고 해당 서브클래스에 추가하는 편이 깔끔하다.
  • 다만 이 리팩터링은 해당 기능을 제공하는 서브클래스가 정확히 무엇인지를 호출자가 알고 있을 때만 적용할 수 있다.
  • 만약 그렇지 못한 상황이라면 서브클래스에 따라 다르게 동작하는 슈퍼클래스의 기만적인 조건부 로직을 다형성으로 바꿔야 한다.

절차

  1. 대상 메서드를 모든 서브클래스에 복사한다.
  2. 슈퍼클래스에서 그 메서드를 제거한다.
  3. 테스트한다.
  4. 이 메서드를 사용하지 않는 모든 서브클래스에서 제거한다.
  5. 테스트한다.

예시

public class Animal {

  public void bark() {
    System.out.println("멍");
  }

}

public class Dog extends Animal{
}

public class Horse extends Animal{
}
// 대상 메서드를 모든 서브클래스에 복사한다.
public class Animal {

  public void bark() {
    System.out.println("멍");
  }

}

public class Dog extends Animal{

  **public void bark() {
    System.out.println("멍");
  }**

}

public class Horse extends Animal{

  **public void bark() {
    System.out.println("멍");
  }**

}
// 슈퍼 클래스에서 그 메서드를 제거한다.
public class Animal {

  ~~public void bark() {
    System.out.println("멍");
  }~~

}

public class Dog extends Animal{

  **public void bark() {
    System.out.println("멍");
  }**

}

public class Horse extends Animal{

  **public void bark() {
    System.out.println("멍");
  }**

}
// 이 메서드를 사용하지 않는 모든 서브클래스에서 제거한다.
public class Animal {


}

public class Dog extends Animal{

  **public void bark() {
    System.out.println("멍");
  }**

}

public class Horse extends Animal{

  **~~public void bark() {
    System.out.println("멍");
  }~~**

}

12.5 필드 내리기

  • 반대 리팩터링 : 필드 올리기

배경

  • 서브클래스 하나에서만 사용하는 필드를 해당 서브클래스로 옮긴다.

절차

  1. 대상 필드를 모든 서브클래스에 정의한다.
  2. 슈퍼클래스에서 그 필드를 제거한다.
  3. 테스트한다.
  4. 이 필드를 사용하지 않는 모든 서브클래스에서 제거한다.
  5. 테스트한다.

예시

public class Boat {

  private String fuel;

}

public class JetBoat extends Boat{
}

public class Raft extends Boat{
}
// 대상 필드를 모든 서브클래스에 정의한다.
public class Boat {

  private String fuel;

}

public class JetBoat extends Boat{

  **private String fuel;**

}

public class Raft extends Boat{

  **private String fuel;**

}
// 슈퍼클래스에서 그 필드를 제거한다.
public class Boat {

  ~~private String fuel;~~

}

public class JetBoat extends Boat{

  private String fuel;

}

public class Raft extends Boat{

  private String fuel;

}
// 이 필드를 사용하지 않는 모든 서브클래스에서 제거한다.
public class Boat {

}

public class JetBoat extends Boat{

  public String fuel;

}

public class Raft extends Boat{

    ~~private String fuel;~~

}

12.6 타입 코드를 서브클래스로 바꾸기

  • 반대 리팩터링 : 서브클래스 제거하기
  • 하위 리팩터링
    • 타입 코드를 상태/전략 패턴으로 바꾸기
    • 서브클래스 추출하기

배경

  • 서브클래스가 타입코드보다 매력적인 이유 중 하나는 조건에 따라 다르게 동작하도록 해주는 다형성을 제공해주기 때문이다.
    • 특히 타입 코드에 따라 동작이 달라져야 하는 함수가 여러 개일 떄 유용하다. 서브 클래스를 이용해 이런 함수들에 조건부 로직을 다형성으로 바꾸기를 적용할 수 있다.
  • 또 다른 매력은 특정 타입에서만 의미가 있는 값을 사용하는 필드가 메서드가 있을 때 발현된다.
    • 이런 상황이라면 서브 클래스를 만들고 필요한 서브클래스만 필드를 갖도록 정리한다(필드 내리기)
  • 해당 리팩터링은 대상 클래스에 직접 적용할지, 아니면 타입 코드 자체에 적용할지 고민해야 한다.
    • 대상 클래스에 직접 적용한다면 A라는 클래스와 A라는 클래스의 하위 타입인 B를 만든다.
    • 타입 코드 자체에 적용한다면 타입 코드에 어떠한 ‘속성’을 부여하고, 이 속성을 클래스로 정의해 A속성과 B속성 같은 서브클래스를 만드는 식이다.

절차

  1. 타입 코드 필드를 자가 캡슐화 한다.
  2. 타입 코드 값 하나를 선택하여 그 값에 해당하는 서브클래스를 만든다. 타입 코드 게터 메서드를 오버라읻드하여 해당 타입 코드의 리터럴 값을 반환하게 한다.
  3. 매개변수로 받은 타입 코드와 방금 만든 서브클래스를 매핑하는 선택 로직을 만든다.
    1. 직접 상속일 때는 생성자를 팩터리 함수로 바꾸기를 작용하고 선택 로직을 팩터리에 넣는다.
    2. 간접 상속일 때는 선택 로직을 생성자에 두면 될 것이다.
  4. 테스트한다.
  5. 타입 코드 값 각각에 대해 서브클래스 생성과 선택 로직 추가를 반복한다. 클래스 하나가 완성될 때마다 테스트한다.
  6. 타입 코드 필드를 제거한다.
  7. 테스트한다.
  8. 타입 코드 접근자를 이용하는 메서드 모두에 메서드 내리기와 조건부 로직을 다형성으로 바꾸기를 적용한다.

예시

public class Employee {

  private String name;
  private String type;

  public Employee(String name, String type) {
    this.name = name;
    this.type = type;
  }

  public void validateType(String type) {
    if (!Arrays.asList("engineer", "manager", "salesperrson").contains(type)) {
      throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }

  @Override
  public String toString() {
    return name + " (" + type + ")";
  }

}
// 1- 첫번쨰로, 타입 코드 변수를 자가 캡슐화 한다.
public class Employee {

  private String name;
  private String type;

  public Employee(String name, String type) {
    validateType(type);
    this.name = name;
    this.type = type;
  }

  public void validateType(String type) {
    if (!Arrays.asList("engineer", "manager", "salesperson").contains(type)) {
      throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }

  **public String getType() {
    return this.type;
  }**

  @Override
  public String toString() {
    return this.name + " (" + **this.getType()** + ")";
  }

}
// 2 - 직접 상속 방식으로 구현할 것이므로 직원 클래스 자체를 서브클래싱한다. 타입 코드 게터를 오버라이드하여 적절한 리터럴 값을 반환하기만 하면 되므로 아주 간단하게 처리 가능하다.
**public class Engineer extends Employee{

  public Engineer(String name, String type) {
    super(name, type);
  }

  @Override
  public String getType() {
    return "engineer";
  }

}**
// 3 - 생성자를 팩터리 함수로 바꿔 선택로직을 담을 별도 장소를 마련한다.
public class Employee {

  private String name;
  private String type;

  public Employee(String name, String type) {
    validateType(type);
    this.name = name;
    this.type = type;
  }

  **public Employee createEmployee(String name, String type) {
    return new Employee(name, type);
  }**

  public void validateType(String type) {
    if (!Arrays.asList("engineer", "manager", "salesperson").contains(type)) {
      throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }

  public String getType() {
    return this.type;
  }

  @Override
  public String toString() {
    return this.name + " (" + this.getType() + ")";
  }

}
// 새로 만든 서브클래스를 사용하기 위한 선택 로직을 팩터리에 추가한다.
public Employee createEmployee(String name, String type) {
    switch (type) {
      case "engineer" : return new Engineer(name, type);
    }

    return new Employee(name, type);
  }
// 5 - 남은 유형들에도 같은 작업을 반복하고 6 - 모든 유형에 적용했다면 타입 코드 필드와 슈퍼클래스의 게터(서브클래스에서 재정의한 메서드)를 제거한다.
public class Employee {

  private String name;
  private String type;

  public Employee(String name, String type) {
    validateType(type);
    this.name = name;
    this.type = type;
  }

  public Employee createEmployee(String name, String type) {
    switch (type) {
      case "engineer" : return new Engineer(name, type);
      case "manager" : return new Manager(name, type);
      case "salesperson" : return new SalesPerson(name, type);
    }

    return new Employee(name, type);
  }

  public void validateType(String type) {
    if (!Arrays.asList("engineer", "manager", "salesperson").contains(type)) {
      throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }

  public String getType() {
    return this.type;
  }

  @Override
  public String toString() {
    return this.name + " (" + this.getType() + ")";
  }

}
// 6 - 모든 유형에 적용했다면 타입 코드 필드와 슈퍼클래스의 게터를 제거한다.
**public abstract class Employee {**

  private String name;
  private String type;

  public Employee(String name, String type) {
    this.validateType(type);
    this.name = name;
        ~~this.type = type;~~
  }

  public static Employee createEmployee(String name, String type) {
    switch (type) {
      case "engineer" : return new Engineer(name, type);
      case "manager" : return new Manager(name, type);
      case "salesperson" : return new SalesPerson(name, type);
    }
            return new Employee(name, type);
  }

  public void validateType(String type) {
    if (!Arrays.asList("engineer", "manager", "salesperson").contains(type)) {
      throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }

  @Override
  public String toString() {
    return name + " (" + getType() + ")";
  }

  **public abstract String getType();**

}
// 7 - 검증 로직역시 제거한다. switch 문이 사실상 똑같은 검증을 수행해주기 때문
**public abstract class Employee {**

  private String name;
  private String type;

  public Employee(String name, String type) {
    ~~this.validateType(type);~~
    this.name = name;
  }

  public static Employee createEmployee(String name, String type) {
    switch (type) {
      case "engineer" : return new Engineer(name, type);
      case "manager" : return new Manager(name, type);
      case "salesperson" : return new SalesPerson(name, type);
      **default: throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");**
    }
            ~~return new Employee(name, type);~~
  }

  public void validateType(String type) {
    if (!Arrays.asList("engineer", "manager", "salesperson").contains(type)) {
      throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }

  @Override
  public String toString() {
    return name + " (" + getType() + ")";
  }

  **public abstract String getType();**

}
// 생성자에 건네는 타입 코드 인수는 쓰이지 않으니 없애버린다.
public abstract class Employee {

  private String name;
  private String type;

  public Employee(String name~~, String type~~) {
    this.name = name;
  }

  public static Employee createEmployee(String name, String type) {
    switch (type) {
      case "engineer" : return new Engineer(name~~, type~~);
      case "manager" : return new Manager(name~~, type~~);
      case "salesperson" : return new SalesPerson(name~~, type~~);
      default: throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }

  }

  public void validateType(String type) {
    if (!Arrays.asList("engineer", "manager", "salesperson").contains(type)) {
      throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }

  @Override
  public String toString() {
    return name + " (" + getType() + ")";
  }

  public abstract String getType();

}
  • 8 - 서브클래스들에는 타입 코드 게터가 여전히 남아있는데 이를 제거하고 싶지만 이 메서드를 이용하는 코드가 어딘가에 남아있을 수 있다. 그러니 조건부 로직을 다형성으로 바꾸기와 메서드 내리기로 문제를 해결하자.

예시 : 간접 상속할 때

public class Employee {

  private String name;
  private String type;

  public Employee(String name, String type) {
    this.validateType(type);
    this.name = name;
    this.type = type;
  }

  public void validateType(String type) {
    if (!Arrays.asList("engineer", "manager", "salesperson").contains(type)) {
      throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }

  public String getType() {
    return type;
  }

  public void setType(String type) {
    this.type = type;
  }

  public String capitalizedType() {
    return this.type.substring(0,1).toUpperCase() + this.type.substring(1).toLowerCase();
  }

  @Override
  public String toString() {
    return this.name + " (" + this.capitalizedType() + ")";
  }

}
// 1 - 타입 코드를 객체로 바꾸기(기본형을 객체로 바꾸기)
public class EmployeeType {

  private String value;

  public EmployeeType(String value) {
    this.value = value;
  }

  @Override
  public String toString() {
    return this.value;
  }
}

public class Employee {

  private String name;
  private EmployeeType type;

public Employee(String name, String type) {
    this.validateType(type);
    this.name = name;
    this.type = new EmployeeType(type);
  }

  public void validateType(String type) {
    if (!Arrays.asList("engineer", "manager", "salesperson").contains(type)) {
      throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }

  **public String getTypeString() {
    return this.type.toString();
  }**

  public String getType() {
    return type;
  }

    **public void setType(String type) {
    this.type = new EmployeeType(type);
  }**

  public String capitalizedType() {
    return this.**getTypeString()**.substring(0,1).toUpperCase() + this.**getTypeString()**.substring(1).toLowerCase();
  }

  @Override
  public String toString() {
    return this.name + " (" + this.capitalizedType() + ")";
  }

}
/// 앞 예시와 같은 방식으로 직원 유형을 리팩터링 한다.
public class Employee {

  private String name;
  private EmployeeType type;

  public Employee(String name, String type) {
    this.validateType(type);
    this.name = name;
    setType(type);
  }

  public void validateType(String type) {
    if (!Arrays.asList("engineer", "manager", "salesperson").contains(type)) {
      throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }

  public String getTypeString() {
    return type.toString();
  }

  public EmployeeType getType() {
    return type;
  }

  **public void setType(String type) {
    this.type = Employee.createEmployeeType(type);
  }

  public static EmployeeType createEmployeeType(String aString) {
    switch (aString) {
      case "engineer" :
        return new Engineer();
      case "manager" :
        return new Manager();
      case "salesperson" :
        return new SalesPerson();
      default: throw new IllegalArgumentException("직원 유형을 찾을 수 없습니다.");
    }
  }**

  public String capitalizedType() {
    return this.getTypeString().substring(0,1).toUpperCase() + this.getTypeString().substring(1).toLowerCase();
  }

  @Override
  public String toString() {
    return this.name + " (" + this.capitalizedType() + ")";
  }

}

public class EmployeeType {

}

public class Engineer extends EmployeeType {

  @Override
  public String toString() {
    return "engineer";
  }

}

public class Manager extends EmployeeType{

  @Override
  public String toString() {
    return "manager";
  }

}

public class SalesPerson extends EmployeeType {

  @Override
  public String toString() {
    return "salesman";
  }

}

12.7 서브 클래스 제거하기

  • 반대 리펙터링 : 타입 코드를 서브클래스로 바꾸기

배경

  • 더 이상 쓰이지 않는 서브클래스는 해당 서브클래스를 슈퍼클래스의 필드로 대체해 제거하는 게 최선이다.

절차

  1. 서브클래스의 생성자를 팩터리 함수로 바꾼다.
    1. 생성자를 사용하는 측에서 데이터 필드를 이용해 어떤 서브클래스를 생성할지 결정한다면 그 결정 로직을 슈퍼클래스의 팩터리 메서드에 넣는다.
  2. 서브 클래스의 타입을 검사하는 코드가 있다면 그 검사 코드에 함수 추출하기와 함수 옮기기를 차례로 적용하여 슈퍼클래스로 옮긴다. 하나 변경할 때마다 테스트한다.
  3. 서브클래스의 타입을 나타내는 필드를 슈퍼클래스에 만든다.
  4. 서브클래스를 참조하는 메서드가 방금 만든 타입 필드를 이용하도록 수정한다.
  5. 서브클래스를 지운다.
  6. 테스트한다.

이 리팩터링을 다수의 서브클래스에 한꺼번에 적용할 때가 많다.

그럴 때는 팩터리 함수를 추가하고 검사 코드를 옮기는 캡슐화 단계들(1과 2)을 먼저 실행한 다음 개별 서브클래스를 하나씩 슈퍼클래스로 흡수시킨다.

예시

public class Person {

  private String name;

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

  public String getName() {
    return name;
  }

  public String getGenderCode() {
    return "X";
  }

}

public class Male extends Person {
  public Male(String name) {
    super(name);
  }

  @Override
  public String getGenderCode() {
    return "M";
  }

}

public class Female extends Person {
  public Female(String name) {
    super(name);
  }

  @Override
  public String getGenderCode() {
    return "F";
  }

}

서브클래스를 삭제하기 전 혹시라도 해당 클래스들을 사용하는 클라이언트가 있는지 살핀다.

// 1 - 서브클래스를 캡슐화 하는 방법은 바로 생성자를 팩터리 함수로 바꾸기다.
public class Person {

  private String name;

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

  **public Person createPerson(String name) {
    return new Person(name);
  }**

  public String getName() {
    return name;
  }

  public String getGenderCode() {
    return "X";
  }

}

public class Male extends Person {
  public Male(String name) {
    super(name);
  }

  public Male createMale(String name) {
    return new Male(name);
  }

  @Override
  public String getGenderCode() {
    return "M";
  }

}

public class Female extends Person {
  public Female(String name) {
    super(name);
  }

  public Female createFemale(String name) {
    return new Female(name);
  }

  @Override
  public String getGenderCode() {
    return "F";
  }

}

직관적이긴 해도 이런 류의 객체는 성별 코드를 사용하는 곳에서 직접 생성될 가능성이 크다.

public class Person {

  private String name;

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

  public Person createPerson(String name) {
    return new Person(name);
  }

  **public List<Person> loadFromInput(List<Record> recordList) {
    List<Person> personList = new ArrayList<>();

    Person person;

    for (Record record : recordList) {
      switch (record.getGender()) {
        case "M" :
          person = new Male(record.getName());

          break;
        case "F" :
          person = new Female(record.getName());

          break;
        default :
          person = new Person(record.getName());
      }

      personList.add(person);
    }

    return personList;
  }**

  public String getName() {
    return name;
  }

  public String getGenderCode() {
    return "X";
  }

}

그렇다면 생성할 클래스를 선택하는 로직을 함수로 추출하고, 그 함수를 팩터리 함수로 삼는 편이 낫다.

public class Person {

  private String name;

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

  public Person createPerson(String name) {
    return new Person(name);
  }

  public List<Person> loadFromInput(List<Record> recordList) {
    List<Person> personList = new ArrayList<>();

    Person person;

    for (Record record : recordList) {
      **personList.add(createPerson(record));**
    }

    return personList;
  }

  **public Person createPerson(Record record) {
    Person person;

    switch (record.getGender()) {
      case "M" :
        person = new Male(record.getName());

        break;
      case "F" :
        person = new Female(record.getName());

        break;
      default :
        person = new Person(record.getName());
    }

    return person;
  }**

  public String getName() {
    return name;
  }

  public String getGenderCode() {
    return "X";
  }

}

이제 두 함수를 청소해보자. createPerson()에서 변수 person을 인라인한다.

public class Person {

  private String name;

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

  public Person createPerson(String name) {
    return new Person(name);
  }

  public List<Person> loadFromInput(List<Record> recordList) {
    List<Person> personList = new ArrayList<>();

    Person person;

    for (Record record : recordList) {
      personList.add(createPerson(record));
    }

    return personList;
  }

  public Person createPerson(Record record) {
    switch (record.getGender()) {
      case "M":
        **return new Male(record.getName());**

      case "F":
        **return new Female(record.getName());**

      default:
        **return new Person(record.getName());**
    }
  }

  public String getName() {
    return name;
  }

  public String getGenderCode() {
    return "X";
  }

}

그런 다음 loadFromInput()의 반복문을 파이프라인으로 바꾼다.

public class Person {

  private String name;

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

  public Person createPerson(String name) {
    return new Person(name);
  }

  public List<Person> loadFromInput(List<Record> recordList) {
    **return recordList.stream().map(e -> createPerson(e)).collect(Collectors.toList());**
  }

  public Person createPerson(Record record) {
    switch (record.getGender()) {
      case "M":
        return new Male(record.getName());

      case "F":
        return new Female(record.getName());

      default:
        return new Person(record.getName());
    }
  }

  public String getName() {
    return name;
  }

  public String getGenderCode() {
    return "X";
  }

}

이 팩터리가 서브클래스 생성을 캡슐화해주지만 코드의 다른 부분에선 instanceof를 사용하는 모습이 눈에 띈다. 이 타입 검사 코드를 함수로 추출한다.

public static void main(String[] args) {

    List<Record> recordList = new ArrayList<>();
    Record record = new Record("Andrew", "M");
    Record record1 = new Record("Angela", "F");
    Record record2 = new Record("Tim", "M");

    recordList.add(record);
    recordList.add(record1);
    recordList.add(record2);

    List<Person> people = Person.loadFromInput(recordList);

    long count = people.stream().filter(e -> isMale(e)).count();

    System.out.println("count = " + count);
  }

  private static boolean isMale(Person person) {
    return person instanceof Male;
  }
public class Person {

  private String name;

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

  public Person createPerson(String name) {
    return new Person(name);
  }

  public static List<Person> loadFromInput(List<Record> recordList) {
    return recordList.stream().map(e -> createPerson(e)).collect(Collectors.toList());
  }

  public static Person createPerson(Record record) {
    switch (record.getGender()) {
      case "M":
        return new Male(record.getName());

      case "F":
        return new Female(record.getName());

      default:
        return new Person(record.getName());
    }
  }

  **public boolean isMale(Person person) {
    return person instanceof Male;
  }**

  public String getName() {
    return name;
  }

  public String getGenderCode() {
    return "X";
  }

}
// 3 - 이제 서브클래스들의 차이를 나타낼 필드를 추가한다. 성별 정보는 Person 클래스 외부에서 정해 전달하는 방식이니 생성자에서 매개변수로 받아 설정하도록 작성한다.
public class Person {

  private String name;
  **private String genderCode;**

  public Person(String name, String genderCode) {
    this.name = name;
    **this.genderCode = (genderCode != null && !genderCode.isEmpty()) ? genderCode : "X";**
  }

  public static List<Person> loadFromInput(List<Record> recordList) {
    return recordList.stream().map(e -> createPerson(e)).collect(Collectors.toList());
  }

  public static Person createPerson(Record record) {
    switch (record.getGender()) {
      case "M":
        return new Male(record.getName());

      case "F":
        return new Female(record.getName());

      default:
        return new Person(record.getName());
    }
  }

  public boolean isMale(Person person) {
    return person instanceof Male;
  }

  public String getName() {
    return name;
  }

  public String getGenderCode() {
    **return genderCode;**
  }

}
// 4 - 남성인 경우의 로직을 슈퍼클래스로 옮긴다. 이를 위해 팩터리는 Person을 반환하도록 수정하고 instanceof를 사용해 검사하던 코드는 성별 코드 필드를 이용하도록 수정한다.
public class Person {

  private String name;
  private String genderCode;

  public Person(String name, String genderCode) {
    this.name = name;
    this.genderCode = (genderCode != null && !genderCode.isEmpty()) ? genderCode : "X";
  }

  public static List<Person> loadFromInput(List<Record> recordList) {
    return recordList.stream().map(e -> createPerson(e)).collect(Collectors.toList());
  }

  public static Person createPerson(Record record) {
    switch (record.getGender()) {
      case "M":
        return new Person(record.getName(), "M");

      case "F":
        return new Person(record.getName(), "F");

      default:
        return new Person(record.getName(), "X");
    }
  }

  public boolean isMale(Person person) {
    return "M".equals(genderCode);
  }

  public String getName() {
    return name;
  }

  public String getGenderCode() {
    return genderCode;
  }

}
public class Person {

  private String name;
  private String genderCode;

  public Person(String name, String genderCode) {
    this.name = name;
    this.genderCode = (genderCode != null && !genderCode.isEmpty()) ? genderCode : "X";
  }

  public static List<Person> loadFromInput(List<Record> recordList) {
    return recordList.stream().map(e -> createPerson(e)).collect(Collectors.toList());
  }

  public static Person createPerson(Record record) {
    switch (record.getGender()) {
      case "M":
        return new Person(record.getName(), "M");

      case "F":
        return new Person(record.getName(), "F");

      default:
        return new Person(record.getName(), "X");
    }
  }

  public boolean isMale(Person person) {
    return "M".equals(genderCode);
  }

  public String getName() {
    return name;
  }

  public String getGenderCode() {
    return genderCode;
  }

}

테스트에 성공하면 서브클래스들을 제거한다.

728x90

'Study > Refactoring' 카테고리의 다른 글

리팩터링 2  (0) 2023.08.31
리팩터링 1  (0) 2023.08.31
리팩터링 11  (0) 2023.08.29
리팩터링 챕터 10  (0) 2023.08.28
리팩터링 챕터 9  (0) 2023.08.27