Smart Views: Thoughts from the Community

68 views
Skip to first unread message

JPatrick Davenport

unread,
May 19, 2015, 6:01:24 PM5/19/15
to comp...@googlegroups.com
Hello,
I'm starting to write my first real web app in Compojure/Hiccup. One thing I've toyed with is having the widgets of the view call the domain layer directly. The goal is to decouple the controller from having to provide the view with the view's data dependencies. 

I've structured this using the Component lib. At boot a middleware wrapper assoc'es to the request the domain layer (which comes from a prior component). Each widget takes the request map. As a result each view may call domain layer protocols if necessary.

What do you all think of this approach? I've done something like this in Spring MVC.

Thanks,
JPD

James Reeves

unread,
May 19, 2015, 7:37:13 PM5/19/15
to Compojure
In general, idiomatic Clojure tends to aim for the least complex solution, where complexity is a measure of interdependency.

So an immutable data structure is the ideal, since being a constant, it cannot be affected by anything else. Pure functions are next best thing, since their output is only affected by their input, and it's usually recommended to maximise their use where possible.

Side-effectful functions, particularly those that access I/O, are the most complex, since anything can potentially affect their operation. If our aim is to reduce complexity, we want to limit how these functions connect with the rest of our code.

If you're giving a view function all the information in the request map, along with direct connections to databases and other I/O sources, then you're effectively maximising complexity. Anything can affect the output of the function, making it unpredictable, difficult to reason about, and difficult to test. I'd suggest this isn't the route you want to be going down.

- James

--
You received this message because you are subscribed to the Google Groups "Compojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to compojure+...@googlegroups.com.
To post to this group, send email to comp...@googlegroups.com.
Visit this group at http://groups.google.com/group/compojure.
For more options, visit https://groups.google.com/d/optout.

JPatrick Davenport

unread,
May 20, 2015, 7:59:47 AM5/20/15
to comp...@googlegroups.com, ja...@booleanknot.com
So then what is the best practice?

Sections of my view are just lists in the Hiccup sense. As such they are but defs. Others are dynamic. They might need a list of states for a drop down, they might need a to get some text dynamically. Of course the route could get the data for the view and pass it as arguments (view-index list-of-states list-of-positions company-name). But now I've got a really tightly coupled route + view.

If instead the request had a key called :domain that was a map of {:protocol-name protocol-instance}, the view would know to get the right domain protocol for whatever it needed (I wasn't going to allow direct DB access in either the view or route. Everything would be at least veneered by the protocol; allowing for testing and all that). This does make it a bit more difficult to test because the exact contract to the view is probably a :keys destructuring. So I have to jump through some hoops to know what protocol goes to what key. I do though get a simplified interaction between the route and the view.

Has anyone implemented a Pet Store (http://www.oracle.com/technetwork/java/petstore1-3-1-02-139690.html) version with Compojure? I've never seen a production ready piece of software with Compojure that was not a toy. I'm sure they exist. How else could there be a group for Compojure et .al without people doing real work with? I just cannot find a way to tier the application. Many articles have direct DB SQL calls occurring in functions. There are global variables for the DB pool. None of that seems correct to me. 

To continue on, since I hope to get some sound development guidelines, I thought perhaps to structure the views as closures. A constructor function would return another function, let's call it R. R's params would be whatever R needed to operate minimally. For example, R needs to display a user's detail. R would only need to get the user ID, (R id). Internally, R would have access to the UserProtocol's (find-by-id id). So the route would get the id value from the request, pop it into the instance of R that it has and R would go to town.

Again, this is more complex since R does not appear to be pure. But R itself is actually pure. The impure operation is in the protocol instance. R's constructor method could very well get a protocol that always returns the same map no matter what the input value. I can reason about R within the context of R and its dependency contracts. 

The closures feel closer to Lisp/Clojure way. Is this closer to the Compojure way?

James Reeves

unread,
May 21, 2015, 10:15:17 AM5/21/15
to Compojure
If I were writing it, I'd first start by creating a protocol of all the database functions I'd need.

    (defprotocol Database
      (list-states [db])
      (list-positions [db])
      (get-company-name [db]))

I'd then extend by database component record with this protocol:

    (extend-type DatabaseComponent
      Database
      (list-states [{:keys [conn]} ...)
      (list-positions [{:keys [conn]} ...)
      (get-company-name [{:keys [conn]}])

The main reason for using a protocol, and not just writing the functions directly, is that it makes it easier to create a mock database for unit testing purposes.

Next I'd create a function to just pull all the relevant data into a map:

    (defn fetch-index-data [db-component]
      {:states (list-states db-component)
       :positions (list-positions db-component)
       :company-name (get-company-name db-component)})

Then a view to receive this information:

    (defn view-index [{:keys [states positions company-name]}]
      (html ...))

This distinction between fetching the data and viewing it helps define an exact boundary between I/O and processing the resulting data. We can see at a glance what data a view needs, rather than the I/O functions being buried in the view code. And it's also easier to test, of course.

Rather that pass the components via the request map, I'd use a closure instead:

    (defn endpoint [{:keys [db]}]
      (routes
       (GET "/" [] (view-index (fetch-index-data db)))
       ...))

This is the approach I use in Duct, a template/micro-framework for structuring component-based web apps. Not only is it a little faster (because we only have to destructure the component once), in my view it's a little cleaner as well.

- James
Reply all
Reply to author
Forward
0 new messages