- Lab
-
Libraries: If you want this lab, consider one of these libraries.
- Core Tech
Creational Design Pattern in C++
BrewForge runs the software behind a self-service coffee kiosk. The original prototype hard-wires a single espresso machine implementation, scatters brewing, grinding and logging concerns into one class, and rebuilds full drink recipes from scratch for every order. Your job across this lab is to turn that prototype into a maintainable creational-pattern showcase: components depend on abstractions instead of concrete classes, a single guarded configuration object controls shop-wide settings, drinks are assembled with a fluent builder and cloned from ready-made signature recipes, whole families of products (regular versus decaf) are swapped through an abstract factory, and the checkout flow depends on an injected payment interface you can substitute with a stub in tests.
Lab Info
Table of Contents
-
Challenge
Step 1: Meet the BrewForge kiosk codebase
BrewForge: one kiosk, one tangled class
BrewForge runs the software behind a self-service coffee kiosk. The prototype you inherited works, sort of: a single
CoffeeMachineclass heats water, grinds beans, tracks shop settings and writes log lines, all in one place. Drinks are built by hand-filling a struct field by field, every order rebuilds a recipe from nothing, and the checkout code calls a concreteCreditCardProcessordirectly. None of this is wrong, exactly, but none of it can be swapped, tested in isolation, or reused without dragging the rest along with it.Over the next four steps you will not add features. You will change how the same behavior is produced, one class at a time, using the five creational patterns of the Gang of Four. ## Where each pattern lands
- Step 2 -- SOLID split.
CoffeeMachinestops doing its own heating and grinding. Those become separate, substitutable collaborators it depends on through interfaces. - Step 3 -- Singleton. Shop-wide settings (target temperature, default dose) move into one guarded
MachineConfigobject that every machine reads from, instead of each machine keeping its own copy. - Step 4 -- Builder and Prototype. Custom drinks get assembled through a fluent
CoffeeDrinkBuilderchain; signature drinks get cloned from a prototype registry instead of rebuilt field by field. - Step 5 -- Factory Method, Abstract Factory, and injected services. A factory method picks the right drink for an order type, an abstract factory swaps whole regular-versus-decaf ingredient families, and checkout depends on an injected
PaymentProcessorinterface rather than a concrete class.
Each step edits existing files under
src/; nothing you build in an earlier step gets thrown away later -- it gets depended on.info> If you get stuck, you can refer to the provided solution code for each task, available in the
solutionfolder. - Step 2 -- SOLID split.
-
Challenge
Step 2: Rebuild the coffee machine around SOLID abstractions
Splitting the machine along its seams
CoffeeMachinecurrently owns three unrelated jobs: reaching a temperature, grinding a dose, and writing a log line. The SOLID fix is not to rename methods -- it is to give each job its own interface (HeatingElement,Grinder,Logger) and a concrete implementation, then haveCoffeeMachinehold pointers to those interfaces instead of doing the work itself. That is what makes each piece substitutable: a test can handCoffeeMachinea fake heater without ever touching real hardware.Start with the heating element, since it has no dependency on anything else in the file. ## The second collaborator: grinding
With the heater reporting real numbers instead of echoing requests,
BladeGrinderneeds the same treatment: a real grinder does not turn every requested gram into an identical gram of output, because different grind sizes lose more or less material. Model that loss explicitly sogrind()returns something a caller can actually trust. ## Wiring the collaborators togetherHeatingElementandGrinderare now independent and testable. The last piece isCoffeeMachineitself: it should stop containing any brewing logic of its own and instead orchestrate the two interfaces it holds, plus a third injectedLoggerinterface that replaces the print statements the old monolith had scattered through it. -
Challenge
Step 3: Guard shared machine settings with a singleton
One settings object, guarded
Right now nothing stops two parts of the kiosk from holding two different copies of "the" shop temperature. A singleton fixes that by making the class itself refuse to be copied and exposing exactly one way to reach an instance: a static accessor.
MachineConfig's constructor is private and its copy operations are already deleted in the starter -- your job is the accessor that hands back the one shared object. ## Making the machine read the singletonA singleton is only useful once something consults it.
CoffeeMachine::brew()already calls adescribeSettings()method as part of assembling its output; right now that method is a stub. Complete it so the description comes fromMachineConfig::instance(), and every machine in the kiosk will report the same settings the moment you change them in one place. -
Challenge
Step 4: Assemble drinks with a fluent builder and clone prototypes
Assembling drinks by chaining, not filling a struct
Hand-filling a drink struct field by field scales badly once a drink can have any number of syrups. A fluent builder fixes this by making every setter return the builder itself, so calls read as one chain:
builder.withSyrup("vanilla").withSyrup("caramel").build(). Start with the setter that has to accumulate a list rather than just overwrite a field. ## Closing the chainEvery setter so far just records state.
build()is where that accumulated state finally becomes a realCoffeeDrink, which is also where a chain with zero syrup calls has to still produce a sensible result rather than a string with a dangling separator. ## Cloning instead of rebuildingSignature drinks -- the kiosk's named specials -- do not need to be assembled fresh through the builder every time; they need to be copied from a known-good prototype. That is the Prototype pattern: a
clone()method on the prototype itself, returning a new object rather than a pointer to the one the registry keeps. Getting this wrong by returning the original pointer would let one order share, and potentially corrupt, the registry's signature recipe. -
Challenge
Step 5: Centralize creation with factories and inject services
Choosing a drink from an order type
Somewhere, an order type has to turn into an actual drink -- today that logic is duplicated wherever it is needed. A factory method centralizes it: one function that takes the order type and returns the finished drink, so every caller asks the factory instead of repeating the branching itself. ## Swapping whole families of ingredients
Regular and decaf orders do not just differ in one bean field -- they differ in a whole family of ingredients that all have to agree with each other. An abstract factory captures that: one interface,
IngredientFactory, with two implementations that each produce a self-consistent family. Implement the regular family first. ## The second familyThe checkout code that consumes an
IngredientFactoryshould never need to know which concrete family it is holding. Implement the decaf side of the same interface so that swappingRegularIngredientFactoryforDecafIngredientFactoryis the only change needed anywhere in the pipeline. ## Checkout depends on an interface, not a classThe last concrete dependency left in the pipeline is payment:
OrderServiceshould hold astd::shared_ptr<PaymentProcessor>handed to it at construction, not a specific card processor it constructs itself. That single change is what lets a test substitute a stub processor later without touchingOrderServiceat all. -
Challenge
Step 6: Run the application
Every task is done, and each one was graded the moment you completed it. What you have not done yet is watch the whole application run at once -- which is the only place the pieces you wrote separately become a single working program.
Start the application
Open the Terminal tab and run:
make app && ./appThe program runs in the terminal and prints its output there. Every behaviour it shows is code you wrote in the previous steps, so read the output against what each task asked for -- that is the whole lab, working as one program. ### Run the checks yourself
The lab ran each task's test for you as you went, but they are ordinary tests and you can run them whenever you like. The whole suite:
bash runTest.shOr one task at a time:
bash runTest.sh "task 2.1"Each failure names the task it belongs to, so the output tells you which step to go back to rather than which line to stare at. Two things worth trying before you finish: break something on purpose and watch the matching check fail, then put it back. Knowing what a failure looks like is worth as much as knowing what a pass looks like, and it costs you nothing here.
When you are done, everything you wrote is still in the editor tabs -- the lab is yours to keep reading.
About the author
Real skill practice before real-world application
Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.
Learn by doing
Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.
Follow your guide
All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.
Turn time into mastery
On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.