Prototype Pattern #
Imagine a game system managing hundreds of NPCs (Non-Player Characters). Every Soldier NPC has the same base stats — health 100, attack 15, armor 10 — plus equipment already loaded from a database, animations already parsed from files, and an AI behavior tree already compiled. Creating each new soldier from scratch means repeating all those expensive steps for every instance. A far more efficient approach: make one “template” soldier, then have each new soldier clone the template and modify only the values that need to differ — name, position, or carried items. That is the essence of the Prototype Pattern: creating new objects by duplicating an existing one, rather than building it from scratch every time.
What Is the Prototype Pattern? #
The Prototype Pattern is a creational design pattern that creates new objects by cloning an existing object (called the prototype), instead of calling a constructor and filling every field from zero. The cloned object acts as a mold — it holds ready-made initial state, and each clone can modify specific parts without affecting the original prototype.
In Go, there is no built-in clone mechanism like in Java or C++. The Prototype Pattern is implemented explicitly through an interface with a Clone() method, and every struct is responsible for implementing its own clone correctly — including handling reference-type fields like maps, slices, and pointers.
Two properties define the Prototype Pattern:
- Cloning produces an independent object — modifications to the clone must not affect the prototype, and vice versa
- The client does not need to know the concrete type — it only needs to know the object can be cloned through the interface
flowchart LR
subgraph "Without Prototype"
direction TB
C1[Create NPC 1\\nfrom scratch] --> D1["Load DB\\nParse animations\\nCompile AI\\n⏱ ~200ms"]
C2[Create NPC 2\\nfrom scratch] --> D2["Load DB\\nParse animations\\nCompile AI\\n⏱ ~200ms"]
C3[Create NPC 3\\nfrom scratch] --> D3["Load DB\\nParse animations\\nCompile AI\\n⏱ ~200ms"]
end
subgraph "With Prototype"
direction TB
T[Template NPC] --> D4["Load DB\\nParse animations\\nCompile AI\\n⏱ ~200ms once"]
D4 --> P[Prototype ready]
P -->|Clone + modify| N1["NPC 1 ⏱ ~1ms"]
P -->|Clone + modify| N2["NPC 2 ⏱ ~1ms"]
P -->|Clone + modify| N3["NPC 3 ⏱ ~1ms"]
endWhen Is Prototype Needed #
Prototype is not a pattern for every situation. There are three concrete conditions that make Prototype the right choice.
Condition 1: Expensive Object Creation #
If creating the object involves time-consuming or resource-heavy operations — database queries, file reads, network calls, parsing large XML/JSON, or intensive upfront computation — cloning is far more efficient than rebuilding from scratch.
// ANTI-PATTERN: every request creates a new ReportTemplate from scratch
func GenerateReport(reportType string, data []Row) (*Report, error) {
template := &ReportTemplate{}
template.LoadStylesheet("corporate.css") // read file
template.LoadLogoAsset("logo.png") // read file
template.ParseLayout("quarterly_layout.xml") // parse XML
template.InitChartEngine() // initialize engine
// total: ~150ms for every report
return template.Render(data)
}
// CORRECT: the template is prepared once, each report clones it
var reportPrototype *ReportTemplate
func init() {
reportPrototype = &ReportTemplate{}
reportPrototype.LoadStylesheet("corporate.css")
reportPrototype.LoadLogoAsset("logo.png")
reportPrototype.ParseLayout("quarterly_layout.xml")
reportPrototype.InitChartEngine()
// total: ~150ms once at startup
}
func GenerateReport(reportType string, data []Row) (*Report, error) {
template := reportPrototype.Clone() // ~1ms — just a field copy
return template.Render(data)
}
Condition 2: Many Objects with Similar Structure #
When many objects share the same base state and only differ in a few fields, Prototype is cleaner than creating many subclasses or repeatedly calling a constructor with many parameters.
Condition 3: Object Creation at Runtime #
When the type of object to create is determined at runtime — for example, based on user configuration or incoming data — a Prototype Registry enables object creation without hardcoding concrete types in client code.
Shallow Copy vs Deep Copy #
This is the most critical aspect of the Prototype Pattern. A mistake in the clone implementation is the source of bugs that are very hard to detect, because objects look different while still sharing the same data in memory.
Shallow Copy: Sharing References #
A shallow copy copies field values directly. For primitive fields (int, string, bool, float64) this is safe because the values get copied. But for reference-type fields (map, slice, pointer, interface), only the memory address is copied — not the data.
type Config struct {
Name string
Timeout int
Headers map[string]string // reference type — DANGEROUS for shallow copy
Tags []string // reference type — DANGEROUS for shallow copy
}
// ANTI-PATTERN: a shallow copy incorrectly called "clone"
func (c *Config) ShallowClone() *Config {
copy := *c // copy all fields directly
return ©
// copy.Headers and c.Headers point to the SAME MAP in memory!
// copy.Tags and c.Tags point to the SAME SLICE in memory!
}
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" // SAFE — string is a value type
clone.Timeout = 60 // SAFE — int is a value type
clone.Headers["Auth"] = "Bearer token-clone" // DANGEROUS — modifies the original too!
clone.Tags = append(clone.Tags, "staging") // DANGEROUS — depends on slice capacity
fmt.Println(original.Headers["Auth"]) // "Bearer token-clone" — not "Bearer token-original"!
// A bug that is extremely hard to trace
}
Deep Copy: Every Object Independent #
A deep copy creates a fresh copy of every reference type. The resulting clone is truly independent — no shared state between the clone and the prototype.
// CORRECT: a safe deep copy
func (c *Config) Clone() *Config {
// Copy primitive fields via value copy
clone := &Config{
Name: c.Name,
Timeout: c.Timeout,
}
// Deep copy for maps — create a new map, copy every entry
clone.Headers = make(map[string]string, len(c.Headers))
for k, v := range c.Headers {
clone.Headers[k] = v
}
// Deep copy for slices — create a new slice, copy every element
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" — unaffected
fmt.Println(original.Tags) // ["production"] — unaffected
}
Nested Objects: Recursive Deep Copy #
For structs that contain other structs (nested objects), the deep copy must be done recursively.
type Address struct {
Street string
City string
ZIP string
}
type Customer struct {
Name string
Email string
Address *Address // pointer — needs deep copy
Orders []*Order // slice of pointers — needs deep copy
Metadata map[string]interface{} // map with interface{} — needs special care
}
func (c *Customer) Clone() *Customer {
clone := &Customer{
Name: c.Name,
Email: c.Email,
}
// Deep copy the pointer to Address
if c.Address != nil {
addressCopy := *c.Address // struct value copy — safe because all fields are strings
clone.Address = &addressCopy
}
// Deep copy the slice of pointers
if c.Orders != nil {
clone.Orders = make([]*Order, len(c.Orders))
for i, order := range c.Orders {
clone.Orders[i] = order.Clone() // every Order must have its own Clone() too
}
}
// Deep copy the map
if c.Metadata != nil {
clone.Metadata = make(map[string]interface{}, len(c.Metadata))
for k, v := range c.Metadata {
clone.Metadata[k] = v // be careful if a value is a reference type
}
}
return clone
}
flowchart TD
subgraph "Shallow Copy"
direction LR
O1[Original] -->|share| M1[map in memory]
C1[Clone] -->|share| M1
style M1 fill:#ff9999
end
subgraph "Deep Copy"
direction LR
O2[Original] --> M2[original map]
C2[Clone] --> M3[new map — a copy]
style M2 fill:#99ff99
style M3 fill:#99ff99
endShallow Copy on Reference Types Is a Hidden Bug
Bugs from shallow copy usually do not show up in unit tests, because tests tend to use one instance at a time. The bug appears in production when two goroutines modify objects they believe are independent at the same time, causing an intermittent race condition that is extremely hard to reproduce.
Full Implementation: Game Entity System #
Let’s build a realistic Prototype implementation — an entity system for a game that manages various character types with cloneable stats and equipment.
The Prototype Interface #
package entity
// Cloneable is the Prototype interface — every game entity must implement it.
type Cloneable interface {
Clone() Cloneable
}
// Stats stores an entity's numeric attributes.
type Stats struct {
Health int
MaxHealth int
Attack int
Defense int
Speed int
Level int
}
// Clone creates an independent copy of Stats.
func (s Stats) Clone() Stats {
return s // a struct without pointer/slice/map — value copy is already safe
}
// Equipment stores the items an entity is wearing.
type Equipment struct {
Weapon string
Armor string
Helmet string
Ring string
}
// Clone creates an independent copy of Equipment.
func (e Equipment) Clone() Equipment {
return e // a struct without pointer/slice/map — value copy is already safe
}
Concrete Prototype: GameEntity #
// GameEntity represents a character in the game.
type GameEntity struct {
ID string
Name string
Type string
Stats Stats
Equipment Equipment
Skills []string // slice — needs deep copy
Buffs map[string]int // map — needs deep copy
AIBehavior string
SpawnWeight float64
}
// Clone produces a new GameEntity independent of the prototype.
// Any modification to the clone does not affect the original entity.
func (e *GameEntity) Clone() Cloneable {
clone := &GameEntity{
ID: generateID(), // a new ID for every 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 type-asserts a Cloneable into a *GameEntity.
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())
}
Usage: Clone and Modify #
func main() {
// Create a soldier prototype — the expensive process runs once
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 for various soldier variants — almost free
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
// The prototype is unaffected by clone modifications
fmt.Println(soldierPrototype.Stats.Health) // 100 — unchanged
fmt.Println(soldierPrototype.Skills) // [BasicAttack Block] — not extended
fmt.Println(soldierPrototype.Equipment.Weapon) // Iron Sword — unchanged
}
Prototype Registry #
In complex systems, prototypes are not held directly in variables — they are stored in a registry that can be accessed from anywhere and supports registering new prototypes at runtime.
package entity
import (
"fmt"
"sync"
)
// Registry stores a collection of prototypes that can be cloned at any time.
// Thread-safe for use from multiple goroutines.
type Registry struct {
mu sync.RWMutex
prototypes map[string]Cloneable
}
// NewRegistry creates a new, empty Registry.
func NewRegistry() *Registry {
return &Registry{
prototypes: make(map[string]Cloneable),
}
}
// Register adds a prototype under a given name.
// Overwrites an existing prototype if the name is the same.
func (r *Registry) Register(name string, prototype Cloneable) {
r.mu.Lock()
defer r.mu.Unlock()
r.prototypes[name] = prototype
}
// Create returns a clone of the registered prototype.
// Returns an error if the prototype name is not found.
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 not found in registry", name)
}
return prototype.Clone(), nil
}
// List returns the names of all registered prototypes.
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 removes a prototype from the registry.
func (r *Registry) Unregister(name string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.prototypes, name)
}
Using the Registry in a real application:
func setupEntityRegistry() *entity.Registry {
registry := entity.NewRegistry()
// Register all prototypes at 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 creates a new entity instance from the registry.
// The only way to create entities across the whole game — no hardcoded types here.
func Spawn(registry *entity.Registry, entityType string) (*entity.GameEntity, error) {
cloned, err := registry.Create(entityType)
if err != nil {
return nil, fmt.Errorf("failed to spawn entity %q: %w", entityType, err)
}
return entity.AsEntity(cloned), nil
}
func main() {
registry := setupEntityRegistry()
// Spawn various entities from the registry — no concrete types here
for i := 0; i < 10; i++ {
soldier, _ := Spawn(registry, "soldier")
soldier.Name = fmt.Sprintf("Soldier-%d", i+1)
}
// Plugins can register new entities into the registry at 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 all fields
Clone-->>Reg: new instance
Reg-->>Game: *GameEntity (independent)
Game->>Clone: Modify name, position, etc.
Note over Proto: Prototype is unaffectedCombinations with Other Patterns #
Prototype rarely stands alone — it is often combined with other patterns for stronger solutions.
Prototype + Factory Method #
Factory Method can use Prototype as its internal mechanism. Instead of creating new objects, the factory clones a pre-configured prototype.
// EntityFactory uses an internal prototype registry
type EntityFactory struct {
registry *entity.Registry
}
func NewEntityFactory(registry *entity.Registry) *EntityFactory {
return &EntityFactory{registry: registry}
}
// CreateEntity is a factory method that uses cloning behind the scenes
func (f *EntityFactory) CreateEntity(entityType string) (*entity.GameEntity, error) {
return Spawn(f.registry, entityType)
}
// CreateElite creates an elite version of an entity — clone + modify
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 #
A Builder can use a prototype as the base object, then modify specific fields through its fluent API.
// EntityBuilder uses a prototype as its starting point
type EntityBuilder struct {
entity *entity.GameEntity
}
func NewEntityBuilderFrom(prototype *entity.GameEntity) *EntityBuilder {
return &EntityBuilder{
entity: entity.AsEntity(prototype.Clone()), // clone first, modify later
}
}
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
}
// Usage: Prototype + Builder for highly customized entities
bossVariant := NewEntityBuilderFrom(soldierPrototype).
WithName("Veteran Commander").
WithLevel(10).
WithSkill("CommandRoar").
WithSkill("BattleCry").
Build()
Testing Prototype #
Testing the Prototype Pattern focuses on two things: ensuring the clone produces equal values, and ensuring the clone is truly independent of the 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())
// Values must be equal
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 differ: 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())
// Modifying the clone must not affect the original
cloned.Name = "Elite Soldier"
cloned.Skills = append(cloned.Skills, "PowerStrike")
cloned.Buffs["poison"] = 3
if original.Name != "Soldier" {
t.Errorf("original.Name affected: got %q", original.Name)
}
if len(original.Skills) != 1 {
t.Errorf("original.Skills affected: got %v", original.Skills)
}
if _, ok := original.Buffs["poison"]; ok {
t.Errorf("original.Buffs affected — 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("two clones share the same ID: %s", clone1.ID)
}
if clone1.ID == original.ID {
t.Errorf("clone has the same ID as the 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")
// Both must be independent — not the same pointer
if entity1 == entity2 {
t.Error("registry returned the same pointer, not a 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")
}
}
When to Use and When Not to #
USE Prototype if:
✓ Object creation involves expensive operations (I/O, network, parsing)
✓ Many objects share the same initial state and only differ in a few fields
✓ The type of object to create is determined at runtime, not compile time
✓ You want to avoid subclass explosion for every small variation
✓ You need object snapshots that can be modified without affecting the original
AVOID Prototype if:
✗ Object creation is already cheap and simple — no performance benefit
✗ The struct contains circular references — cloning becomes very complex
✗ The object holds external resources (connections, file handles) that cannot be cloned
✗ All fields are primitives and the constructor is already expressive enough
✗ The team is unfamiliar with deep copy — the shallow copy bug risk is too high
Objects with External Resources Are Not Suited to Prototype
If the struct stores resources tied to the OS or network — such as
*sql.DB,*os.File,net.Conn, or active channels — cloning the struct does not clone those resources. The clone and the original will share the same resource, which can cause bugs that are extremely hard to debug. For such objects, use the Factory Pattern or an Object Pool.
Prototype Review Checklist #
CLONE IMPLEMENTATION:
□ All primitive fields are copied correctly (value copy)
□ All maps are deep copied (create a new map, copy every entry)
□ All slices are deep copied (create a new slice, copy the elements)
□ All pointers are deep copied (allocate a new struct, copy its value)
□ Nested structs with reference types are also cloned recursively
□ A new unique ID or identifier is generated for every clone
REGISTRY (if used):
□ Register and Create operations are thread-safe (use sync.RWMutex)
□ Create returns an error when the name is not found
□ Prototypes stored in the registry are not modified from outside
TESTING:
□ Test that the clone produces the same values as the original
□ Test that modifying the clone does not affect the original
□ Test that modifying the original does not affect an already-made clone
□ Test that every clone has a unique ID
□ Test for race conditions if Clone() is called from multiple goroutines
DOCUMENTATION:
□ The Clone() method documents whether it is shallow or deep copy
□ Fields intentionally shared (if any) are clearly explained
□ External resources that are not cloned are explicitly mentioned
Summary #
- Prototype clones an existing object — instead of building from scratch; ideal when object construction is expensive or when many objects share the same initial state.
- Deep copy is a duty, not a choice — shallow copying reference types (map, slice, pointer) creates shared mutable state that becomes a source of extremely hard-to-debug race condition bugs.
- Every field needs different handling: primitives are copied by value assignment; maps are recreated and their entries copied; slices are recreated and their elements copied; pointers are reallocated.
- The Prototype Registry enables dynamic registration and creation of prototypes — great for plugin systems, multi-tenant configs, and game entity systems.
- Powerful combinations: Prototype + Factory Method to encapsulate creation logic; Prototype + Builder for step-by-step customization of a base object.
- Objects with external resources are not suitable for cloning —
*sql.DB,net.Conn, and*os.Filecannot be meaningfully cloned; use Factory or Object Pool for those cases.- Testing must include an independence test — verify that clone modifications do not affect the original; this is the most important test for proving the deep copy works correctly.
- Don’t use Prototype for simple objects — if the constructor is already cheap and expressive, adding
Clone()only adds complexity without real benefit.