Prototype Pattern #

Bayangkan sebuah sistem game yang mengelola ratusan NPC (Non-Player Character). Setiap NPC Prajurit memiliki stats dasar yang sama — health 100, attack 15, armor 10 — ditambah equipment yang sudah di-load dari database, animasi yang sudah di-parse dari file, dan AI behavior tree yang sudah di-compile. Membuat setiap prajurit baru dari nol berarti mengulangi semua proses mahal itu untuk setiap instance. Pendekatan yang jauh lebih efisien: buat satu prajurit “template”, lalu setiap prajurit baru cukup meng-clone template tersebut dan memodifikasi nilai yang perlu berbeda — nama, posisi, atau item yang dibawa. Inilah inti dari Prototype Pattern: membuat objek baru dengan menduplikasi objek yang sudah ada, bukan membangunnya dari nol setiap kali.

Apa itu Prototype Pattern? #

Prototype Pattern adalah creational design pattern yang memungkinkan pembuatan objek baru dengan cara mengkloning objek yang sudah ada (disebut prototype), bukan dengan memanggil constructor dan mengisi semua field dari nol. Objek yang di-clone berfungsi sebagai cetakan — ia menyimpan state awal yang sudah siap, dan setiap clone bisa memodifikasi bagian tertentu tanpa mempengaruhi prototype aslinya.

Di Golang, tidak ada mekanisme clone bawaan seperti di Java atau C++. Prototype Pattern diimplementasikan secara eksplisit melalui interface dengan method Clone(), dan setiap struct bertanggung jawab untuk mengimplementasikan clone-nya sendiri dengan benar — termasuk menangani field-field yang merupakan reference type seperti map, slice, dan pointer.

Dua properti yang mendefinisikan Prototype Pattern:

  • Clone menghasilkan objek independen — modifikasi pada clone tidak boleh mempengaruhi prototype, dan sebaliknya
  • Client tidak perlu tahu concrete type — cukup tahu bahwa objek bisa di-clone melalui interface
flowchart LR
    subgraph "Tanpa Prototype"
        direction TB
        C1[Buat NPC 1\ndari nol] --> D1["Load DB\nParse animasi\nCompile AI\n⏱ ~200ms"]
        C2[Buat NPC 2\ndari nol] --> D2["Load DB\nParse animasi\nCompile AI\n⏱ ~200ms"]
        C3[Buat NPC 3\ndari nol] --> D3["Load DB\nParse animasi\nCompile AI\n⏱ ~200ms"]
    end

    subgraph "Dengan Prototype"
        direction TB
        T[Template NPC] --> D4["Load DB\nParse animasi\nCompile AI\n⏱ ~200ms sekali"]
        D4 --> P[Prototype siap]
        P -->|Clone + modif| N1["NPC 1 ⏱ ~1ms"]
        P -->|Clone + modif| N2["NPC 2 ⏱ ~1ms"]
        P -->|Clone + modif| N3["NPC 3 ⏱ ~1ms"]
    end

Kapan Prototype Dibutuhkan #

Prototype bukan pattern untuk semua situasi. Ada tiga kondisi konkret yang membuat Prototype menjadi pilihan tepat.

Kondisi 1: Object Creation yang Mahal #

Jika pembuatan objek melibatkan operasi yang memakan waktu atau resource — query database, pembacaan file, network call, parsing XML/JSON besar, atau komputasi awal yang intensif — maka clone jauh lebih efisien daripada membuat ulang dari nol.

// ANTI-PATTERN: setiap request membuat ReportTemplate baru dari nol
func GenerateReport(reportType string, data []Row) (*Report, error) {
    template := &ReportTemplate{}
    template.LoadStylesheet("corporate.css")        // baca file
    template.LoadLogoAsset("logo.png")              // baca file
    template.ParseLayout("quarterly_layout.xml")    // parse XML
    template.InitChartEngine()                      // inisialisasi engine
    // total: ~150ms untuk setiap laporan

    return template.Render(data)
}

