Architecture & Patterns110 min total · 26 parts
ACID, SOLID, and Design Patterns: A Complete Software Design Reference
Part 14 of 26 · ~3 min
Creational Patterns: Builder and Prototype
Builder assembles a complicated object across several small steps instead of one big one, and it's worth reaching for the moment that object accumulates a long list of fields where most of them are optional on any given use. Skip it and the fallback is a giant constructor with parameters stacked ten or twelve deep — nobody can read the call site without counting positions, and it's far too easy to swap two adjacent arguments of the same type by mistake — or a pile of overloads covering every likely combination, which is the well-known "telescoping constructor" mess.
Posting a new open shift on ShiftDesk can involve a skill requirement, a minimum age, whether a background check is required, a recurring weekly pattern, and whether it needs a licensed van driver — most of them optional, most postings needing only two or three:
public class ShiftPosting
{
public string SiteId { get; set; } = "";
public string? SkillRequired { get; set; }
public int? MinimumAge { get; set; }
public bool RequiresBackgroundCheck { get; set; }
public bool RequiresVanDriver { get; set; }
public string? RecurringPattern { get; set; }
}
public class ShiftPostingBuilder
{
private readonly ShiftPosting _posting = new();
public ShiftPostingBuilder ForSite(string siteId) { _posting.SiteId = siteId; return this; }
public ShiftPostingBuilder RequiringSkill(string skill) { _posting.SkillRequired = skill; return this; }
public ShiftPostingBuilder WithMinimumAge(int age) { _posting.MinimumAge = age; return this; }
public ShiftPostingBuilder RequiringBackgroundCheck() { _posting.RequiresBackgroundCheck = true; return this; }
public ShiftPostingBuilder RequiringVanDriver() { _posting.RequiresVanDriver = true; return this; }
public ShiftPosting Build() => _posting;
}
// Readable at the call site, in any order, naming only the options that actually apply.
var posting = new ShiftPostingBuilder()
.ForSite("shelter")
.RequiringVanDriver()
.Build();
Prototype takes a different escape route out of expensive construction: rather than assembling an object from raw inputs again, copy one that's already fully built and tweak the copy. That's worth it precisely when assembling from scratch costs something real — a settings lookup, a computed default skill list per site — and what's actually wanted is a near-identical variant of something already sitting in memory. Every Saturday, the shelter's coordinator spins up the same recurring shift with one or two tweaks:
public class ShiftTemplate : ICloneable
{
public string SiteId { get; set; } = "";
public TimeSpan StartTime { get; set; }
public List<string> RequiredSkills { get; set; } = new();
public object Clone()
{
// A shallow Clone() is not enough on its own — RequiredSkills is a
// reference type, so the original and the clone would share the
// same List instance unless it's copied explicitly, as done here.
return new ShiftTemplate
{
SiteId = SiteId,
StartTime = StartTime,
RequiredSkills = new List<string>(RequiredSkills)
};
}
}
var standardSaturday = new ShiftTemplate { SiteId = "shelter", StartTime = new TimeSpan(9, 0, 0) };
var adoptionEventVariant = (ShiftTemplate)standardSaturday.Clone();
adoptionEventVariant.RequiredSkills.Add("public speaking"); // doesn't affect standardSaturday.RequiredSkills
Read that inline comment again, because it's flagging the trap that catches nearly everyone the first time they implement Prototype in a language with reference semantics. Copy a field the naive way and what actually gets copied for anything that isn't a primitive is a pointer to the same underlying object — not a second, independent object. Touch the "cloned" list afterward without realizing that, and the edit lands on the original too, silently, with the only fix being an explicit, deliberate copy of each nested field inside Clone() itself.