Prototype パターン — `clone()` と `copy constructor`、どっちを選ぶべきか
既存インスタンスを複製して「型 + 設定済み」を使い回す Prototype。Java の Cloneable の闇と、現代的な代替 (copy constructor / toBuilder / record withers)。

どういう問題を解くパターンか
「設定済みのインスタンス」を型として登録しておき、必要なときに複製して使う パターン。
典型例:
- 複雑な初期化を済ませたオブジェクトを、名前で引き当てて使い回す (ゲームのキャラクタテンプレート、ドキュメントのテンプレート)
- 設定ファイルから 「ベース」 を読み込み、個別パラメータだけ差し替えて使う
- データベースの
SELECT結果を 「原型」 として、画面側で部分編集して POST
GoF 定義では 「具体クラスではなく既存インスタンスから生成する」 ですが、 実務的には 「不変の原型 + 可変の差分」 という構造を作るための道具と考えると使いどころが分かります。
いつ使う / 使わない
| 使うべき | 使わないほうがよい |
|---|---|
| 複製元の構築コストが高い (DB アクセス, 計算) | 単純な new で十分 |
| 多数の 「型」 を名前で管理したい | 型が 1〜2 種類 |
| 不変のベース + 可変の差分で表現したい | 全フィールドが毎回変わる |
実装: Cloneable を使った古典的な形
Java の Object#clone() と java.lang.Cloneable マーカーインタフェースを使う方式です。
Prototype インタフェース
public interface Prototype extends Cloneable {
void greet(String message);
Prototype createClone();
}
具象クラス
public class ConcretePrototype implements Prototype {
private final char symbol;
public ConcretePrototype(char symbol) {
this.symbol = symbol;
}
@Override
public void greet(String message) {
int length = message.getBytes(StandardCharsets.UTF_8).length;
String bar = String.valueOf(symbol).repeat(length);
System.out.println(bar);
System.out.println(" [" + message + "] ");
System.out.println(bar);
}
@Override
public Prototype createClone() {
try {
return (Prototype) super.clone();
} catch (CloneNotSupportedException e) {
// Cloneable を実装しているので起きない
throw new AssertionError(e);
}
}
}
原型管理クラス
public final class PrototypeRegistry {
private final Map<String, Prototype> prototypes = new HashMap<>();
public void register(String key, Prototype proto) {
prototypes.put(key, proto);
}
public Prototype create(String key) {
Prototype p = prototypes.get(key);
if (p == null) throw new NoSuchElementException(key);
return p.createClone();
}
}
呼び出し側
public class Main {
public static void main(String[] args) {
PrototypeRegistry registry = new PrototypeRegistry();
registry.register("star", new ConcretePrototype('*'));
registry.register("sharp", new ConcretePrototype('#'));
registry.create("star").greet("hello");
registry.create("sharp").greet("world");
}
}
Cloneable の 3 つの罠
Joshua Bloch (Effective Java) が長年指摘しているとおり、Cloneable は設計ミスの産物です。
1. shallow copy しかしない
デフォルトの super.clone() は参照フィールドをそのままコピーします。
中に List や可変オブジェクトがあると、クローン間で状態が共有されてバグります。
対策: clone() 内で参照フィールドも明示的に複製する。
@Override
public ConcretePrototype createClone() {
try {
ConcretePrototype c = (ConcretePrototype) super.clone();
c.items = new ArrayList<>(this.items); // ← 手動でディープコピー
return c;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
2. final フィールドと相性が悪い
clone() はコンストラクタを呼ばないので、
final フィールドをあとから書き換えるのが面倒 (リフレクションが要る)。
不変オブジェクトとは根本的に相性が悪い。
3. 継承で壊れる
スーパークラスで clone() を実装すると、サブクラスで型が合わなくなる、
CloneNotSupportedException の扱いに一貫性が持てない等、落とし穴が多数。
現代的な代替案
A. Copy Constructor / Static Factory Method (推奨)
public final class ConcretePrototype {
private final char symbol;
private final List<String> items;
public ConcretePrototype(char symbol, List<String> items) {
this.symbol = symbol;
this.items = List.copyOf(items);
}
/** コピーコンストラクタ */
public ConcretePrototype(ConcretePrototype src) {
this(src.symbol, src.items);
}
/** または static factory method */
public static ConcretePrototype copyOf(ConcretePrototype src) {
return new ConcretePrototype(src);
}
}
メリット: final と両立。コピー処理が通常のコンストラクタに集約される。
B. record + 「wither」 形式
Java 16+ の record では、特定フィールドだけ差し替えた新しいインスタンスを返す
「wither」 メソッドを書くのが定番です。
public record UserProfile(String name, String city, int age) {
public UserProfile withCity(String city) {
return new UserProfile(this.name, city, this.age);
}
public UserProfile withAge(int age) {
return new UserProfile(this.name, this.city, age);
}
}
var alice = new UserProfile("Alice", "Tokyo", 30);
var moved = alice.withCity("Osaka"); // Prototype 的に差分だけ変える
C. Builder の toBuilder()
Builder パターン で紹介した toBuilder() も Prototype 的な使い方です。
User modified = original.toBuilder().city("osaka").build();
落とし穴 (まとめ)
Cloneableは新規コードで使わない — Copy Constructor か record wither に逃げる- ディープコピーが必要なら明示的にコピー — shallow copy で状態共有事故
- 登録する原型を可変にしない — 原型自身が書き換えられたら全クローンに影響
まとめ: Prototype というパターン名は古いが、
「設定済みインスタンス + 複製」という発想は現代でも有効です。
ただし Java での実装は Cloneable でなく copy constructor / record wither を使うのが安全。
「原型を名前で登録→引き当てて複製」 の構造だけが本質です。
関連記事
- アルゴリズム·
TemplateMethod パターン — Spring JdbcTemplate に見る典型実装
処理の骨格は親、差分は子に書き分ける Template Method。Spring の `*Template` 系の設計思想と、Strategy / Decorator との使い分け。
- アルゴリズム·
FactoryMethod パターン — Spring BeanFactory の背骨を自作する
インスタンス生成の種類を抽象側に閉じ込める Factory Method パターン。JDK/Spring の実例、Simple Factory との違い、現代 Java での書き方まで。
- アルゴリズム·
Adapter パターン — 外部ライブラリをドメインに染めない防波堤
既存の API を自分たちのドメインに合わせて包むアダプタ。クラスアダプタ vs オブジェクトアダプタ、SLF4J や Spring の HandlerAdapter などの実例まで。