// BENAR: template disiapkan sekali, setiap laporan clone-nya
var reportPrototype *ReportTemplate

func init() {
    reportPrototype = &ReportTemplate{}
    reportPrototype.LoadStylesheet("corporate.css")
    reportPrototype.LoadLogoAsset("logo.png")
    reportPrototype.ParseLayout("quarterly_layout.xml")
    reportPrototype.InitChartEngine()
    // total: ~150ms sekali saat startup
}

func GenerateReport(reportType string, data []Row) (*Report, error) {
    template := reportPrototype.Clone() // ~1ms — hanya copy field
    return template.Render(data)
}

Kondisi 2: Banyak Objek dengan Struktur Mirip #

Ketika banyak objek berbagi state dasar yang sama dan hanya berbeda di beberapa field, Prototype lebih bersih daripada membuat banyak subclass atau memanggil constructor dengan banyak parameter berulang.

Kondisi 3: Object Creation di Runtime #

Ketika tipe objek yang harus dibuat ditentukan saat runtime — misalnya berdasarkan konfigurasi pengguna atau data yang masuk — Prototype Registry memungkinkan pembuatan objek tanpa hardcode tipe konkret di kode client.


Shallow Copy vs Deep Copy #

Ini adalah aspek paling krusial dalam Prototype Pattern. Kesalahan dalam implementasi clone adalah sumber bug yang sangat sulit dideteksi karena objek tampak berbeda padahal masih berbagi data yang sama di memori.

Shallow Copy: Berbagi Reference #

Shallow copy menyalin nilai field secara langsung. Untuk field bertipe primitif (int, string, bool, float64), ini aman karena nilainya ter-copy. Tapi untuk field bertipe reference (map, slice, pointer, interface), yang ter-copy hanyalah alamat memori — bukan datanya.

type Config struct {
    Name    string
    Timeout int
    Headers map[string]string // reference type — BERBAHAYA untuk shallow copy
    Tags    []string          // reference type — BERBAHAYA untuk shallow copy
}

// ANTI-PATTERN: shallow copy yang keliru disebut "clone"
func (c *Config) ShallowClone() *Config {
    copy := *c // salin semua field secara langsung
    return &copy
    // copy.Headers dan c.Headers menunjuk ke MAP YANG SAMA di memori!
    // copy.Tags dan c.Tags menunjuk ke SLICE YANG SAMA di memori!
}

func demonstrateShallowCopyDanger() {
    original := &Config{
        Name:    "payment-service",
        Timeout: 30,
        Headers: map[string]string{"Auth": "Bearer token-original"},
        Tags:    []string{"production"},
    }

    clone := original.ShallowClone()
    clone.Name = "refund-service"         // AMAN — string adalah value type
    clone.Timeout = 60                    // AMAN — int adalah value type
    clone.Headers["Auth"] = "Bearer token-clone" // BERBAHAYA — memodifikasi original juga!
    clone.Tags = append(clone.Tags, "staging")   // BERBAHAYA — tergantung kapasitas slice

    fmt.Println(original.Headers["Auth"]) // "Bearer token-clone" — bukan "Bearer token-original"!
    // Bug yang sangat sulit di-trace
}

Deep Copy: Setiap Objek Independen #

Deep copy membuat salinan baru untuk setiap reference type. Clone yang dihasilkan benar-benar independen — tidak ada shared state antara clone dan prototype.

// BENAR: deep copy yang aman
func (c *Config) Clone() *Config {
    // Salin primitive fields melalui value copy
    clone := &Config{
        Name:    c.Name,
        Timeout: c.Timeout,
    }

    // Deep copy untuk map — buat map baru, salin setiap entry
    clone.Headers = make(map[string]string, len(c.Headers))
    for k, v := range c.Headers {
        clone.Headers[k] = v
    }

    // Deep copy untuk slice — buat slice baru, salin setiap elemen
    clone.Tags = make([]string, len(c.Tags))
    copy(clone.Tags, c.Tags)

    return clone
}

