|
| 1 | +# Prototype — Creational Design Pattern |
| 2 | + |
| 3 | +> **This is Prototype, not Builder or Factory.** Factory decides *which* object to create. Builder assembles one object step by step. Prototype skips construction entirely — it copies an already-configured object and lets you mutate only what differs. |
| 4 | +
|
| 5 | +--- |
| 6 | + |
| 7 | +## What is it? |
| 8 | + |
| 9 | +The Prototype pattern creates new objects by cloning an existing instance (the *prototype*) rather than constructing from scratch. The original holds a valid, fully-initialized configuration; every clone starts with that same baseline and can be independently modified. |
| 10 | + |
| 11 | +It sits in the *Creational* family because its sole concern is producing new instances — but its mechanism is copying, not constructing. |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## When to use it |
| 16 | + |
| 17 | +- When constructing an object from scratch is expensive (database lookups, network calls, heavy computation) and a pre-loaded instance can be reused as a starting point. |
| 18 | +- When you need many slightly-different configurations of the same object and spelling out every field for each variant is verbose and error-prone. |
| 19 | +- When the exact type of the object is only known at runtime, and you want to avoid a large `switch` / factory dispatch just to get a new instance. |
| 20 | + |
| 21 | +**Avoid it** when the object is cheap to construct and has only a few fields. Cloning a trivial struct is needless indirection. |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## Prototype vs Factory vs Builder — side by side |
| 26 | + |
| 27 | +| | Factory | Builder | Prototype | |
| 28 | +|---|---|---|---| |
| 29 | +| Goal | Choose *which* type to create | Configure *how* to construct one type | Copy a *known-good* instance | |
| 30 | +| Entry point | A function returning a new object | A builder; caller chains setters then calls `Build()` | A `Clone()` method on an existing instance | |
| 31 | +| Validation | At selection time | At `Build()` time | On the original; clones inherit validity | |
| 32 | +| Example here | `NewDatabaseFactory("postgres")` | `NewRequestBuilder().WithUrl(...).Build()` | `original.Clone()` | |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +## Go's approach vs. OOP languages |
| 37 | + |
| 38 | +In Java, Prototype often involves implementing `Cloneable` and overriding `Object.clone()`. Go has no such interface — the pattern is expressed directly as a method that returns a new value. |
| 39 | + |
| 40 | +| Mechanism | Purpose | |
| 41 | +|---|---| |
| 42 | +| `ServerConfig` struct | The prototype — holds all configuration fields | |
| 43 | +| `Clone() ServerConfig` | Returns a fully independent deep copy | |
| 44 | +| `maps.Clone(sc.Tags)` | Deep-copies the map so mutations on the clone don't affect the original | |
| 45 | +| `append([]T{}, src...)` | Deep-copies slices by allocating a new backing array | |
| 46 | + |
| 47 | +Because Go's `maps.Clone` and the slice append trick both allocate new backing memory, the clone and the original share no mutable state. Scalar fields (`HashAlgorithms`, etc.) are copied by value automatically. |
| 48 | + |
| 49 | +--- |
| 50 | + |
| 51 | +## Structure of this implementation |
| 52 | + |
| 53 | +``` |
| 54 | +prototype/ |
| 55 | +├── main.go # Usage demonstration |
| 56 | +└── server_config/ |
| 57 | + ├── server_config.go # ServerConfig struct + Clone() + NewDefaultServerConfig() |
| 58 | + └── server_config_test.go # Deep-copy correctness tests |
| 59 | +``` |
| 60 | + |
| 61 | +### The prototype struct |
| 62 | + |
| 63 | +[server_config/server_config.go](server_config/server_config.go) defines `ServerConfig`: |
| 64 | + |
| 65 | +```go |
| 66 | +type ServerConfig struct { |
| 67 | + Upstreams []ServiceEndpoint |
| 68 | + Downstreams []ServiceEndpoint |
| 69 | + Tags map[string]string |
| 70 | + Middlewares []string |
| 71 | + HashAlgorithms string |
| 72 | +} |
| 73 | +``` |
| 74 | + |
| 75 | +`ServiceEndpoint` is a plain struct (no pointers), so copying it by value is safe — no nested heap allocation to worry about. |
| 76 | + |
| 77 | +### The `Clone()` method |
| 78 | + |
| 79 | +```go |
| 80 | +func (sc ServerConfig) Clone() ServerConfig { |
| 81 | + return ServerConfig{ |
| 82 | + Tags: maps.Clone(sc.Tags), |
| 83 | + Upstreams: append([]ServiceEndpoint{}, sc.Upstreams...), |
| 84 | + Downstreams: append([]ServiceEndpoint{}, sc.Downstreams...), |
| 85 | + Middlewares: append([]string{}, sc.Middlewares...), |
| 86 | + HashAlgorithms: sc.HashAlgorithms, |
| 87 | + } |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +- **`maps.Clone`** — allocates a new map and copies every key-value pair. Mutations to one map do not affect the other. |
| 92 | +- **`append([]T{}, src...)`** — allocates a new backing array with the same elements. Appending to one slice does not affect the other's length or elements. |
| 93 | +- **Value receiver** — `sc` is already a copy of the caller's struct, so scalar fields (`HashAlgorithms`) are automatically independent. |
| 94 | + |
| 95 | +### The default constructor |
| 96 | + |
| 97 | +`NewDefaultServerConfig()` returns a ready-to-use `ServerConfig` that serves as the prototype. In production, this would typically be a configuration loaded from a file or environment — the prototype pattern shines when that load is expensive and you need many derived configurations. |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +## Why deep copy matters |
| 102 | + |
| 103 | +Go's assignment and function calls copy struct values, but slices and maps contain a pointer to their underlying data. A naive `return sc` would copy the slice *header* (pointer + length + capacity) but not the backing array. Both the original and the copy would point to the same memory — a mutation in one would be visible in the other. |
| 104 | + |
| 105 | +```go |
| 106 | +// WRONG — shallow copy |
| 107 | +func (sc ServerConfig) ShallowClone() ServerConfig { |
| 108 | + return sc // sc.Tags still points to original's map |
| 109 | +} |
| 110 | + |
| 111 | +clone := original.ShallowClone() |
| 112 | +clone.Tags["env"] = "staging" // also mutates original.Tags |
| 113 | +``` |
| 114 | + |
| 115 | +`Clone()` avoids this by explicitly allocating new backing storage for every reference type field. |
| 116 | + |
| 117 | +--- |
| 118 | + |
| 119 | +## Testing |
| 120 | + |
| 121 | +[`server_config_test.go`](server_config/server_config_test.go) covers: |
| 122 | + |
| 123 | +| Test | What is verified | |
| 124 | +|---|---| |
| 125 | +| `TestCloneHasSameValuesAsOriginal` | Clone starts with identical values across all fields | |
| 126 | +| `TestMutatingCloneTagsDoesNotAffectOriginal` | Tags map is independently allocated | |
| 127 | +| `TestAppendingToCloneMiddlewaresDoesNotAffectOriginal` | Middlewares slice has its own backing array | |
| 128 | +| `TestModifyingCloneScalarFieldsDoesNotAffectOriginal` | Scalar fields are independent by value | |
| 129 | +| `TestTwoClonesFromSameBaseAreIndependent` | Two clones from the same base do not share state | |
| 130 | +| `TestChangesReflectInOriginal` | Original regression — confirms Upstreams are deep-copied | |
| 131 | + |
| 132 | +--- |
| 133 | + |
| 134 | +## Key interview talking points |
| 135 | + |
| 136 | +**Q: Why use a value receiver on `Clone()` instead of a pointer receiver?** |
| 137 | +A value receiver means Go passes a copy of the struct into the method body. Scalar fields are already independent — only slices and maps need explicit deep-copy. A pointer receiver would also work, but the value receiver signals that `Clone()` does not mutate the receiver, which is consistent with its purpose. |
| 138 | + |
| 139 | +**Q: What breaks if you forget to deep-copy a map field?** |
| 140 | +Both the original and the clone hold a pointer to the same underlying hash table. Any write to one (`clone.Tags["k"] = "v"`) is immediately visible in the other. The bug is silent — there is no panic, just incorrect shared state, which is the hardest class of bug to diagnose. |
| 141 | + |
| 142 | +**Q: When would you prefer Prototype over Factory?** |
| 143 | +When the object already exists in a valid, expensive-to-reproduce state. A Factory always constructs from scratch; Prototype says "I already have a good one — give me a copy." Classic cases: game entity spawning (clone a pre-loaded enemy config), feature-flag server configs (clone the base config and override one flag per variant), or test fixtures (clone a canonical object and mutate one field per test case). |
| 144 | + |
| 145 | +**Q: What happens if `ServiceEndpoint` contained a pointer field?** |
| 146 | +The slice append trick copies `ServiceEndpoint` values, but if a field inside it were a pointer, the pointer itself would be copied — both slices' elements would point to the same heap object. You would need to deep-copy at the element level too. Keeping value types in the slice avoids this class of problem entirely. |
0 commit comments