Clojure Multimethods: The Open Closed Principle

Store sign with Open and Closed

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:

Redis® CLI running PING command

That works fine.

But now, we want to implement an "ECHO" command:

Redis® CLI running 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.

Open-Closed principle cafe sign

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.

Open sign in store

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

Your email address will not be published. Required fields are marked *