Model-View-Controller, a classic architectural pattern in Clojure
I'm sure for some of you Model-View-Controller (MVC) resembles more of an anti-pattern than something to reach for when writing Clojure, but I think it's a great example of what architecture should accomplish, especially what can be accomplished in Clojure.
MVC decouples state, state changes, and state usage. One way to compose this architecture uses three classic software design patterns: Strategy as our controller to update the model, observer to notify the view and controller, and composite as our view hierarchy.
If we want to do this in Clojure, we can use the Clojure design pattern analogs. Let's use a higher-order functions for our strategy; a watch for our observer (watching an atom or ref); and seesaw components for our composite, we can build ourselves a simple desktop application: an adder.
The Controller
Since for the purposes of this example we'll pretend we're purists, our strategy will be a couple higher-order functions (no protocols). So let's create a couple delegate functions to update our model.
;; Higher-order functions as our "strategy delegates".
(defn change-display
"Takes an atom `model` and a string `input`, and updates
`model` by appending `input` to the currently displayed
text in our calculator."
[model input]
(swap! model update :display #(apply str (concat % input))))
(defn solve
[model]
(swap! model update :display #(apply + (mapv (fn [s]
(Integer/parseInt s))
(string/split % #"\+")))))
I understand in the land of YAGNI, this seems more like set dressing with code complexity than something useful, but it has a usefulness in a bigger code base as we're about to see how it decouples state changes from the using the state.
The View
I never wrote about the composite pattern in Clojure (not yet anyway, subscribe for design patterns part 3). To summarize, the composite pattern creates an interface for building hierachies with the intent that children in the hierarchy will follow the interface and propagate changes down the tree. We can do this pretty easy in Clojure using higher-order functions (again) or protocols, but for now we'll rely on Seesaw, a wrapper around Java's Swing components.
;; A very simple view hierarchy using a few seesaw components.
(defn create-view-children
[display state change-fn solve-fn]
(let [display-label (s/label :text display)
plus-button (s/button :text "+"
:listen [:action (fn [e]
(change-fn state "+"))])
equal-button (s/button :text "="
:listen [:action (fn [e]
(solve-fn state))])
keypad-buttons (mapv (fn [n]
(s/button :text (str n)
:listen [:action (fn [e]
(change-display state (str n)))]))
(range 9))
keypad (s/grid-panel :rows 3
:columns 3
:items keypad-buttons)
panel (s/grid-panel :rows 1
:columns 2
:items [plus-button equal-button])]
(s/border-panel
:items [[panel :south] [display-label :north] [keypad :center]])))
(defn create-view
[children]
(s/frame :title "Positive Integer Adder"
:visible? true
:content children
:on-close :exit
:width 400
:height 300))
For our view, we use map
to programmatically create our number buttons in the hierarchy as well as directly creating plus and equals buttons. Storing them in a couple panels to describe the layout of the adder's UI. We will pass these components to our view factory function create-view
which will set up the Swing frame for us. Now all we need to do is update our view everytime the model's state changes to update our display-label
.
The Model
Lastly, we see our model and it's observer are pretty simple. Just an atom and a watch. I added the state-logger function to help debug the output, but we could add any number of responses to the state changes here without affecting the view (unless, of course, we removed or changed update-view).
;; We can add many "observers" to change other view state/composition
;; or even change the strategy used by the buttons to add the numbers now
;; that we've decoupled all three.
(defn update-view
[view state]
(fn [_ _ _ new-state]
(let [display (:display new-state)]
(s/config! view :content (create-view-children display
state
change-display
solve)))))
(defn state-logger
[_ _ _ new-state]
(println "State changed to: " new-state))
(defn -main
[& args]
(let [;; Our model, diligently keeping track of our state
state (atom {:display ""})
;; The root of our view hierachy
view (create-view (create-view-children 0 state change-display solve))]
;; Observe
(add-watch state :update-view (update-view view state))
(add-watch state :logger state-logger)
;; Start the app
(s/native!)
(s/invoke-now (fn [] (s/show! (s/pack! view))))))
As the comments suggest, we can even change the controller or view we want to use. Then, we put it all together in -main
, creating our model (atom), view (seesaw), and start watching the show unfold.
Putting it all together
I'm guessing you've already put all the code examples together, but let me help you out by doing just that.
(ns janetacarr.mvc-adder
(:require [seesaw.core :as s]
[clojure.string :as string])
(:gen-class))
;; Higher-order functions as our "strategy delegates".
(defn change-display
"Takes an atom `model` and a string `input`, and updates
`model` by appending `input` to the currently displayed
text in our calculator."
[model input]
(swap! model update :display #(apply str (concat % input))))
(defn solve
[model]
(swap! model update :display #(apply + (mapv (fn [s]
(Integer/parseInt s))
(string/split % #"\+")))))
;; A very simple view hierarchy using a few seesaw components.
(defn create-view-children
[display state change-fn solve-fn]
(let [display-label (s/label :text display)
plus-button (s/button :text "+"
:listen [:action (fn [e]
(change-fn state "+"))])
equal-button (s/button :text "="
:listen [:action (fn [e]
(solve-fn state))])
keypad-buttons (mapv (fn [n]
(s/button :text (str n)
:listen [:action (fn [e]
(change-display state (str n)))]))
(range 9))
keypad (s/grid-panel :rows 3
:columns 3
:items keypad-buttons)
panel (s/grid-panel :rows 1
:columns 2
:items [plus-button equal-button])]
(s/border-panel
:items [[panel :south] [display-label :north] [keypad :center]])))
(defn create-view
[children]
(s/frame :title "Positive Integer Adder"
:visible? true
:content children
:on-close :exit
:width 400
:height 300))
;; We can add many "observers" to change other view state/composition
;; or even change the strategy used by the buttons to add the numbers now
;; that we've decoupled all three.
(defn update-view
[view state]
(fn [_ _ _ new-state]
(let [display (:display new-state)]
(s/config! view :content (create-view-children display
state
change-display
solve)))))
(defn state-logger
[_ _ _ new-state]
(println "State changed to: " new-state))
(defn -main
[& args]
(let [;; Our model, diligently keeping track of our state
state (atom {:display ""})
;; The root of our view hierachy
view (create-view (create-view-children 0 state change-display solve))]
;; Observe
(add-watch state :update-view (update-view view state))
(add-watch state :logger state-logger)
;; Start the app
(s/native!)
(s/invoke-now (fn [] (s/show! (s/pack! view))))))
If we run this code with our Clojure CLI, we'll see this window pop up:
Hitting equals at this point will call our solve
strategy, updating the model to contain the string "22" and, of course, the display in the application will also show 22 instead of 11+11.
Conclusion
I understand coding an MVC app under one hundred lines of code seems a bit trite, but we can bridge the mental gap here to something like Rails or other web frameworks by replacing our seesaw components with serving HTML fairly easily. Don't be so quick to dismiss the potential of a little state and some higher-order functions.