Bertrand Meyer’s Open Closed Principle states that adding new behavior shouldn’t change existing code.
It should only extend code.
Clojure has a great form for that:
The Multimethod
For example, here’s a data store similar to Redis®.
You can run commands like:
That works fine.
But now, we want to implement an "ECHO"
command:
And this shouldn’t modify existing code.
There shouldn’t be any regression.
Here’s the existing code:
(defn output [comand input & _]
(cond
(= "PING" command)
"PONG"))
Code language: Clojure (clojure)
The naïve way is adding another conditional:
(defn output [comand input & _]
(cond
(= "PING" command)
"PONG"
(= "ECHO" command)
(nth input 0)))
Code language: Clojure (clojure)
…but this breaks the Open Closed principle.
If you want to add a new command, you have to change the output
function, not extend it.
This example is so simple it doesn’t matter.
But there could be dozens of commands, each with dozens of lines of code.
We don’t want to modify the main function to add a command.
Instead, we define a multimethod:
(defmulti output (fn [command & _]
(lower-case command)))
(defmethod output "ping" [_ input & _]
"PONG")
Code language: Clojure (clojure)
This supports the "ping"
command, so let’s add the "echo"
command on lines 10-11:
;; Only one defmulti
(defmulti output (fn [command & _]
(lower-case command)))
;; A defmethod for each command
(defmethod output "ping" [_ input & _]
"PONG")
;; The command -----V
(defmethod output "echo" [_ input & _]
(nth input 0 ""))
Code language: JavaScript (javascript)
Each defmethod
is a possible method it can use.
It decides which to use, based on the first argument.
For example, this:
;; This --v "echo" is the dispatch value
(output "echo" ["foo"])
Code language: Clojure (clojure)
…will use this defmethod:
;; This "echo" -----v means it will call this method
(defmethod output "echo" [_ input & _]
(nth input 0 ""))
Code language: Clojure (clojure)
…because its first argument is "echo"
defmulti
sets how this dispatches:
(defmulti output (fn [command & _]
(lower-case command)))
Code language: Clojure (clojure)
The function’s return value of (lower-case command)
means it will dispatch on the lower-case version of command
.
And in defmethod
, the dispatch value goes after the method name:
;; dispatch value --v
(defmethod output "echo" [_ input & _]
(nth input 0 ""))
Code language: Clojure (clojure)
So when the dispatch value is "echo"
, it calls this method for output
.
When the dispatch value is something else, it calls another method if there is one.
So adding the "echo"
command changes nothing from the "ping"
command.
It doesn’t add a conditional, like the example above.
We added a feature, without changing any code.
Leave a Reply