#動機
記事ではUnitテストとコードの改善の改善の関連性が述べられています。
その一例として、ループ処理の抽象化のサンプルがあります。
このコードに着想を得て、ドメインロジックが分離に関するサンプルを書いて見ようと思いました。
(あと、そろそろROM専から脱しようかなと。。。)
##改善前
double bestValue = Double.MIN_VALUE;
Job bestJob = null;
for (Job job : jobs) {
if (score(job) > bestValue) {
bestJob = job;
}
}
return bestJob;
##改善後
public static <J> J argMax(Iterable<J> collection,
Function<J, Double> score) {
return Ordering.natural().onResultOf(score).max(collection);
}
この例のようにリファクタリングを重ねる事で処理(この例ではscore
ファンクション)と手続きが綺麗に分離されます。
すると、本来行いたい処理が見通しが良くなります。
(テスタビリティ、メンテナンシビリティの向上)
そこで、ドメインをメソッドとして抽出するサンプルするを書いてみたいと思います。
#サンプル
体重、身長、生活習慣で構成されるPersonから健康な人のみを抽出するとケースを考えて見ます。
ここで、健康な人の判定を業務ドメインとします。
まずは、Person
クラス
import java.util.List;
import lombok.Builder;
import lombok.Getter;
import lombok.Singular;
import lombok.Value;
@Builder
@Value
public class Person {
String name;
double height;
double weight;
@Singular("lifestyleHabit") List<String> lifestyleHabits;
// 本来は重い計算処理等で用いる実装です。これ位なら、直接calcBmiをgetBmiとして公開していいかと。
@Getter(lazy=true) double bmi = calcBmi();
private double calcBmi(){
double bmi = this.weight / Math.pow(this.height / 100, 2);
return bmi;
}
}
続いて健康な人の判定、抽出を行うHealthLogic
クラス
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class HealthLogic {
// 手続き
public List<Person> retrieveHealthyPerson(List<Person> persons) {
Predicate<Person> isHealthy = buildHealthPrediactor();
return persons.stream().filter(isHealthy).collect(Collectors.toList());
}
// 以下、ドメインロジック
public Predicate<Person> buildHealthPrediactor() {
return buildIdealBmiPrediactor().and(buildNoUnhealthyHabitPrediactor());
}
public Predicate<Person> buildIdealBmiPrediactor() {
final double MIN_BMI = 18.5;
final double MAX_BMI = 25.0;
return p -> MIN_BMI < p.getBmi() && MAX_BMI > p.getBmi();
}
public Predicate<Person> buildNoUnhealthyHabitPrediactor() {
List<String> badHabits = Arrays.asList("smoker");
return p -> !p.getLifestyleHabits().stream().anyMatch(badHabits::contains);
}
}
最後にテスト
import static org.testng.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.testng.annotations.Test;
import domain.sample.HealthLogic;
import domain.sample.Person;
public class HealthLogicTest {
@Test
public void retrieveHealthyPerson() {
Person p1 = Person.builder().name("fat").height(169).weight(73).build();
Person p2 = Person.builder().name("normal").height(169).weight(63).build();
Person p3 = Person.builder().name("smoker").height(169).weight(63).lifestyleHabit("smoker").build();
Person p4 = Person.builder().name("thin").height(169).weight(43).build();
List<Person> persons = new ArrayList<>();
persons.addAll(Arrays.asList(p1, p2, p3, p4));
HealthLogic testee = new HealthLogic();
List<Person> result = testee.retrieveHealthyPerson(persons);
assertEquals(result.size(), 1);
assertEquals(result.get(0), p2);
// System.out.println(result);
// [Person(name=normal, height=169.0, weight=63.0, lifestyleHabits=[],
// bmi=22.058051188683873)]
}
// 以下略...
@Test
public void buildHealthPrediactor() {
throw new RuntimeException("Test not implemented");
}
@Test
public void buildIdealBmiPrediactor() {
throw new RuntimeException("Test not implemented");
}
@Test
public void buildNoUnhealthyHabitPrediactor() {
throw new RuntimeException("Test not implemented");
}
}
サンプルは手抜きでHealthLogic
クラス内にドメインロジックと手続きをまとめて実装してしまっています。
実業務ではドメインロジックの肥大化に応じ、別クラスに出す等のリファクタリングを行っていき、見通しが悪くならないよう心掛けると良いと思います。