Expressing OOP Design Patterns in Clojure

In recent years, my work has mainly involved writing Clojure, predominantly in a functional programming style. Object-oriented programming (OOP) design patterns appear to be lightweight in functional languages, and sometimes one might not even be aware that certain practices are design patterns. Let’s discuss how commonly used design patterns might look in Clojure. Due to the different nature of the language, some patterns related to language constructs are not as necessary in Clojure. For instance, I rarely find the need for the iterator pattern when writing Clojure.

Objects are first-class citizens in OOP languages, while functions take that role in functional languages. Consequently, in the following, I might directly use functions to replace objects, such as using a production function instead of a production object in the factory pattern.

Strategy Pattern

Definition: The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern lets the algorithm vary independently from clients that use it.

(defn quack [] (println "quack"))
(defn squeak [] (println "squeak"))

(defn make-sound [duck?]
  (let [sound-f (if duck? quack squeak)]
    (sound-f)))

(make-sound true)

Brief Analysis:

Strategy Pattern: The context object actively specifies which strategy object to use for an action.

State Pattern: The state in the context object determines which state object to use for an operation. The state object, in turn, changes the context object’s state during the operation.

Singleton Pattern

Definition: Ensure a class has only one instance and provide a global point to access it.

(let [conn (delay (create-conn))]
 (defn get-instance [] @conn))
(get-instance)

Factory Pattern

Definition: Define an interface for creating objects but let subclasses alter the type of objects that will be created.

(defn create-adder
  [n]
  (fn [x]
    (+ n x)))

(map (fn [x] (create-adder x))
  (range 10))

Abstract Factory Pattern seems to have little difference from the Factory Pattern in Clojure.

Decorator Pattern

Definition: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.

Clojure’s type inheritance is weak, and functions are still used for implementation.

(defn origin [n] n)

(defn decorator
  "add 5"
  [f]
  (fn [n]
    (+ (f n) 5)))

(def new-f (decorator origin))
(new-f 3)

Brief Analysis:

Decorator: Decorator and the decorated object implement the same interface, enhancing functionality within the same interface.

Adapter: Adapter and the adapted object implement different interfaces. The intention is to change the interface to meet the client’s expectations.

Facade Pattern: Uses one interface to implement a group of interfaces in a subsystem. The intention is to provide a simplified interface for the subsystem.

Observer Pattern

Definition: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

(def subject (atom nil))

(defn subscribe [key ref old-state new-state]
  (print "Current val is " new-state))

(add-watch subject :sub subscribe)
(reset! subject {:foo "bar"})
(remove-watch subject :sub)

Command Pattern

Definition: Encapsulate a request as an object, thereby allowing for parameterization of clients with different requests, queuing of requests, and providing support for undoable operations.

(def command1 (fn [] (print "command1")))
(def command2 (fn [] (print "command2")))

(doseq [cmd [command1 command2]]
  (cmd))

Brief Analysis:

Strategy Pattern: The caller needs to pass consistent parameters.

Command Pattern: Parameters for different commands are encapsulated in command objects, leaving only an execute method.

Adapter Pattern

Definition: Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

(defn add [x y] (+ x y))

(defn adopter [x y]
  (add (Integer/parseInt x) (Integer/parseInt y)))

(adopter "1" "2")

Brief Analysis:

Decorator: Decorator and the decorated object implement the same interface, enhancing functionality within the same interface.

Adapter: Adapter and the adapted object implement different interfaces. The intention is to change the interface to meet the client’s expectations.

Facade Pattern: Uses one interface to implement a group of interfaces in a subsystem. The intention is to provide a simplified interface for the subsystem.

Updated on: Sat February 24, 2024