12장
상속 다루기
12.1 메서드 올리기
- 반대 리팩터링 : 메서드 내리기
배경
- 여러 메서드들의 본문 코드가 똑같을 때 적용하기 가장 쉽다.
- 여러 클래스의 두 메서드를 각각 매개변수화 하면 궁극적으로 같은 메소드가 되기도 하는데, 이런 경우 각각의 함수를 매개변수화 한 다음 메서드를 상속 계층의 위로 올린다.
- 만약 해당 메서드의 본문에서 참조하는 필드들이 서브 클래스에만 있는 경우에는 해당 필드들을 먼저 슈퍼 클래스로 올린 후에 메서드를 올린다.
절차
- 똑같이 동작하는 메서드인지 면밀히 살핀다.
- 하는 일은 같지만 코드가 다른 경우 본문 코드가 똑같아질 때까지 리팩터링 한다.
- 메서드 안에서 호출하는 다른 메서드와 참조하는 필드들을 슈퍼클래스에서도 호출하고 참조할 수 있는지 확인한다.
- 메서드 시그니처가 다르다면 함수 선언 바꾸기로 슈퍼클래스에서 사용하고 싶은 형태로 통일한다.
- 슈퍼클래스에 새로운 메서드를 생성하고, 대상 메서드의 코드를 복사한다.
- 정적 검사를 수행한다.
- 서브클래스 중 하나의 메서드를 제거한다.
- 테스트한다.
- 모든 서브클래스의 메서드가 없어질 때까지 다른 서브클래스의 메서드를 하나씩 제거한다.
예시
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 필드 올리기
- 반대 리팩터링 : 필드 내리기
배경
- 필드들이 어떻게 이용되는지 분석하여 비슷한 방식으로 쓰인다 판단되면 슈퍼 클래스로 끌어 올린다.
- 데이터 중복 선언을 줄일 수 있다.
- 해당 필드를 사용하는 동작을 슈퍼클래스로 옮길 수 있다.
절차
- 후보 필드들을 사용하는 곳 모두가 그 필드들을 똑같은 방식으로 사용하는지 면밀히 살핀다.
- 필드들의 이름이 각기 다르다면 똑같은 이름으로 바꾼다.(필드 이름 바꾸기)
- 슈퍼 클래스에 새로운 필드를 생성한다.
- 서브클래스에서 이 필드에 접근할 수 있어야 한다.
- 서브클래스의 필드들을 제거한다.
- 테스트한다.
예시
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 생성자 본문 올리기
배경
- 메서드 올리기의 조금 특별한 케이스
- 다만 생성자는 할 수 있는 일과 호출 순서에 제약이 있으므로 조금 다른 식으로 접근해야 한다.
절차
- 슈퍼클래스에 생서자가 없다면 하나 정의한다. 서브클래스의 생성자들에서 이 생성자가 호출되는지 확인한다.
- 문장 슬라이드하기로 공통 문장 모두를 super() 호출 직후로 옮긴다.
- 공통 코드를 슈퍼클래스에 추가하고 서브클래스들에서는 제거한다. 생성자 매개변수 중 공통 코드에서 참조하는 값들을 모두 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.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 메서드 내리기
- 반대 리팩터링 : 메서드 올리기
배경
- 특정 서브클래스와만 관련된 메서드는 슈퍼클래스에서 제거하고 해당 서브클래스에 추가하는 편이 깔끔하다.
- 다만 이 리팩터링은 해당 기능을 제공하는 서브클래스가 정확히 무엇인지를 호출자가 알고 있을 때만 적용할 수 있다.
- 만약 그렇지 못한 상황이라면 서브클래스에 따라 다르게 동작하는 슈퍼클래스의 기만적인 조건부 로직을 다형성으로 바꿔야 한다.
절차
- 대상 메서드를 모든 서브클래스에 복사한다.
- 슈퍼클래스에서 그 메서드를 제거한다.
- 테스트한다.
- 이 메서드를 사용하지 않는 모든 서브클래스에서 제거한다.
- 테스트한다.
예시
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 필드 내리기
- 반대 리팩터링 : 필드 올리기
배경
- 서브클래스 하나에서만 사용하는 필드를 해당 서브클래스로 옮긴다.
절차
- 대상 필드를 모든 서브클래스에 정의한다.
- 슈퍼클래스에서 그 필드를 제거한다.
- 테스트한다.
- 이 필드를 사용하지 않는 모든 서브클래스에서 제거한다.
- 테스트한다.
예시
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속성 같은 서브클래스를 만드는 식이다.
절차
- 타입 코드 필드를 자가 캡슐화 한다.
- 타입 코드 값 하나를 선택하여 그 값에 해당하는 서브클래스를 만든다. 타입 코드 게터 메서드를 오버라읻드하여 해당 타입 코드의 리터럴 값을 반환하게 한다.
- 매개변수로 받은 타입 코드와 방금 만든 서브클래스를 매핑하는 선택 로직을 만든다.
- 직접 상속일 때는 생성자를 팩터리 함수로 바꾸기를 작용하고 선택 로직을 팩터리에 넣는다.
- 간접 상속일 때는 선택 로직을 생성자에 두면 될 것이다.
- 테스트한다.
- 타입 코드 값 각각에 대해 서브클래스 생성과 선택 로직 추가를 반복한다. 클래스 하나가 완성될 때마다 테스트한다.
- 타입 코드 필드를 제거한다.
- 테스트한다.
- 타입 코드 접근자를 이용하는 메서드 모두에 메서드 내리기와 조건부 로직을 다형성으로 바꾸기를 적용한다.
예시
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과 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;
}
}
테스트에 성공하면 서브클래스들을 제거한다.
'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 |