This post is part of the series SOLID Wash Tunnel.
Definition
"Command Message pattern is used to invoke functionality provided by other applications. It would typically use Remote Procedure Invocation by taking advantage of Messaging."
- Enterprise Integration Patterns
I want to start by saying that the command message pattern is not only used to invoke functionality from other applications, but also from within the same application. For example a different module, which is still part of the same running process.
One application (or module) constructs a command, which is basically a message or put simplier just an object. The command is sent over to a message bus of some kind. The bus can be an in-memory implementation, or a sepparate process.
An other application (or module) is subscribed to the bus and receives the command, upon which it handles it based on some business logic.
Command Message vs Command
We should not confuse command message pattern with the GoF definition of the command pattern!
In the GoF definition the command itself is responsible to execute an operation. Which is not the case in the command message pattern, where the execution part of it, is deferred to a command handler which is typically in a different application, but may very well be within the same application.
It is important to mention that they do share some similarities.
Stand-alone Objects
A commands is a method call turned into a stand-alone object. This promotes sepparation of concern, loose coupling, and provides us with capabilities like:
- Passing commands as method arguments.
- Storing commands inside other objects (in-memory), files or databases.
- Queueing commands.
- Scheduling commands.
- Command execution interopability via serialization and execution in-or-out of process.
- Command invocation decoupling from the object handling the invocation.
Reversible Operations
The command's execution can be stored for reversing its effects. In our implementation we will not be supporting reversible operations. But, generally speaking, there are two ways to implement it:
- Keeping a history log of the executed commands, and replaying them up to a certain point, which runs untill (but not including) the failed command, so the state of the system gets restored.
- Or when a command fails you can perform logical inverse or compensating operations to restore the state of the system.
Implementation
The command message pattern is used accross the codebase. Lets elaborate one use case, and relate it to the UML diagram above.
ISignal
ISignal
is analogous to Command
. It is an abstraction of any object that is a command.
public interface ISignal
{
}
WashProgramSelectedSignal
WashProgramSelectedSignal
is analogous to ConcreteCommand
. It is an implementation of ISignal
that contains an IWashProgram
that the user has selected. Remember we said that a command is a stand-alone object that contains all information needed to perform an operation. That information in this case is the Program
property.
public class WashProgramSelectedSignal : ISignal
{
public IWashProgram Program { get; }
public WashProgramSelectedSignal(IWashProgram program)
{
Program = program;
}
}
ISignalHandler
ISignalHandler
is analogous to CommandHandler
. It is an abstraction of an object that receives a command and acts upon it. It contains one method Handle(T signal)
, where T
is any object which is an ISignal
.
public interface ISignalHandler<in T> : ISignalHandler
where T : ISignal
{
void Handle(T signal);
}
WashProgramSelectedSignalHandler
WashProgramSelectedSignalHandler
is analogous to ConcreteCommandHandler
. It is an implementation of ISignalHandler<in T>
where T
is WashProgramSelectedSignal
. The handler implements the Handle
method which in our case uses the injected IMemory
and stores the WashProgramSelectedSignal
inside of memory.
The class itself is defined within the WashProgramSelectedSignal
command as a private
class. This is a matter of preference, but I suggest following this approach because it promotes high cohesion.
public class WashProgramSelectedSignal : ISignal
{
...
private class WashProgramSelectedSignalHandler : ISignalHandler<WashProgramSelectedSignal>
{
private readonly IMemory _memory;
public WashProgramSelectedSignalHandler(IMemory memory)
{
_memory = memory;
}
public void Handle(WashProgramSelectedSignal signal)
=> _memory.SetOrOverride("WPSS", signal);
}
}
ISignalTransmitter
ISignalTransmitter
is analogous to Bus
. It is an abstraction of an object that dispatches or transmits a specific command. It contains one method Transmit<T>
, where T
is any object which is an ISignal
.
public interface ISignalTransmitter
{
void Transmit<T>(T signal) where T : ISignal;
}
Motherboard
Motherboard
is analogous to ConcreteBus
. It is an implementation of ISignalTransmitter
. In real life the bus or transmitter would be a motherboard, which is the housing of many electronic components, and represents the bus that those components use to communicate between each other. The communication is represented via signals which are basically commands.
We will elaborate more on the Motherboard
, when we talk about the mediator pattern.
UserPanel
UserPanel
is a class within Module A which uses the ISignalTransmitter
to dispatch a WashProgramSelectedSignal
command.
public class UserPanel :
IUserPanel,
ICustomerInformationCollector,
IWashProcessStarter
{
private readonly ISignalTransmitter _transmitter;
private readonly IWashProgramFactory _programFactory;
public UserPanel(
ISignalTransmitter transmitter,
IWashProgramFactory programFactory)
{
_transmitter = transmitter;
_programFactory = programFactory;
}
...
public ICustomerInformationCollector SelectBuiltInProgram(ProgramType type)
{
IWashProgram program = _programFactory.Create(type);
_transmitter.Transmit(new WashProgramSelectedSignal(program));
return this;
}
...
}
Principles
Principle | Applied | Explanation |
---|---|---|
SRP | ✅ | WashProgramSelectedSignalHandler deals only with handling WashProgramSelectedSignal command. |
OCP | ✅ | Any handler implementing ISignalHandler is closed for modification, but open for extension. Often the handlers are extended to support logging via the decorator pattern. |
LSP | ✅ | Any ISignal can be supplied to the ISignalTransmitter.Transmit<T> method. |
ISP | ✅ | Any of the implementations of ISignalHandler , makes use of all the methods of ISignalHandler . |
DIP | ✅ | Any of the ISignal implementations, does not depend on its handler. Although the handlers are defined within the signal classes themselves, the signals are not logically coupled to them. |
Continue the series on Mediator.
If you found this article helpful please give it a share in your favorite forums 😉.
The solution project is available at GitHub.