func demonstrateDeepCopySafety() {
    original := &Config{
        Name:    "payment-service",
        Timeout: 30,
        Headers: map[string]string{"Auth": "Bearer token-original"},
        Tags:    []string{"production"},
    }

    clone := original.Clone()
    clone.Headers["Auth"] = "Bearer token-clone"
    clone.Tags = append(clone.Tags, "staging")

    fmt.Println(original.Headers["Auth"]) // "Bearer token-original" — tidak terpengaruh
    fmt.Println(original.Tags)            // ["production"] — tidak terpengaruh
}

Nested Object: Deep Copy Rekursif #

Untuk struct yang mengandung struct lain (nested object), deep copy harus dilakukan secara rekursif.

type Address struct {
    Street string
    City   string
    ZIP    string
}

type Customer struct {
    Name    string
    Email   string
    Address *Address       // pointer — perlu deep copy
    Orders  []*Order       // slice of pointer — perlu deep copy
    Metadata map[string]interface{} // map dengan interface{} — perlu perhatian khusus
}

func (c *Customer) Clone() *Customer {
    clone := &Customer{
        Name:  c.Name,
        Email: c.Email,
    }

    // Deep copy pointer ke Address
    if c.Address != nil {
        addressCopy := *c.Address // struct value copy — aman karena semua field string
        clone.Address = &addressCopy
    }

    // Deep copy slice of pointer
    if c.Orders != nil {
        clone.Orders = make([]*Order, len(c.Orders))
        for i, order := range c.Orders {
            clone.Orders[i] = order.Clone() // setiap Order harus punya Clone() juga
        }
    }

    // Deep copy map
    if c.Metadata != nil {
        clone.Metadata = make(map[string]interface{}, len(c.Metadata))
        for k, v := range c.Metadata {
            clone.Metadata[k] = v // hati-hati jika value adalah reference type
        }
    }

    return clone
}
flowchart TD
    subgraph "Shallow Copy"
        direction LR
        O1[Original] -->|berbagi| M1[map di memori]
        C1[Clone] -->|berbagi| M1
        style M1 fill:#ff9999
    end

    subgraph "Deep Copy"
        direction LR
        O2[Original] --> M2[map asli]
        C2[Clone] --> M3[map baru — salinan]
        style M2 fill:#99ff99
        style M3 fill:#99ff99
    end

Shallow Copy pada Reference Type adalah Bug Terselubung

Bug dari shallow copy biasanya tidak muncul di unit test karena test cenderung menggunakan satu instance pada satu waktu. Bug ini muncul di production ketika dua goroutine memodifikasi objek yang dikira independen secara bersamaan, menyebabkan race condition yang intermittent dan sangat sulit di-reproduce.


Implementasi Lengkap: Game Entity System #

Mari bangun implementasi Prototype yang realistis — sistem entity untuk game yang mengelola berbagai jenis karakter dengan stats dan equipment yang bisa di-clone.

Interface Prototype #

package entity

// Cloneable adalah interface Prototype — semua entity game harus mengimplementasikannya.
type Cloneable interface {
    Clone() Cloneable
}

// Stats menyimpan atribut numerik sebuah entity.
type Stats struct {
    Health    int
    MaxHealth int
    Attack    int
    Defense   int
    Speed     int
    Level     int
}

// Clone membuat salinan Stats yang independen.
func (s Stats) Clone() Stats {
    return s // struct tanpa pointer/slice/map — value copy sudah aman
}

// Equipment menyimpan item yang dipakai entity.
type Equipment struct {
    Weapon string
    Armor  string
    Helmet string
    Ring   string
}

