One common piece of programming advice out you'll get is to prefer for things to be "explicit" rather than "implicit". There are a lot of meanings to this saying, and I don't think it's usually described in much detail, so I'll give you an example:
Most programming languages have functions. In our program, we could call functions themselves "implicit". The explicit version of this (especially in object-oriented languages) might be the "command" pattern.
Why is a function "implicit"?
Functions are built-in to the programming languages we use, so an example in Java might be:
1 2 3 4 5 |
|
Functions are executed by the compiler/interpreter directly, at the location the code was written. If they have side effects, they happen immediately. Based on where the code is in the program, and from where it is invoked - that is when it takes effect.
It's hard to inspect a function, or augment it. Some languages have reflection, but they are hard to work with - you only get some meta-information about the code, and can't augment the inspection mechanism as it's built into the language.
It's hard to audit what happened - there's no record of what functions executed. Once again, since the compiler/interpreter is executing the code for you, you don't get a record of what executed - you have to build some other way of representing this - like logs. But logs generally aren't very inspectable either - usually for human consumption and hard to parse structurally. Not only that, but it's likey hard or at least awkward to access this information from the program itself at runtime.
You usually can't serialize functions. Since functions contain arbitrary code, and generally can have any number of hidden effects or dependencies (unless we're writing Haskell), it's not simple or even possible to serialize functions themselves.
These aren't things we normally think about as "problems". Of course these things are true - we write code and functions to execute something. We aren't generally looking to manipulate the code itself... it can easily get too abstract. However, if you are in the position that you want to do any of these things, you are going to have a hard time using functions directly.
How is the "Command" pattern explicit?
For those not familiar, the "command" pattern is the idea of representing an abstract "command" as an object or datastructure in the code. Something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
We definitely have more code here now, but what do we get from it?
We can inspect the data. If you have a Command
object, you can
call command.name()
and get the name of it. You could even add
more methods to it for other types of data - like a description, or
even a reference to a list of commands that it depends on or
invokes.
We can "evaluate" it 0 to N times. Since it's just a piece of data, we can store it in a variable and decide when or if we want to execute it. Of course, in more functional languages, this is possible with raw functions - but not in Java.
We can store it in a database. Commands now have a concrete representation, and could be stored in a database. It's true that the code within the object itself wouldn't be stored - but as long as we aren't changing the semantics of the effect that is represented by the object, we are fine. We can now look at a list of commands store into the database and understand what happened - we can audit the actions in a meaningful way.
We can "undo" it by augmenting the object. We could define an "undo" method that takes the output and reverses the effect. Of course, this isn't possible with all types of transformations, but it very well might work perfectly for your problem domain. The point is that we can define arbitrary additional functionality and attach it to the command itself.
Easy to test if it's the return value of a function. If you represent the object as some sort of external "effect", returning it from a function instead of invoking that effect directly can give us a huge advantage. By returning the command, we can write a test that simply looks at the returned command and validates whether it makes sense for the given input. If we were to execute the side effect directly from within the tested function, we'd have to look for the visible side-effects elsewhere in the system, or maintain costly mocks.
We can send it over the wire - i.e. it's language agnostic. Since we are defining the operations we can do in terms of data, we can serialize it over the wire and have a completely different programming language carry out the instruction.
It's no wonder that there are many programs that utilize this
pattern - probably the most popular being UIs that desire "undo"
functionality. You can represent the actions possible in your UI as
"commands" that have an effect - and then you simply keep track of a
list of commands that have run. To undo, you either invoke a special
undo()
function on the command, or alternately you could replay all
the previous commands from a known-fresh state.
Code as Data
Now, I will say that there are languages that are considered
homoiconic such as
Clojure. What this means is "code is data"
such that actual code can be "escaped" in a way that prevents it from
being executed immediately, and instead gets stored as a structure
that can be iterated over and manipulated (usually nested lists if the
language is lisp-like). This accomplishes some of the points made
above, but doesn't exactly support augmenting it with arbitrary
additional functionality and metadata. It's much more flexible than
other languages, but still doesn't give you the ultimate freedom that
you get by making these concepts "first class citizens" in the
code. Not only that, but this idea is much more powerful than the
Command
pattern itself - and that's a topic for another post.
Conclusion
Of course, to interpret these command objects that have meta-information, we need to write actual language-specific code to execute them. We can't, and wouldn't want to apply this pattern everywhere - as it adds extra code, and a level of abstraction/indirection into the system. However, this is a powerful pattern to apply to many problem domains. Although it doesn't always work this way - the concept is similar in nature to DSLs in that we have a language embedded in a language. By making this language less powerful than the host language, we restrict the types of incorrect programs we can generate, and overall make the system easier to maintain and reason about.