어플리케이션이 다루는 도메인에 필요한 기본 타입을 만들지 않고 언어가 제공하는 기본 타입을 사용하는 경우가 많음
기본형으로는 단위 또는 표기법을 표현하기 어려움
Order 클래스에서 사용하는 기본 타입의 변수를 객체로 바꾼다
Priority 클래스를 생성하고 역할에 맞는 데이터 필드, 필요한 기능들을 추가한다
Order 클래스에서 생성자 체인(Constructor Chaining)을 활용하여 문자열을 받는 생성자 호출시
Priority 객체를 포함하는 생성자 호출로 체이닝한다
class Order {
private String priority;
// ...생성자, getter
}
class OrderProcessor {
public long numberOfHighPriorityOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.getPriority() == "high" || o.getPriority() == "rush")
.count();
}
}
class Priority {
private String value;
private List<String> legarValues = List.of("low", "normal", "high", "rush");
public Priority(String value) {
if (legarValues.contains(value)) {
this.value = value;
} else {
throw new IllegalArgumentException("illegal value for priority");
}
}
@Override
public String toString() {
return this.value;
}
private int index() {
return this.legarValues.indexOf(this.value);
}
public boolean higherThan(Priority other) {
return this.index() > other.index();
}
}
class Order {
private Priority priority;
public Order(String priority) {
this(new Priority(priority));
}
public Order(Priority priority) {
this.priority = priority;
}
public Priority getPriority() {
return priority;
}
}
class OrderProcessor {
public long numberOfHighPriorityOrders(List<Order> orders) {
return orders.stream()
.filter(o -> o.getPriority().higherThan(new Priority("normal")))
.count();
}
}
첫 번째 케이스 해당 객체를 상속받은 서브클래스로 해결이 가능한 경우
Employee 에 있는 타입 문자열들을 각 각의 하위클래스로 생성하고 각 역할에 해당하는 타입을 리턴
class Employee {
private String name;
private String type;
public Employee(String name, String type) {
this.validate(type);
this.name = name;
this.type = type;
}
private void validate(String type) {
List<String> legalTypes = List.of("engineer", "manager", "salesman");
if (!legalTypes.contains(type)) {
throw new IllegalArgumentException(type);
}
}
// ...getType, toString
}
abstract class Employee {
private String name;
public Employee(String name) {
this.name = name;
}
public static Employee createEmployee(String name, String type) {
return switch (type) {
case "engineer" -> new Engineer(name);
case "manager" -> new Manager(name);
case "salesman" -> new Salesman(name);
default -> throw new IllegalArgumentException(type);
};
}
public abstract String getType();
// ...toString
}
class Engineer {
// ...생성자, getType => return "engineer"
}
class Manager {
// ...생성자, getType => return "manager"
}
class Salesman {
// ...생성자, getType => return "salesman"
}
두 번째 케이스 해당 객체를 상속받은 서브클래스가 이미 있는 경우
Type 부모클래스를 별도로 생성하여 해당 클래스를 상속받는 하위클래스들을 생성
해당 객체에서 Type 클래스에 더 적합한 역할의 함수들은 이동
class Employee {
private String name;
private String type;
public Employee(String name, String type) {
this.validate(type);
this.name = name;
this.type = type;
}
private void validate(String type) {
List<String> legalTypes = List.of("engineer", "manager", "salesman");
if (!legalTypes.contains(type)) {
throw new IllegalArgumentException(type);
}
}
public String capitalizedType() {
return this.type.substring(0, 1).toUpperCase() + this.type.substring(1).toLowerCase();
}
// ...toString
}
class EmployeeType {
public String capitalizedType() {
return this.toString().substring(0, 1).toUpperCase() + this.toString().substring(1).toLowerCase();
}
}
class Engineer {
// ...toString "engineer"
}
class Manager {
// ...toString "manager"
}
class Salesman {
// ...toString "salesman"
}
class Employee {
private String name;
private EmployeeType type;
public Employee(String name, String type) {
this.name = name;
this.type = this.employeeType(type);
}
private EmployeeType employeeType(String typeValue) {
return switch (typeValue) {
case "engineer" -> new Engineer();
case "manager" -> new Manager();
case "salesman" -> new Salesman();
default -> throw new IllegalArgumentException(typeValue);
};
}
public String capitalizedType() {
return this.type.capitalizedType();
}
// ...toString
}
Employee 클래스를 상위 클래스로 type 을 하위클래스로 생성한다
switch case 의 조건부 로직을 하위클래스의 다형성을 이용한 로직으로 변경한다
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 Employee {
private List<String> availableProjects;
public Employee(List<String> availableProjects) {
this.availableProjects = availableProjects;
}
public Employee() {};
protected abstract int vacationHours();
public boolean canAccessTo(String project) {
return this.availableProjects.contains(project);
}
}
class FullTimeEmployee {
public FullTimeEmployee(List<String> availableProjects) {
super(availableProjects);
}
@Override
public int vacationHours() {
return 120;
}
}
class PartTimeEmployee {
public PartTimeEmployee(List<String> availableProjects) {
super(availableProjects);
}
@Override
public int vacationHours() {
return 80;
}
}
class TemporalEmployee {
public TemporalEmployee(List<String> availableProjects) {
super(availableProjects);
}
@Override
public int vacationHours() {
return 32;
}
}
아래 코드를 보면 동일한 코드로 분기되어 있는 코드가 보인다
this.voyage.zone().equals("china") && this.hasChinaHistory()
china 를 기준으로 하위클래스를 생성하여 코드를 분리할 수 있으며 팩토리를
생성하여 분기에 따라 상위 or 하위 클래스를 생성한다
private 한 행위 중 상속에 필요한 행위들을 protected 로 변경하고 상속받아 재정의한다
class VoyageRating {
private Voyage voyage;
private List<VoyageHistory> history;
public VoyageRating(Voyage voyage, List<VoyageHistory> history) {
this.voyage = voyage;
this.history = history;
}
public char value() {
final int vpf = this.voyageProfitFactor();
final int vr = this.voyageRisk();
final int chr = this.captainHistoryRisk();
return (vpf * 3 > (vr + chr * 2)) ? 'A' : 'B';
}
private int captainHistoryRisk() {
int result = 1;
if (this.history.size() < 5) result += 4;
result += this.history.stream().filter(v -> v.profit() < 0).count();
if (this.voyage.zone().equals("china") && this.hasChinaHistory()) result -= 2;
return Math.max(result, 0);
}
private int voyageRisk() {
int result = 1;
if (this.voyage.length() > 4) result += 2;
if (this.voyage.length() > 8) result += this.voyage.length() - 8;
if (List.of("china", "east-indies").contains(this.voyage.zone())) result += 4;
return Math.max(result, 0);
}
private int voyageProfitFactor() {
int result = 2;
if (this.voyage.zone().equals("china")) result += 1;
if (this.voyage.zone().equals("east-indies")) result +=1 ;
if (this.voyage.zone().equals("china") && this.hasChinaHistory()) {
result += 3;
if (this.history.size() > 10) result += 1;
if (this.voyage.length() > 12) result += 1;
if (this.voyage.length() > 18) result -= 1;
} else {
if (this.history.size() > 8) result +=1 ;
if (this.voyage.length() > 14) result -= 1;
}
return result;
}
private boolean hasChinaHistory() {
return this.history.stream().anyMatch(v -> v.zone().equals("china"));
}
}
record Voyage(String zone, int length) {}
record VoyageHistory(String zone, int profit) {}
class VoyageRating {
protected Voyage voyage;
protected List<VoyageHistory> history;
public VoyageRating(Voyage voyage, List<VoyageHistory> history) {
this.voyage = voyage;
this.history = history;
}
public char value() {
final int vpf = this.voyageProfitFactor();
final int vr = this.voyageRisk();
final int chr = this.captainHistoryRisk();
return (vpf * 3 > (vr + chr * 2)) ? 'A' : 'B';
}
protected int captainHistoryRisk() {
int result = 1;
if (this.history.size() < 5) result += 4;
result += this.history.stream().filter(v -> v.profit() < 0).count();
return Math.max(result, 0);
}
private int voyageRisk() {
int result = 1;
if (this.voyage.length() > 4) result += 2;
if (this.voyage.length() > 8) result += this.voyage.length() - 8;
if (List.of("china", "east-indies").contains(this.voyage.zone())) result += 4;
return Math.max(result, 0);
}
protected int voyageProfitFactor() {
int result = 2;
if (this.voyage.zone().equals("china")) result += 1;
if (this.voyage.zone().equals("east-indies")) result +=1 ;
result += historyLengthFactor();
result += voyageLengthFactor();
return result;
}
protected int voyageLengthFactor() {
return (this.voyage.length() > 14) ? -1 : 0;
}
protected int historyLengthFactor() {
return (this.history.size() > 8) ? 1 : 0;
}
}
class RatingFactory {
public static VoyageRating createVoyageRating(Voyage voyage, List<VoyageHistory> history) {
if (voyage.zone().equals("china") && hasChinaHistory(history)) {
return new ChinaExperienceVoyageRating(voyage, history);
} else {
return new VoyageRating(voyage, history);
}
}
private static boolean hasChinaHistory(List<VoyageHistory> history) {
return history.stream().anyMatch(v -> v.zone().equals("china"));
}
}
class ChinaExperienceVoyageRating {
public ChinaExperienceVoyageRating(Voyage voyage, List<VoyageHistory> history) {
super(voyage, history);
}
@Override
protected int captainHistoryRisk() {
int result = super.captainHistoryRisk() - 2;
return Math.max(result, 0);
}
@Override
protected int voyageProfitFactor() {
return super.voyageProfitFactor() + 3;
}
@Override
protected int voyageLengthFactor() {
int result = 0;
result += historyLengthFactor();
if (this.voyage.length() > 12) result += 1;
if (this.voyage.length() > 18) result -= 1;
return result;
}
@Override
protected int historyLengthFactor() {
return (this.history.size() > 10) ? 1 : 0;
}
}