C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
110: The Command Pattern
You've probably encountered situations where your code feels too "tight." Maybe you have a UI button that directly calls a method on a database service, or a controller that knows way too much about the internal workings of a business logic class. When you do that, you're tying the invoker (the thing that triggers the action) directly to the receiver (the thing that actually does the work).
The Command Pattern is how we break that bond. It turns a request into a stand-alone object. This sounds like overkill until you realize it lets you queue operations, log them, or—my favorite—implement a robust undo system without making your main logic a nightmare of if-else statements.
Setting up our Smart Home gadgets
Let's build a smart home remote. To start, we need some actual devices to control. I'll keep these simple: a Light and a Thermostat. These are our "Receivers." They don't know anything about commands or remotes; they just know how to do their jobs.
public class Light
{
public void TurnOn() => Console.WriteLine("The light is bright!");
public void TurnOff() => Console.WriteLine("The light is off.");
}
public class Thermostat
{
public void SetTemperature(int temp) => Console.WriteLine($"Temp set to {temp}°C");
}
The "quick and dirty" way
Now, I could just build a RemoteControl class and give it a reference to the light and the thermostat. I'll show you how I'd probably do it if I were rushing to meet a deadline on a Friday afternoon:
public class RemoteControl
{
private Light _light;
private Thermostat _thermostat;
public RemoteControl(Light light, Thermostat thermostat)
{
_light = light;
_thermostat = thermostat;
}
public void PressLightButton() => _light.TurnOn();
public void PressTempButton() => _thermostat.SetTemperature(22);
}
Wait. I can already see the problem here. Every time I buy a new smart device—like a smart blind or a coffee maker—I have to go back into the RemoteControl class and add new properties and new methods. This is a violation of the Open/Closed Principle. The remote should be open for extension but closed for modification. We're treating the remote like a hard-wired switchboard instead of a universal controller.
Abstracting the action with ICommand
To fix this, I need to stop the remote from knowing what it's controlling. Instead, it should just know how to execute a "command." I'll create a simple interface that represents any action that can be triggered.
public interface ICommand
{
void Execute();
void Undo();
}
Now, I'll wrap those device-specific calls into their own classes. Each class acts as a bridge between the remote and the device. Notice how the LightOnCommand takes the Light object in its constructor; it knows who the receiver is, but the remote won't have to.
public class LightOnCommand : ICommand
{
private readonly Light _light;
public LightOnCommand(Light light) => _light = light;
public void Execute() => _light.TurnOn();
public void Undo() => _light.TurnOff();
}
public class TempSetCommand : ICommand
{
private readonly Thermostat _thermostat;
private readonly int _temp;
private int _previousTemp = 20; // Simplified for this example
public TempSetCommand(Thermostat thermostat, int temp)
{
_thermostat = thermostat;
_temp = temp;
}
public void Execute()
{
_thermostat.SetTemperature(_temp);
}
public void Undo() => _thermostat.SetTemperature(_previousTemp);
}
Building a remote that doesn't care what it's controlling
Now I can rewrite the RemoteControl. Instead of hard-coding devices, I'll give it "slots" (like buttons) that hold an ICommand. The remote doesn't know if it's turning on a light, launching a missile, or ordering a pizza—it just calls Execute().
public class RemoteControl
{
private ICommand _currentSlot;
private ICommand _lastCommand;
public void SetCommand(ICommand command)
{
_currentSlot = command;
}
public void PressButton()
{
_currentSlot.Execute();
_lastCommand = _currentSlot;
}
public void PressUndo()
{
_lastCommand?.Undo();
}
}
I love this approach because the RemoteControl is now completely generic. If you want to add a GarageDoorCommand tomorrow, you don't touch a single line of code in the RemoteControl class. You just create the new command class and plug it in at runtime.
Putting it all together
Here is how this looks in action. We instantiate the receivers, wrap them in commands, and hand those commands to the invoker.
var livingRoomLight = new Light();
var nestThermostat = new Thermostat();
var lightOn = new LightOnCommand(livingRoomLight);
var setWarm = new TempSetCommand(nestThermostat, 24);
var remote = new RemoteControl();
remote.SetCommand(lightOn);
remote.PressButton(); // Output: The light is bright!
remote.SetCommand(setWarm);
remote.PressButton(); // Output: Temp set to 24°C
remote.PressUndo(); // Output: Temp set to 20°C
📋 Practical Task
Building a Cinema Mode Macro Command
In the Command Pattern, a "Macro Command" is a command that contains a list of other commands and executes them in sequence. This is perfect for a "Cinema Mode" button that does multiple things at once.
Your Task: Create a class called MacroCommand that implements ICommand. It should take a list of ICommand objects in its constructor. When Execute() is called, it should loop through all the stored commands and call Execute() on each. Additionally, implement Undo() so that it reverses the commands in the opposite order they were executed.
Test your implementation by creating a CinemaMode macro that turns on a light, sets a temperature, and then (if you've added one) closes the blinds. When you call Undo() on the macro, ensure the devices return to their previous states in reverse order.
There are no comments for now.