It's also really neat.
First let's look at a very simple situation where we may need something like this.
Let's say we have two types of objects, a glass bottle and I don't know, a wooden box. If we drop the glass bottle it shatters instantly and is gone. The box however can be dropped a few times before breaking.
Let's see how two simple versions of these classes may be set up.
class Bottle { bool _broken = false; public void Break() { if (_broken) { Console.WriteLine("The bottle is already broken"); } else { _broken = true; Console.WriteLine("The bottle shatters"); } } } class Box { bool _broken = false; int _hitPoints = 3; public void Hit() { if (_broken) { Console.WriteLine("The box is broken irreparably.") } else { _hitPoints--; if (_hitPoints <= 0) { _broken = true; Console.WriteLine("The box breaks.") } } } }
The methods Hit() and Break() are clearly fairly different from one another other besides the fact that they are both void and parameterless and they are both probably going to called during very similar situations.
So how would a client class have to deal with this? We can set up a very simple situation to demonstrate the problems that arise.
class Program { static void Main(string[] args) { Bottle bottle = new Bottle(); Box box = new Box(); string input = ""; while (input.ToLower() != "q") { Console.WriteLine( "Press B to drop the (B)ottle, O to drop the B(o)x or Q to (Q)uit."); input = Console.ReadLine(); if (input.ToLower() == "b") { bottle.Break(); } else if (input.ToLower() == "o") { box.Drop(); } } } }
It works now but this clearly will not do.
The above code is pretty bad on a number of levels. For starters the more objects added that can be dropped, the bulkier and just awful our client class becomes.
Secondly the client class (in this over simplified case it's the Program class) has too many responsibilities: accepting and interpreting user input and responding to the input (that problem specifically I'm not actually going over in this article but it's something to think about). A general design guideline is: one class, one responsibility. Moreover, in some cases our client class may not even have access to Bottles and Boxes as they may be hidden.
The Command pattern helps by introducing a new type of object called a command object which will contain a single method, Execute().
It's not an easy pattern to grasp at first (it wasn't for me anyway) but it's usefulness became apparent once I did.
Before we dive into the code, let's look at a real world analogy that mirrors how the command pattern works.
In a cafe we can can order many different types of beverages and each have a different procedure for making it. We give our drink order to the chashier who writes down the specifics of the drink we want. The slip encapsulates our drink order. It is then passed off to the barrista who interprets what's written on the slip and makes your drink.
You, the customer, know how to order a drink so you create a command (your order) and pass it off to the chashier. The chashier in turn takes the command you created and passes it to the barrista who makes the drink. Most real life cafe cashiers, of course, must also need to know how to prepare a drink, but for our purposes they do not and have completely separate responsibilities.
Now let's look at the various pieces of the pattern and see how they fit into the cafe scenario.
The Command pattern has a client class which is responsible for creating our command objects (the customer), the client then passes the command to the invoker. The invoker contains one or more references to a command objects which the client sets. Now the invoker can invoke one of these commands at any time, which is like having the barrista make the drinks. Since the invoker can call these methods at any time, we can even set up ques of commands to be executed in a certain order.
Moving back to our Box/Bottle issue, let's take a look at some code now to hopefully help gel the whole idea.
Starting with our command interface and objects:
interface ICommand { void Execute(); } class BottleDropCommand : ICommand { Bottle _bottle; public BottleDropCommand(Bottle bottle) { _bottle = bottle; } public void Execute() { _bottle.Break(); } } class BoxDropCommand : ICommand { Box _box; public BoxDropCommand(Box box) { _box = box; } public void Execute() { _box.Drop(); } }
Now we need our invoker, a sort of item dropping interface to work with. This will hold two ICommand objects that we can execute at any time.
class SimpleItemDropper { //We have two ICommand's at the moment but this can be extended //deal with any amount. ICommand _commandOne; ICommand _commandTwo public void SetCommandOne(ICommand command) { _commandOne = command; } public void SetCommandTwo(ICommand command) { _commandTwo = cammand; } //The item dropper doesn't care if it's a box or a bottle, it just //executes the set command! public void ExecuteOne() { _commandOne.Execute(); } public void ExecuteTwo() { _commandTwo.Execute(); } }
Now our client class becomes:
class Program { static void Main(string[] args) { Bottle bottle = new Bottle(); Box box = new Box(); SimpleItemDropper itemDropper = new SimpleItemDropper(); ICommand dropBottle = new BottleDropCommand(bottle); ICommand dropBox = new BoxDropCommand(box); itemDropper.SetCommandOne(dropBottle); itemDropper.SetCommandTwo(dropBox); itemDropper.ExecuteOne(); itemDropper.ExecuteOne(); itemDropper.ExecuteTwo(); itemDropper.ExecuteTwo(); itemDropper.ExecuteTwo(); itemDropper.ExecuteTwo(); itemDropper.ExecuteTwo(); Console.ReadLine(); } }Our output should look something like this: And that's the Command pattern, a great way to * encapsulate method invocation * separate the invocation of a method from the object receiving the invocation request. There are many more great uses of this pattern as it can do some very clever things. I suggest investigating further and more importantly experimenting with the pattern yourself. This is a very simple (and useless) example and the pattern can be tweaked and extended to your needs (just like any pattern!) but I've successfully conveyed how powerful this pattern is. Thanks for reading!
No comments :
Post a Comment