// Clone membuat salinan Equipment yang independen.
func (e Equipment) Clone() Equipment {
    return e // struct tanpa pointer/slice/map — value copy sudah aman
}

Concrete Prototype: GameEntity #

// GameEntity merepresentasikan karakter dalam game.
type GameEntity struct {
    ID          string
    Name        string
    Type        string
    Stats       Stats
    Equipment   Equipment
    Skills      []string          // slice — perlu deep copy
    Buffs       map[string]int    // map — perlu deep copy
    AIBehavior  string
    SpawnWeight float64
}

// Clone menghasilkan GameEntity baru yang independen dari prototype.
// Semua modifikasi pada clone tidak mempengaruhi entity asli.
func (e *GameEntity) Clone() Cloneable {
    clone := &GameEntity{
        ID:          generateID(),   // ID baru untuk setiap clone
        Name:        e.Name,
        Type:        e.Type,
        Stats:       e.Stats.Clone(),
        Equipment:   e.Equipment.Clone(),
        AIBehavior:  e.AIBehavior,
        SpawnWeight: e.SpawnWeight,
    }

    // Deep copy Skills
    if e.Skills != nil {
        clone.Skills = make([]string, len(e.Skills))
        copy(clone.Skills, e.Skills)
    }

    // Deep copy Buffs
    if e.Buffs != nil {
        clone.Buffs = make(map[string]int, len(e.Buffs))
        for k, v := range e.Buffs {
            clone.Buffs[k] = v
        }
    }

    return clone
}

// AsEntity melakukan type assertion untuk mendapatkan *GameEntity dari Cloneable.
func AsEntity(c Cloneable) *GameEntity {
    if e, ok := c.(*GameEntity); ok {
        return e
    }
    return nil
}

func generateID() string {
    return fmt.Sprintf("entity-%d", time.Now().UnixNano())
}

Penggunaan: Clone dan Modifikasi #

func main() {
    // Buat prototype prajurit — proses mahal dilakukan sekali
    soldierPrototype := &entity.GameEntity{
        Name: "Soldier",
        Type: "humanoid",
        Stats: entity.Stats{
            Health:    100,
            MaxHealth: 100,
            Attack:    15,
            Defense:   10,
            Speed:     5,
            Level:     1,
        },
        Equipment: entity.Equipment{
            Weapon: "Iron Sword",
            Armor:  "Leather Armor",
        },
        Skills:     []string{"BasicAttack", "Block"},
        Buffs:      map[string]int{},
        AIBehavior: "aggressive",
        SpawnWeight: 0.6,
    }

    // Clone untuk berbagai variasi prajurit — hampir gratis
    regularSoldier := entity.AsEntity(soldierPrototype.Clone())
    regularSoldier.Name = "Regular Soldier"

    eliteSoldier := entity.AsEntity(soldierPrototype.Clone())
    eliteSoldier.Name = "Elite Soldier"
    eliteSoldier.Stats.Health = 150
    eliteSoldier.Stats.Attack = 25
    eliteSoldier.Equipment.Weapon = "Steel Sword"
    eliteSoldier.Equipment.Armor = "Chain Mail"
    eliteSoldier.Skills = append(eliteSoldier.Skills, "PowerStrike")

    captainSoldier := entity.AsEntity(soldierPrototype.Clone())
    captainSoldier.Name = "Captain"
    captainSoldier.Stats.Level = 5
    captainSoldier.Stats.Health = 200
    captainSoldier.Buffs["leadership_aura"] = 1

    // Prototype tidak terpengaruh oleh modifikasi clone
    fmt.Println(soldierPrototype.Stats.Health) // 100 — tidak berubah
    fmt.Println(soldierPrototype.Skills)       // [BasicAttack Block] — tidak bertambah
    fmt.Println(soldierPrototype.Equipment.Weapon) // Iron Sword — tidak berubah
}

Prototype Registry #

