Decorator Pattern
Part of the Object Oriented Programming section of Coddy's GO journey — lesson 93 of 107.
The Decorator pattern allows you to add new behavior to objects dynamically by wrapping them in other objects. While Adapter changes an interface to match another, Decorator keeps the same interface but enhances functionality by layering wrappers around the original object.
In Go, we implement this by having both the base type and decorators implement the same interface:
type Notifier interface {
Send(message string) string
}
type BasicNotifier struct{}
func (b BasicNotifier) Send(message string) string {
return "Sending: " + message
}Decorators wrap another Notifier and add behavior before or after delegating to it:
type TimestampDecorator struct {
wrapped Notifier
}
func (t TimestampDecorator) Send(message string) string {
timestamped := "[2024-01-15] " + message
return t.wrapped.Send(timestamped)
}
type UppercaseDecorator struct {
wrapped Notifier
}
func (u UppercaseDecorator) Send(message string) string {
return strings.ToUpper(u.wrapped.Send(message))
}The power of Decorator lies in stacking multiple wrappers to combine behaviors:
notifier := BasicNotifier{}
withTimestamp := TimestampDecorator{wrapped: notifier}
withBoth := UppercaseDecorator{wrapped: withTimestamp}
fmt.Println(notifier.Send("Hello"))
// Sending: Hello
fmt.Println(withBoth.Send("Hello"))
// SENDING: [2024-01-15] HELLODecorator is ideal when you need to add responsibilities to objects without modifying their code, especially when different combinations of features are needed. It's commonly used for logging, caching, authentication, and compression layers.
Challenge
EasyLet's build a coffee shop ordering system using the Decorator pattern! You'll create a base coffee drink and decorators that add extras like milk, sugar, and whipped cream. Each decorator wraps the previous one, building up both the description and the price—a perfect real-world example of how decorators layer functionality.
You'll organize your code across three files:
beverage.go: Define your core interface and base coffee type.Create a
Beverageinterface with two methods:Description() string— returns what the drink isCost() float64— returns the price
Implement a
Coffeestruct as your base beverage. Its description should beCoffeeand its cost should be2.00.decorators.go: Create your decorator types that add extras to any beverage.Each decorator wraps a
Beverageand implements the same interface. Build three decorators:MilkDecorator— adds, Milkto the description and0.50to the costSugarDecorator— adds, Sugarto the description and0.25to the costWhipDecorator— adds, Whipto the description and0.75to the cost
Each decorator should call the wrapped beverage's methods and enhance the result.
main.go: Build customized drinks by stacking decorators.Read a count of extras to add. Then for each extra, read its type (
milk,sugar, orwhip) and wrap your current beverage with the appropriate decorator.After applying all extras, print the final drink's description and cost on separate lines. Format the cost as
$X.XXwith exactly two decimal places.
The following inputs will be provided:
- Line 1: Number of extras to add
- Following lines: One extra type per line (
milk,sugar, orwhip)
For example, given:
2
milk
sugarYour output should be:
Coffee, Milk, Sugar
$2.75And given:
3
whip
milk
milkYour output should be:
Coffee, Whip, Milk, Milk
$3.75And given:
0Your output should be:
Coffee
$2.00Notice how each decorator wraps the previous beverage, building up both the description and cost. You can add the same extra multiple times (like double milk), and the order of decorators determines the order in the description. The base coffee doesn't know anything about the extras—each decorator simply enhances whatever it wraps!
Cheat sheet
The Decorator pattern adds new behavior to objects dynamically by wrapping them in other objects. It keeps the same interface but enhances functionality through layering.
Both the base type and decorators implement the same interface:
type Notifier interface {
Send(message string) string
}
type BasicNotifier struct{}
func (b BasicNotifier) Send(message string) string {
return "Sending: " + message
}Decorators wrap another object and add behavior before or after delegating:
type TimestampDecorator struct {
wrapped Notifier
}
func (t TimestampDecorator) Send(message string) string {
timestamped := "[2024-01-15] " + message
return t.wrapped.Send(timestamped)
}
type UppercaseDecorator struct {
wrapped Notifier
}
func (u UppercaseDecorator) Send(message string) string {
return strings.ToUpper(u.wrapped.Send(message))
}Stack multiple decorators to combine behaviors:
notifier := BasicNotifier{}
withTimestamp := TimestampDecorator{wrapped: notifier}
withBoth := UppercaseDecorator{wrapped: withTimestamp}
fmt.Println(notifier.Send("Hello"))
// Sending: Hello
fmt.Println(withBoth.Send("Hello"))
// SENDING: [2024-01-15] HELLOUse Decorator to add responsibilities without modifying object code, especially for different feature combinations. Common uses: logging, caching, authentication, compression.
Try it yourself
package main
import (
"fmt"
)
func main() {
// Read the number of extras
var count int
fmt.Scanln(&count)
// Start with a base coffee
var drink Beverage = &Coffee{}
// Read each extra and wrap the drink with the appropriate decorator
for i := 0; i < count; i++ {
var extra string
fmt.Scanln(&extra)
// TODO: Based on the extra type, wrap drink with the appropriate decorator
// Use MilkDecorator for "milk"
// Use SugarDecorator for "sugar"
// Use WhipDecorator for "whip"
}
// TODO: Print the final description and cost
// Format cost as $X.XX with exactly two decimal places
}
This lesson includes a short quiz. Start the lesson to answer it and track your progress.
All lessons in Object Oriented Programming
1Fundamentals of Go OOP
External FilesGo Workspace & ModulesPackages & ImportsExported vs Unexported NamesIntroduction to OOP in GoStructs as ClassesDefining Methods on StructsPointer vs Value ReceiversStruct InitializationConstructor FunctionsRecap - Simple Calculator4Interfaces
Introduction to InterfacesImplicit ImplementationInterface as ContractEmpty Interface (any)Type AssertionType SwitchInterface CompositionStringer & Error InterfacesRecap - Shape Calculator7Encapsulation
Exported vs Unexported FieldsPackage-Level EncapsulationGetter & Setter MethodsInformation Hiding in GoRecap - Student Records10Generics (Go 1.18+)
Introduction to GenericsType ParametersType ConstraintsGeneric StructsGeneric Methods WorkaroundRecap - Generic Collection2Types & Structs Deep Dive
Basic & Composite TypesCustom Type DefinitionsStruct TagsAnonymous StructsNested StructsZero Values & DefaultsRecap - Contact Book5Composition Over Inheritance
Why Go Has No InheritanceStruct Embedding BasicsMethod PromotionEmbedding Multiple StructsEmbedding vs AggregationShadowing Embedded MethodsRecap - Employee Hierarchy8Error Handling & OOP
The error InterfaceCustom Error TypesError Wrapping (fmt.Errorf)Sentinel Errorserrors.Is() and errors.As()Panic, Defer, and RecoverRecap - File Parser11Standard Library & OOP
io.Reader & io.Writersort.Interfacefmt.Stringer Interfaceencoding/json with Structshttp.Handler InterfaceRecap - REST API Models14Design Patterns Part 2
Command PatternAdapter PatternDecorator PatternTemplate Method PatternState PatternComposite PatternMiddleware as Decorator3Pointers & Memory
Pointer Basics in GoPointers to StructsPass by Value vs ReferenceThe new() FunctionGarbage Collection in GoRecap - Linked List Builder6Polymorphism in Go
Polymorphism via InterfacesDuck Typing in GoInterface Satisfaction RulesPolymorphic CollectionsDependency InjectionRecap - Payment Processor9Concurrency & OOP
Goroutines BasicsChannels & CommunicationBuffered vs Unbuffered ChanSelect Statementsync.Mutex & sync.RWMutexsync.WaitGroupThread-Safe Struct DesignRecap - Worker Pool