Engineering
Glossvation Engineering
アルゴリズム

Prototype パターン — `clone()` と `copy constructor`、どっちを選ぶべきか

既存インスタンスを複製して「型 + 設定済み」を使い回す Prototype。Java の Cloneable の闇と、現代的な代替 (copy constructor / toBuilder / record withers)。

Glossvation ·

Prototype パターン概念図

どういう問題を解くパターンか

「設定済みのインスタンス」を型として登録しておき、必要なときに複製して使う パターン。

典型例:

  • 複雑な初期化を済ませたオブジェクトを、名前で引き当てて使い回す (ゲームのキャラクタテンプレート、ドキュメントのテンプレート)
  • 設定ファイルから 「ベース」 を読み込み、個別パラメータだけ差し替えて使う
  • データベースの 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();

落とし穴 (まとめ)

  1. Cloneable は新規コードで使わない — Copy Constructor か record wither に逃げる
  2. ディープコピーが必要なら明示的にコピー — shallow copy で状態共有事故
  3. 登録する原型を可変にしない — 原型自身が書き換えられたら全クローンに影響

まとめ: Prototype というパターン名は古いが、 「設定済みインスタンス + 複製」という発想は現代でも有効です。 ただし Java での実装は Cloneable でなく copy constructor / record wither を使うのが安全。 「原型を名前で登録→引き当てて複製」 の構造だけが本質です。

関連記事