Dalam sistem yang kompleks, prototype tidak langsung di-hold sebagai variabel — melainkan disimpan dalam sebuah registry yang bisa diakses dari mana saja dan mendukung pendaftaran prototype baru saat runtime.

package entity

import (
    "fmt"
    "sync"
)

// Registry menyimpan kumpulan prototype yang bisa di-clone kapan saja.
// Thread-safe untuk digunakan dari multiple goroutine.
type Registry struct {
    mu         sync.RWMutex
    prototypes map[string]Cloneable
}

// NewRegistry membuat Registry baru yang kosong.
func NewRegistry() *Registry {
    return &Registry{
        prototypes: make(map[string]Cloneable),
    }
}

// Register mendaftarkan sebuah prototype dengan nama tertentu.
// Menimpa prototype yang sudah ada jika nama sama.
func (r *Registry) Register(name string, prototype Cloneable) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.prototypes[name] = prototype
}

// Create mengambil clone dari prototype yang terdaftar.
// Mengembalikan error jika nama prototype tidak ditemukan.
func (r *Registry) Create(name string) (Cloneable, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()

    prototype, ok := r.prototypes[name]
    if !ok {
        return nil, fmt.Errorf("prototype %q tidak ditemukan dalam registry", name)
    }
    return prototype.Clone(), nil
}

// List mengembalikan daftar semua nama prototype yang terdaftar.
func (r *Registry) List() []string {
    r.mu.RLock()
    defer r.mu.RUnlock()

    names := make([]string, 0, len(r.prototypes))
    for name := range r.prototypes {
        names = append(names, name)
    }
    return names
}

// Unregister menghapus prototype dari registry.
func (r *Registry) Unregister(name string) {
    r.mu.Lock()
    defer r.mu.Unlock()
    delete(r.prototypes, name)
}

Penggunaan Registry dalam aplikasi nyata:

func setupEntityRegistry() *entity.Registry {
    registry := entity.NewRegistry()

    // Daftarkan semua prototype saat startup
    registry.Register("soldier", &entity.GameEntity{
        Name:       "Soldier",
        Type:       "humanoid",
        Stats:      entity.Stats{Health: 100, Attack: 15, Defense: 10},
        Skills:     []string{"BasicAttack", "Block"},
        AIBehavior: "aggressive",
    })

    registry.Register("archer", &entity.GameEntity{
        Name:       "Archer",
        Type:       "humanoid",
        Stats:      entity.Stats{Health: 75, Attack: 20, Defense: 5, Speed: 8},
        Skills:     []string{"ArrowShot", "QuickShot", "Retreat"},
        AIBehavior: "ranged",
    })

    registry.Register("mage", &entity.GameEntity{
        Name:       "Mage",
        Type:       "humanoid",
        Stats:      entity.Stats{Health: 60, Attack: 30, Defense: 3, Speed: 4},
        Skills:     []string{"Fireball", "IceSpike", "Teleport"},
        AIBehavior: "caster",
    })

    registry.Register("boss_dragon", &entity.GameEntity{
        Name:        "Ancient Dragon",
        Type:        "dragon",
        Stats:       entity.Stats{Health: 5000, Attack: 80, Defense: 50, Level: 30},
        Skills:      []string{"DragonBreath", "TailSwipe", "FlyingRampage", "AncientRoar"},
        AIBehavior:  "boss",
        SpawnWeight: 0.01,
    })

    return registry
}

// Spawn membuat instance entity baru dari registry.
// Satu-satunya cara membuat entity di seluruh game — tidak ada hardcode tipe di sini.
func Spawn(registry *entity.Registry, entityType string) (*entity.GameEntity, error) {
    cloned, err := registry.Create(entityType)
    if err != nil {
        return nil, fmt.Errorf("gagal spawn entity %q: %w", entityType, err)
    }
    return entity.AsEntity(cloned), nil
}

