Skip to content

Commit 7634d27

Browse files
committed
docs: create README and Applications documentation for Prototype design pattern
1 parent e46a84e commit 7634d27

2 files changed

Lines changed: 286 additions & 0 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Prototype — Real-World Applications
2+
3+
---
4+
5+
## 1. Server Configuration Variants (this implementation)
6+
7+
**Problem:** A service needs multiple configurations — one for production, one for staging, one per feature flag — that share 95% of their fields. Constructing each from scratch is verbose and risks diverging defaults across variants.
8+
9+
**How Prototype helps:** Load one base `ServerConfig` (from file, environment, or a default constructor), then clone it once per variant and override only what differs:
10+
11+
```go
12+
base := LoadServerConfigFromEnv()
13+
14+
stagingConfig := base.Clone()
15+
stagingConfig.Tags["env"] = "staging"
16+
17+
featureXConfig := base.Clone()
18+
featureXConfig.Middlewares = append(featureXConfig.Middlewares, "feature-x-guard")
19+
```
20+
21+
Each clone is fully independent — mutating one does not affect the others or the base.
22+
23+
---
24+
25+
## 2. Game Entity Spawning
26+
27+
**Problem:** A game has hundreds of enemy types, each defined by a large struct: sprite, hitpoints, speed, attack patterns, loot table, sound effects. Constructing every spawned enemy from a config file at spawn time is too slow for real-time gameplay.
28+
29+
**How Prototype helps:** Load each enemy *template* once at startup. At spawn time, clone the template and assign a position and unique ID:
30+
31+
```go
32+
goblinTemplate := LoadEnemyTemplate("goblin")
33+
34+
// Spawn 50 goblins at different positions
35+
for _, spawnPoint := range spawnPoints {
36+
enemy := goblinTemplate.Clone()
37+
enemy.Position = spawnPoint
38+
enemy.ID = uuid.New()
39+
scene.Add(enemy)
40+
}
41+
```
42+
43+
The expensive deserialization happens once. Every clone is O(fields) rather than O(file I/O + deserialization).
44+
45+
---
46+
47+
## 3. Feature Flag Configuration
48+
49+
**Problem:** A backend serves multiple tenants or experiments. Each needs the base server config plus one or two overrides. A factory would require passing every field; a builder requires the caller to know all defaults.
50+
51+
**How Prototype helps:**
52+
53+
```go
54+
base := productionConfig.Clone()
55+
56+
experimentA := base.Clone()
57+
experimentA.Tags["experiment"] = "new-auth-flow"
58+
experimentA.Middlewares = append(experimentA.Middlewares, "new-auth-middleware")
59+
60+
experimentB := base.Clone()
61+
experimentB.HashAlgorithms = "sha512"
62+
```
63+
64+
The diff between `base` and each experiment is small and explicit. Adding a new field to the base automatically propagates to all future clones — no factory method to update.
65+
66+
---
67+
68+
## 4. Test Fixture Construction
69+
70+
**Problem:** Tests need slightly different versions of a complex object. Repeating full struct literals across test cases is verbose, and when a required field is added to the struct every test breaks.
71+
72+
**How Prototype helps:** Define one canonical prototype per domain object; each test clones and mutates only the relevant field:
73+
74+
```go
75+
validConfig := NewDefaultServerConfig()
76+
77+
// Test: middleware list is empty
78+
noMiddleware := validConfig.Clone()
79+
noMiddleware.Middlewares = nil
80+
81+
// Test: no upstreams configured
82+
noUpstreams := validConfig.Clone()
83+
noUpstreams.Upstreams = nil
84+
```
85+
86+
When `ServerConfig` gains a new required field, only `NewDefaultServerConfig()` needs updating — all test clones inherit the new field automatically.
87+
88+
---
89+
90+
## 5. Document / Template Cloning in CMS
91+
92+
**Problem:** A content management system has document templates: blog post, press release, product page. Each template has pre-set metadata, default sections, and required tags. Authors start from a template, not a blank document.
93+
94+
**How Prototype helps:**
95+
96+
```go
97+
blogTemplate := cms.GetTemplate("blog-post")
98+
99+
newPost := blogTemplate.Clone()
100+
newPost.Title = "Q2 Product Update"
101+
newPost.Author = currentUser
102+
newPost.Tags["category"] = "product"
103+
```
104+
105+
The template is never modified. Each new document starts with the template's defaults and diverges from there. This is exactly the Prototype pattern — clone a known-good base and customize the delta.
106+
107+
---
108+
109+
## 6. Connection Pool Seed Configuration
110+
111+
**Problem:** A connection pool creates N database connections at startup, each needing the same TLS config, timeouts, and credentials — but each connection is a distinct object with its own socket and state.
112+
113+
**How Prototype helps:** Create one seed connection config, clone it N times, then open the actual connection from each clone:
114+
115+
```go
116+
seedConfig := NewDBConnConfig(dsn, tlsCert, timeout)
117+
118+
pool := make([]DBConn, poolSize)
119+
for i := range pool {
120+
cfg := seedConfig.Clone()
121+
pool[i] = openConnection(cfg)
122+
}
123+
```
124+
125+
If the config held a live socket (a pointer field), this would require special handling — the clone copies config *values*, not the connection itself. This is the boundary where Prototype ends and object lifecycle management begins.
126+
127+
---
128+
129+
## Summary: When Prototype earns its complexity
130+
131+
| Application | What Prototype prevents |
132+
|---|---|
133+
| Server config variants | Re-specifying all defaults per variant; diverging defaults |
134+
| Game entity spawning | Per-spawn file I/O and deserialization cost |
135+
| Feature flag configs | Boilerplate factory methods for each experiment |
136+
| Test fixtures | Full struct literals per test; breakage when struct gains fields |
137+
| CMS templates | Blank-document starts; accidental template mutation |
138+
| Connection pool configs | Repeating TLS/timeout config N times |
139+
140+
The interview insight: **Prototype is the right choice when you already have a valid, expensive-to-reproduce instance and need many independent variations of it. Its value is not just ergonomic — a correct `Clone()` is the single place where "deep enough" is defined, and every consumer gets independence guarantees for free.**

creational/prototype/README.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
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

Comments
 (0)