Java 実践パターン
実務で使える実践的なJavaパターンを学びます。
値オブジェクト(Value Object)
プリミティブ型や String をドメインの概念で包む。
なぜ必要か
// NG: プリミティブ型をそのまま使う
public void sendEmail(String email, String userId) { }
public void createUser(String userId, String email) { }
// 引数の順序を間違えても気づかない
sendEmail(userId, email); // コンパイルは通るがバグ
基本パターン
// OK: 値オブジェクトで包む
public record Email(String value) {
public Email {
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("Invalid email: " + value);
}
}
}
public record UserId(String value) {
public UserId {
Objects.requireNonNull(value, "userId must not be null");
}
}
// 型で制約、取り違え防止
public void sendEmail(Email email, UserId userId) { }
sendEmail(userId, email); // コンパイルエラー!
値オブジェクトの特徴
public record Money(BigDecimal amount, String currency) {
public Money {
Objects.requireNonNull(amount);
Objects.requireNonNull(currency);
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
}
// 振る舞いを持てる
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int quantity) {
return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
}
}
Result型
なぜ必要か
例外を使ったエラーハンドリングは、どこで何が起きるか分かりにくい。
// NG: 例外ベース
public User findUser(String id) throws UserNotFoundException {
// 呼び出し側は例外を忘れがち
}
// 使う側
User user = findUser(id); // 例外が飛ぶかもしれないが、コードからは分からない
Result型を使うと、成功/失敗が型で明示される。
基本実装
public sealed interface Result<T> {
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String code, String message) implements Result<T> {}
default boolean isSuccess() {
return this instanceof Success;
}
default boolean isFailure() {
return this instanceof Failure;
}
default T getOrThrow() {
return switch (this) {
case Success(var v) -> v;
case Failure(var code, var msg) ->
throw new RuntimeException(code + ": " + msg);
};
}
default T getOrElse(T defaultValue) {
return switch (this) {
case Success(var v) -> v;
case Failure f -> defaultValue;
};
}
default <U> Result<U> map(Function<T, U> mapper) {
return switch (this) {
case Success(var v) -> new Success<>(mapper.apply(v));
case Failure(var code, var msg) -> new Failure<>(code, msg);
};
}
default <U> Result<U> flatMap(Function<T, Result<U>> mapper) {
return switch (this) {
case Success(var v) -> mapper.apply(v);
case Failure(var code, var msg) -> new Failure<>(code, msg);
};
}
static <T> Result<T> success(T value) {
return new Success<>(value);
}
static <T> Result<T> failure(String code, String message) {
return new Failure<>(code, message);
}
}
使用例
// 戻り値の型で失敗の可能性が明示される
public Result<User> findUser(String id) {
User user = repository.findById(id);
if (user == null) {
return Result.failure("NOT_FOUND", "User not found: " + id);
}
return Result.success(user);
}
// 使用側は Result を処理する必要がある
Result<User> result = findUser("123");
// パターンマッチング
String message = switch (result) {
case Result.Success(var user) -> "Found: " + user.getName();
case Result.Failure(var code, var msg) -> "Error: " + msg;
};
// チェーン
String name = findUser("123")
.map(User::getName)
.map(String::toUpperCase)
.getOrElse("UNKNOWN");
Builder パターン
なぜ必要か
コンストラクタの引数が多いと、何が何だか分からなくなる。
// NG: 引数が多すぎる
HttpRequest request = new HttpRequest(
"POST", // method?
"https://api.example.com/users", // url?
"application/json", // contentType?
"Bearer token", // auth?
"{\"name\": \"Alice\"}", // body?
30 // timeout?
);
Builder パターンなら、何を設定しているか明確。
基本実装
public class HttpRequest {
private final String method;
private final String url;
private final Map<String, String> headers;
private final String body;
private final Duration timeout;
private HttpRequest(Builder builder) {
this.method = builder.method;
this.url = builder.url;
this.headers = Map.copyOf(builder.headers);
this.body = builder.body;
this.timeout = builder.timeout;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String method = "GET";
private String url;
private Map<String, String> headers = new HashMap<>();
private String body;
private Duration timeout = Duration.ofSeconds(30);
public Builder method(String method) {
this.method = method;
return this;
}
public Builder url(String url) {
this.url = url;
return this;
}
public Builder header(String name, String value) {
this.headers.put(name, value);
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder timeout(Duration timeout) {
this.timeout = timeout;
return this;
}
public HttpRequest build() {
Objects.requireNonNull(url, "url is required");
return new HttpRequest(this);
}
}
// getters...
}
// OK: 何を設定しているか明確
HttpRequest request = HttpRequest.builder()
.method("POST")
.url("https://api.example.com/users")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token")
.body("{\"name\": \"Alice\"}")
.timeout(Duration.ofSeconds(10))
.build();
Record と Builder
public record User(String id, String name, String email, int age) {
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String id;
private String name;
private String email;
private int age;
public Builder id(String id) { this.id = id; return this; }
public Builder name(String name) { this.name = name; return this; }
public Builder email(String email) { this.email = email; return this; }
public Builder age(int age) { this.age = age; return this; }
public User build() {
return new User(id, name, email, age);
}
}
}