func main() {
    registry := setupEntityRegistry()

    // Spawn berbagai entity dari registry — tidak ada concrete type di sini
    for i := 0; i < 10; i++ {
        soldier, _ := Spawn(registry, "soldier")
        soldier.Name = fmt.Sprintf("Soldier-%d", i+1)
    }

    // Plugin bisa mendaftarkan entity baru ke registry saat runtime
    registry.Register("custom_mob", loadCustomMobFromConfig("config/custom_mob.yaml"))
}
sequenceDiagram
    participant Game as Game Loop
    participant Reg as Registry
    participant Proto as Prototype
    participant Clone as New Entity

    Game->>Reg: Create("soldier")
    Reg->>Proto: Clone()
    Proto->>Clone: deep copy semua field
    Clone-->>Reg: instance baru
    Reg-->>Game: *GameEntity (independen)
    Game->>Clone: Modifikasi nama, posisi, dll
    Note over Proto: Prototype tidak terpengaruh

Kombinasi dengan Pattern Lain #

Prototype jarang berdiri sendiri — ia sering dikombinasikan dengan pattern lain untuk solusi yang lebih kuat.

Prototype + Factory Method #

Factory Method bisa menggunakan Prototype sebagai mekanisme internalnya. Alih-alih membuat objek baru, factory me-clone prototype yang sudah dikonfigurasi.

// EntityFactory menggunakan registry prototype internal
type EntityFactory struct {
    registry *entity.Registry
}

func NewEntityFactory(registry *entity.Registry) *EntityFactory {
    return &EntityFactory{registry: registry}
}

// CreateEntity adalah factory method yang menggunakan clone di balik layar
func (f *EntityFactory) CreateEntity(entityType string) (*entity.GameEntity, error) {
    return Spawn(f.registry, entityType)
}

// CreateElite membuat versi elite dari entity — clone + modifikasi
func (f *EntityFactory) CreateElite(entityType string) (*entity.GameEntity, error) {
    base, err := Spawn(f.registry, entityType)
    if err != nil {
        return nil, err
    }
    base.Name = "Elite " + base.Name
    base.Stats.Health = int(float64(base.Stats.Health) * 1.5)
    base.Stats.Attack = int(float64(base.Stats.Attack) * 1.3)
    return base, nil
}

Prototype + Builder #

Builder bisa menggunakan prototype sebagai base object, kemudian memodifikasi field tertentu melalui fluent API.

// EntityBuilder menggunakan prototype sebagai titik awal
type EntityBuilder struct {
    entity *entity.GameEntity
}

func NewEntityBuilderFrom(prototype *entity.GameEntity) *EntityBuilder {
    return &EntityBuilder{
        entity: entity.AsEntity(prototype.Clone()), // clone dulu, modifikasi kemudian
    }
}

func (b *EntityBuilder) WithName(name string) *EntityBuilder {
    b.entity.Name = name
    return b
}

func (b *EntityBuilder) WithLevel(level int) *EntityBuilder {
    b.entity.Stats.Level = level
    b.entity.Stats.Health = int(float64(b.entity.Stats.MaxHealth) * (1 + float64(level)*0.1))
    b.entity.Stats.Attack = int(float64(b.entity.Stats.Attack) * (1 + float64(level)*0.05))
    return b
}

func (b *EntityBuilder) WithSkill(skill string) *EntityBuilder {
    b.entity.Skills = append(b.entity.Skills, skill)
    return b
}

func (b *EntityBuilder) Build() *entity.GameEntity {
    return b.entity
}

// Penggunaan: Prototype + Builder untuk entity yang sangat terkustomisasi
bossVariant := NewEntityBuilderFrom(soldierPrototype).
    WithName("Veteran Commander").
    WithLevel(10).
    WithSkill("CommandRoar").
    WithSkill("BattleCry").
    Build()

Testing Prototype #

Testing Prototype Pattern berfokus pada dua hal: memastikan clone menghasilkan nilai yang sama, dan memastikan clone benar-benar independen dari prototype.

