概要
Java のコンビネーションパターンを試してみました
結果は、関数型(コンビネーションパターン)の方がコードは可読性が増すけど、メモリを多く消費するようです。速度はほとんど変わりがないようでした。
参考
やり方
手続き型
public ValidationResult imperativeValidate(Parson parson) {
var result = ImperativeValidator.isValidName(parson.getName());
if (result != ValidationResult.SUCCESS) {
return result;
}
result = ImperativeValidator.isValidAddr(parson.getAddr());
if (result != ValidationResult.SUCCESS) {
return result;
}
return ImperativeValidator.isValidAge(parson.getAge());
}
関数型
public ValidationResult functionalValidate(Parson p) {
return CombinatorPatternValidator.isValidName()
.and(isValidAddr())
.and(isValidAge())
.apply(p);
}
結果
JVM ヒープ
関数型が 124 KB に対して、 手続き型が 66 KB でした。
[QUICK PERF] Measured heap allocation (test method thread): 124.14 Kilo bytes (127 120 bytes)
[QUICK PERF] Measured heap allocation (test method thread): 66.49 Kilo bytes (68 088 bytes)
ベンチマーク
何回かやりましたが、あんまり変わらない、関数型の方がちょっと遅いという感じの結果になっています
(ここはバリデーションの内容によるのかもしれないです)
Benchmark Mode Cnt Score Error Units
CombinatorPatternBenchmarkTest.functional thrpt 5 859447.226 ± 5632.616 ops/s
CombinatorPatternBenchmarkTest.imperative thrpt 5 865311.952 ± 4481.358 ops/s
付録
コード全体
Data
@AllArgsConstructor
@Data
public class Parson {
private String name;
private int age;
private String addr;
private String addr2;
}
enum
public enum ValidationResult {
SUCCESS,
HOGE_CANNOT_BE_USED_IN_NAME,
ADDR_IS_TOO_LONG,
IS_A_CHILD,
}
手続き型バリデーター
public class ImperativeValidator {
public static ValidationResult isValidName(String name) {
return name.contains("hoge") ? HOGE_CANNOT_BE_USED_IN_NAME : SUCCESS;
}
public static ValidationResult isValidAddr(String addr) {
return addr.length() > 100 ? ADDR_IS_TOO_LONG : SUCCESS;
}
public static ValidationResult isValidAge(int age) {
return age <= 18 ? IS_A_CHILD : SUCCESS;
}
}
関数型(コンビネーターパターン)バリデーター
interface CombinatorPatternValidator extends Function<Parson, ValidationResult> {
static CombinatorPatternValidator isValidName() {
return p -> p.getName().contains("hoge") ? HOGE_CANNOT_BE_USED_IN_NAME : SUCCESS;
}
static CombinatorPatternValidator isValidAddr() {
return p -> p.getAddr().length() > 100 ? ADDR_IS_TOO_LONG : SUCCESS;
}
static CombinatorPatternValidator isValidAge() {
return p -> p.getAge() <= 18 ? IS_A_CHILD : SUCCESS;
}
default CombinatorPatternValidator and(CombinatorPatternValidator other) {
return p -> {
var result = this.apply(p);
return result.equals(SUCCESS) ? other.apply(p) : result;
};
}
}
バリデーションサービス
public class ValidationService {
public ValidationResult imperativeValidate(Parson parson) {
var result = ImperativeValidator.isValidName(parson.getName());
if (result != ValidationResult.SUCCESS) {
return result;
}
result = ImperativeValidator.isValidAddr(parson.getAddr());
if (result != ValidationResult.SUCCESS) {
return result;
}
return ImperativeValidator.isValidAge(parson.getAge());
}
public ValidationResult functionalValidate(Parson p) {
return CombinatorPatternValidator.isValidName()
.and(isValidAddr())
.and(isValidAge())
.apply(p);
}
}
テストコード
ヒープ測定
@QuickPerfTest
public class CombinatorPatternTest {
ValidationService service = new ValidationService();
@MeasureHeapAllocation
@Test
void imperative() {
service.imperativeValidate(create());
}
@MeasureHeapAllocation
@Test
void functional() {
service.functionalValidate(create());
}
Parson create() {
return new Parson(RandomStringUtils.randomAlphabetic(10), new Random().nextInt(100), RandomStringUtils.randomAlphabetic(20), RandomStringUtils.randomAlphabetic(20));
}
}
ベンチマーク
@State(value = Scope.Benchmark)
public class CombinatorPatternBenchmarkTest {
ValidationService service = new ValidationService();
@Benchmark
public void imperative() {
service.imperativeValidate(create());
}
@Benchmark
public void functional() {
service.functionalValidate(create());
}
@Test
void benchMark() throws RunnerException {
Options opt = new OptionsBuilder()
.include(CombinatorPatternBenchmarkTest.class.getSimpleName())
.forks(1) // 1回実行
.warmupIterations(1) // 1回繰り返し
.build();
new Runner(opt).run();
}
Parson create() {
return new Parson(RandomStringUtils.randomAlphabetic(10), new Random().nextInt(100), RandomStringUtils.randomAlphabetic(20), RandomStringUtils.randomAlphabetic(20));
}
}