Welcome to the third lesson of our "Applying Design Patterns for Real World Problems using Scala" course! In this lesson, we'll delve into two powerful design patterns: the Command pattern and the Decorator pattern. These patterns will empower you to design a robust and flexible smart home automation and lighting control system in Scala 3. Let's embark on this journey to create a system that’s as adaptable as it is efficient! 🚀
Here's a brief overview of what we'll be doing in this lesson:
-
Basic Setup:
- Abstract Device: Define a trait
Device
and create specific device classes (Light
,Fan
) that extend from it. - Factory Method: Leverage companion objects to serve as factories for generating instances of these devices.
- Abstract Device: Define a trait
-
Command Pattern:
- Purpose: Encapsulates a request as an object, facilitating parameterization, queuing, logging, and support for undoable operations.
- Components:
- Define a
Command
trait and concrete command classes (LightOnCommand
,LightOffCommand
). - Implement a
RemoteControl
class for command executions.
- Define a
-
Decorator Pattern:
- Purpose: Dynamically adds functionalities to existing objects without altering their structure.
- Components:
- Use a decorator class (
ColoredLight
) to add color capabilities to aLight
device.
- Use a decorator class (
Let’s dive into the Scala code and bring these patterns to life! 🎉
Let's start by defining the devices we'll be working with. We'll create a trait Device
and extend it with concrete device classes Light
and Fan
.
Scala1// Abstract Device 2trait Device: 3 def on(): Unit 4 def off(): Unit 5 6// Concrete Device: Light 7class Light extends Device: 8 def on(): Unit = println("Light is on.") 9 def off(): Unit = println("Light is off.") 10 11// Concrete Device: Fan 12class Fan extends Device: 13 private var speed: Int = 0 14 def on(): Unit = println("Fan is on.") 15 def off(): Unit = println("Fan is off.") 16 def setSpeed(speed: Int): Unit = 17 this.speed = speed 18 println(s"Fan speed set to $speed.")
This code defines the foundational elements of our smart home system:
- The
Device
trait establishes a common interface withon()
andoff()
methods. - The
Light
class extendsDevice
with basic functionality to turn the light on and off. - The
Fan
class extendsDevice
and adds the ability to set the fan's speed through thesetSpeed()
method.
We'll now implement factory methods using a companion object to create instances of our devices.
Scala1// Companion Object as Factory 2object DeviceFactory: 3 def createLight(): Device = new Light() 4 def createFan(): Device = new Fan()
By introducing DeviceFactory
, we encapsulate the creation of Device
instances, promoting better code organization and flexibility. The factory methods createLight()
and createFan()
simplify object instantiation.
Next, we'll apply the Command pattern to create flexible automation commands. First, we define a Command
trait as the foundation for our system, alongside concrete command classes for light operations:
Scala1// Command Interface 2trait Command: 3 def execute(): Unit 4 5// Concrete Command: Turn Light On 6class LightOnCommand(device: Device) extends Command: 7 def execute(): Unit = device.on() 8 9// Concrete Command: Turn Light Off 10class LightOffCommand(device: Device) extends Command: 11 def execute(): Unit = device.off()
In this snippet, we establish the Command
trait and implement LightOnCommand
and LightOffCommand
to encapsulate turning a light on and off.
Next, we introduce the RemoteControl
class to handle the execution of commands.
Scala1// Invoker: RemoteControl 2class RemoteControl: 3 private var command: Option[Command] = None 4 5 def setCommand(command: Command): Unit = 6 this.command = Some(command) 7 8 def pressButton(): Unit = 9 command.foreach(_.execute())
The RemoteControl
class acts as the invoker in the Command pattern:
- It holds a reference to a
Command
object. - The
setCommand()
method assigns a command to the remote. - The
pressButton()
method executes the assigned command, if any.
We now put the command pattern into action within our main Scala program.
Scala1@main def runCommandPattern(): Unit = 2 // Create a light instance using the factory 3 val light: Device = DeviceFactory.createLight() 4 5 // Create command instances for turning the light on and off 6 val lightOn = new LightOnCommand(light) 7 val lightOff = new LightOffCommand(light) 8 9 // Set commands in the remote control 10 val remote = new RemoteControl() 11 remote.setCommand(lightOn) 12 13 // Simulate pressing the button 14 remote.pressButton() // Output: Light is on. 15 remote.setCommand(lightOff) 16 remote.pressButton() // Output: Light is off.
This example demonstrates:
- Creating a
Light
device. - Instantiating
LightOnCommand
andLightOffCommand
with the light device. - Using the
RemoteControl
to set and execute commands, turning the light on and off.
Now, we'll enhance our lighting system using the Decorator pattern.
We start with the definition of a ColoredLight
decorator class to add color functionality to our lights:
Scala1// Decorator: ColoredLight 2class ColoredLight(light: Device, color: String) extends Device: 3 def on(): Unit = 4 light.on() 5 println(s"Light color changed to $color.") 6 7 def off(): Unit = light.off()
In this simple snippet:
ColoredLight
acts as a wrapper around aDevice
object (such as aLight
), adding color functionality.- It overrides the
on()
method to first turn on the light and then add the color-changing functionality. - The
off()
method simply delegates to the wrapped device.
Finally, let's apply our ColoredLight
decorator to dynamically add color functionality to light devices.
Scala1@main def useDecoratorPattern(): Unit = 2 // Create a light instance using the factory 3 val light: Device = DeviceFactory.createLight() 4 5 // Decorate the light with color 6 val redLight = new ColoredLight(light, "Red") 7 val blueLight = new ColoredLight(light, "Blue") 8 9 redLight.on() // Output: Light is on. Light color changed to Red. 10 redLight.off() // Output: Light is off. 11 blueLight.on() // Output: Light is on. Light color changed to Blue. 12 blueLight.off() // Output: Light is off.
This code shows how:
- We create
ColoredLight
instances to enhance lights with color. - When
on()
is called, it turns on the light and changes its color, demonstrating dynamic addition of functionality.
By integrating the Command and Decorator design patterns, you've built a smart home system that is both modular and flexible. These patterns enable you to extend functionality and dynamically add features without modifying the core structure. Now it's your turn—get hands-on by implementing these patterns yourself and watch your smart home system come alive! 🌟