func TestGameEntity_Clone_ProducesEqualValues(t *testing.T) {
    original := &entity.GameEntity{
        Name: "Soldier",
        Type: "humanoid",
        Stats: entity.Stats{Health: 100, Attack: 15},
        Skills: []string{"BasicAttack", "Block"},
        Buffs:  map[string]int{"rage": 5},
    }

    cloned := entity.AsEntity(original.Clone())

    // Nilai harus sama
    if cloned.Name != original.Name {
        t.Errorf("Name: got %q, want %q", cloned.Name, original.Name)
    }
    if cloned.Stats.Health != original.Stats.Health {
        t.Errorf("Stats.Health: got %d, want %d", cloned.Stats.Health, original.Stats.Health)
    }
    if !reflect.DeepEqual(cloned.Skills, original.Skills) {
        t.Errorf("Skills tidak sama: got %v, want %v", cloned.Skills, original.Skills)
    }
}

func TestGameEntity_Clone_IsIndependent(t *testing.T) {
    original := &entity.GameEntity{
        Name:   "Soldier",
        Skills: []string{"BasicAttack"},
        Buffs:  map[string]int{"rage": 5},
    }

    cloned := entity.AsEntity(original.Clone())

    // Modifikasi clone tidak boleh mempengaruhi original
    cloned.Name = "Elite Soldier"
    cloned.Skills = append(cloned.Skills, "PowerStrike")
    cloned.Buffs["poison"] = 3

    if original.Name != "Soldier" {
        t.Errorf("original.Name terpengaruh: got %q", original.Name)
    }
    if len(original.Skills) != 1 {
        t.Errorf("original.Skills terpengaruh: got %v", original.Skills)
    }
    if _, ok := original.Buffs["poison"]; ok {
        t.Errorf("original.Buffs terpengaruh — shallow copy bug!")
    }
}

func TestGameEntity_Clone_HasNewID(t *testing.T) {
    original := &entity.GameEntity{Name: "Soldier"}
    clone1 := entity.AsEntity(original.Clone())
    clone2 := entity.AsEntity(original.Clone())

    if clone1.ID == clone2.ID {
        t.Errorf("dua clone memiliki ID yang sama: %s", clone1.ID)
    }
    if clone1.ID == original.ID {
        t.Errorf("clone memiliki ID yang sama dengan original")
    }
}

func TestRegistry_CreateReturnsClone(t *testing.T) {
    registry := entity.NewRegistry()
    prototype := &entity.GameEntity{
        Name:   "Soldier",
        Skills: []string{"BasicAttack"},
    }
    registry.Register("soldier", prototype)

    entity1, err := registry.Create("soldier")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }

    entity2, _ := registry.Create("soldier")

    // Keduanya harus independen — bukan pointer yang sama
    if entity1 == entity2 {
        t.Error("registry mengembalikan pointer yang sama, bukan clone")
    }
}

func TestRegistry_CreateUnknown_ReturnsError(t *testing.T) {
    registry := entity.NewRegistry()

    _, err := registry.Create("unknown_type")
    if err == nil {
        t.Error("expected error for unknown prototype, got nil")
    }
}

Kapan Menggunakan dan Kapan Tidak #

GUNAKAN Prototype jika:
  ✓ Pembuatan objek melibatkan operasi mahal (I/O, network, parsing)
  ✓ Banyak objek memiliki state awal yang sama dan hanya berbeda di beberapa field
  ✓ Tipe objek yang harus dibuat ditentukan saat runtime, bukan compile-time
  ✓ Ingin menghindari subclass explosion untuk setiap variasi kecil
  ✓ Perlu snapshot objek yang bisa dimodifikasi tanpa mempengaruhi aslinya

