Sometimes the easiest way to make a new object is to take one that’s already set up correctly and copy it. Prototype puts that copy operation inside the object being copied, because it’s the only one that truly knows what it contains.
| Category | Creational |
| Smell fixed | S04: Product variants copy fields by hand |
| Code | patterns/creational/prototype |
The Scene
The second step of the trading day adds a new size and colour of CornerStore’s best-selling running shoe:
[002] variant parent=SHOE-RUN-42 sku=SHOE-RUN-44 size=44 colour=Blue
created variant SHOE-RUN-44 of SHOE-RUN-42
[003] variant parent=EBOOK-FIT sku=EBOOK-FIT-2 size=A5 colour=None
rejected: EBOOK-FIT is not a physical product
A variant is almost the same as its parent. Same brand, same manufacturer, same price, same weight, same description. Only the SKU, size and colour change. So who knows how to make one?
The Smell
In the baseline, the catalogue does. Field by field:
var variant = new PhysicalProduct
{
Sku = sku,
Name = parent.Name + " (" + size + ", " + colour + ")",
Price = parent.Price,
CategoryName = parent.CategoryName,
Brand = parent.Brand,
Manufacturer = parent.Manufacturer,
WeightKg = parent.WeightKg,
Size = size,
Colour = colour,
Material = parent.Material,
Description = parent.Description,
};
It works today. Now picture the day someone adds a CountryOfOrigin field to PhysicalProduct. They update the class, the import, maybe the reports. Do they remember a variant-copying block that lives in a completely different file? If they don’t, every new variant quietly loses its country of origin, and nothing fails.
The catalogue is reaching into a product and copying its insides. It knows far too much about a class it doesn’t own.
The Idea
Prototype says: if you want a copy, ask the object for one. The object knows its own state, so it’s the right place to decide what gets copied and what gets replaced.
Think of a photocopier versus retyping a document. When you retype, every word is a chance to make a mistake. With a photocopy, you get exactly what’s on the original. You just write the new details on top.
The Refactor
PhysicalProduct now has a Clone method that takes only the things that change:
public PhysicalProduct Clone(string sku, string size, string colour)
{
return new PhysicalProduct
{
Sku = sku,
Name = Name + " (" + size + ", " + colour + ")",
Price = Price,
CategoryName = CategoryName,
Brand = Brand,
Manufacturer = Manufacturer,
WeightKg = WeightKg,
Size = size,
Colour = colour,
Material = Material,
Description = Description,
};
}
And the catalogue shrinks to two lines:
var variant = parent.Clone(sku, size, colour);
Register(categories[parent.CategoryName], variant);
You might be thinking: “That’s the same list of fields. You just moved it.”
You’re right, and that’s the point. The list now lives inside the class that owns those fields. When someone adds CountryOfOrigin, the Clone method is sitting a few lines away in the same file. The catalogue keeps the jobs that are genuinely its own, like checking that the parent exists, that it’s a physical product, and that the new SKU is free.
classDiagram
class Product {
<<abstract>>
+Sku
+Name
+Price
+CategoryName
}
class PhysicalProduct {
+Brand
+Manufacturer
+WeightKg
+Size
+Colour
+Material
+Description
+Clone(sku, size, colour) PhysicalProduct
}
class Catalog
Product <|-- PhysicalProduct
Catalog --> PhysicalProduct : validates and registers variants
Catalog ..> PhysicalProduct : calls Clone
What It Cost
The smallest refactor in the Creational group: 4 files and about 92 lines added and 16 removed. Most of the additions are tests that prove the clone copies every field and leaves the original untouched.
When Not to Use It
- Objects are cheap and simple to construct. If a constructor with three arguments does the job, copying a prototype adds nothing.
- Some state should be shared, not copied. Cloning raises the classic question: shallow or deep copy? If a product held a list of reviews, should the variant get the same list, a copy of it, or an empty one? Prototype forces you to decide, and getting it wrong causes subtle bugs.
- The copy isn’t really the same kind of thing. If the new object has different responsibilities, build it properly instead of copying something that only looks similar.
The modern C# way
In modern C#, records have this built in. A with expression creates a copy with only the listed properties changed:
var variant = parent with { Sku = sku, Size = size, Colour = colour };
The compiler writes the copy for you, so a new property can never be forgotten. The CornerStore solution keeps an explicit Clone because products are classes with their own naming rules for variants. If your types fit the record model, with is Prototype with the boilerplate removed.
Easily Confused With
Factory Method. Both hand you a new object without the caller building it directly. Factory Method creates from scratch, with the construction rules in a creator. Prototype creates by copying an existing, already-configured instance. If your new object is “the same as that one, but slightly different”, that’s Prototype.
Try It Yourself
dotnet build patterns/creational/prototype/Prototype.slnx
dotnet test patterns/creational/prototype/Prototype.slnx
dotnet run --project patterns/creational/prototype/src/CornerStore/CornerStore.csproj -- scenarios/trading-day.json
Exercise: add a new property to PhysicalProduct and see how naturally your eye lands on Clone while you’re in that file. Compare that with hunting through Catalog.cs in the baseline.
The Prototype README has the participant table and sequence diagram. The full change is in DIFF.md.
Previous: Factory Method · Next: Singleton