HINDARI Prototype jika:
  ✗ Pembuatan objek sudah murah dan sederhana — tidak ada manfaat performa
  ✗ Struct berisi circular reference — clone menjadi sangat kompleks
  ✗ Objek memiliki resource eksternal (koneksi, file handle) yang tidak bisa di-clone
  ✗ Semua field adalah primitif dan konstruktor sudah cukup ekspresif
  ✗ Tim tidak familiar dengan deep copy — risiko bug shallow copy terlalu tinggi

Objek dengan Resource Eksternal Tidak Cocok untuk Prototype

Jika struct menyimpan resource yang terikat ke OS atau jaringan — seperti *sql.DB, *os.File, net.Conn, atau channel yang aktif — cloning struct-nya tidak berarti meng-clone resource tersebut. Clone dan original akan berbagi resource yang sama, yang bisa menyebabkan bug yang sangat sulit di-debug. Untuk objek semacam ini, gunakan Factory Pattern atau Object Pool.


Checklist Review Prototype #

IMPLEMENTASI CLONE:
  □ Semua field primitive ter-copy dengan benar (value copy)
  □ Semua map di-deep copy (buat map baru, salin setiap entry)
  □ Semua slice di-deep copy (buat slice baru, copy elemen)
  □ Semua pointer di-deep copy (alokasi struct baru, copy nilainya)
  □ Nested struct yang punya reference type juga di-clone rekursif
  □ ID atau identifier unik di-generate baru untuk setiap clone

REGISTRY (jika digunakan):
  □ Operasi Register dan Create thread-safe (gunakan sync.RWMutex)
  □ Create mengembalikan error jika nama tidak ditemukan
  □ Prototype yang disimpan di registry tidak dimodifikasi dari luar

TESTING:
  □ Test bahwa clone menghasilkan nilai yang sama dengan original
  □ Test bahwa modifikasi pada clone tidak mempengaruhi original
  □ Test bahwa modifikasi pada original tidak mempengaruhi clone yang sudah dibuat
  □ Test bahwa setiap clone memiliki ID yang unik
  □ Test race condition jika Clone() dipanggil dari multiple goroutine

DOKUMENTASI:
  □ Method Clone() didokumentasikan: shallow atau deep copy?
  □ Field mana yang sengaja di-share (jika ada) dijelaskan dengan jelas
  □ Resource eksternal yang tidak di-clone disebutkan secara eksplisit

Ringkasan #

  • Prototype meng-clone objek yang sudah ada — bukan membangun dari nol; cocok ketika proses pembangunan objek mahal atau ketika banyak objek berbagi state awal yang sama.
  • Deep copy adalah kewajiban, bukan pilihan — shallow copy pada reference type (map, slice, pointer) menciptakan shared mutable state yang menjadi sumber bug race condition yang sangat sulit di-debug.
  • Setiap field perlu penanganan berbeda: primitive di-copy via value assignment; map dibuat baru dan entry-nya di-copy; slice dibuat baru dan elemen-nya di-copy; pointer di-alokasi ulang.
  • Prototype Registry memungkinkan pendaftaran dan pembuatan prototype secara dinamis — cocok untuk plugin system, multi-tenant config, dan game entity system.
  • Kombinasi yang powerful: Prototype + Factory Method untuk encapsulation creation logic; Prototype + Builder untuk customization bertahap dari base object.
  • Objek dengan resource eksternal tidak cocok di-clone*sql.DB, net.Conn, dan *os.File tidak bisa di-clone secara bermakna; gunakan Factory atau Object Pool untuk kasus ini.
  • Testing wajib mencakup independence test — verifikasi bahwa modifikasi clone tidak mempengaruhi original; ini adalah tes paling penting untuk memastikan deep copy berjalan benar.
  • Jangan gunakan Prototype untuk objek sederhana — jika konstruktor sudah murah dan ekspresif, menambahkan Clone() hanya menambah kompleksitas tanpa manfaat nyata.

← Sebelumnya: Builder   Berikutnya: Adapter →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact