Idiomatic access to collaborators/services

102 views
Skip to first unread message

Colin Yates

unread,
Feb 25, 2015, 8:22:28 AM2/25/15
to clo...@googlegroups.com
Hi,

I ran into a bit of a brick wall when thinking about how to register to and dispatch to multiple micro-services and it made me realise the underlying tension came from not having peace about a fundamental design decision; how do you access your collaborators. Note: I am specifically talking about 'providers of functionality' as oppose to state. I think everybody agrees that passing state around is a "good thing" (e.g. *db* is so 90s, (defn my-thing-which-needs-a-db [db] ...) is where it is at).

One option is to receive instances of a service (probably some implementation of a defprotocol):

(den my-service [service-1 service-2 payload]
 (some-method service-1)
 (some-method service-2))

The other is to directly reach into the collaborator's namespace:

(den my-service [payload]
 (service-1-ns/some-function)
 (service-2-ns/some-function))

(maybe some config is passed into my-service which the other services use.

The first approach has a much smaller coupling and makes it much easier to reason about. If there is coupling then it is on the protocol of the collaborator. It is therefore trivial to unit-test as you can stub out the collaborators without redef. It also has echoes of OO services, which might be just fine.

The second approach means you don't end up passing collaborators deep down hierarchy graphs (which I haven't run into actually - Clojure tends to have a much 'flatter' graph then Java). It does mean testing etc. requires redefs.

My inclination is to go for the first as it seems simpler, but I still have an allergic reaction to using protocols like this (because of the OO trap). 

This clearly isn't new ground, but I could find surprisingly little blogs and discussion about this. It is also something that only really becomes a problem in the larger scale as well.

What do you all do?

Shantanu Kumar

unread,
Feb 25, 2015, 9:32:03 AM2/25/15
to clo...@googlegroups.com
Having tried few other ways earlier, I now prefer the `first` approach you described. Protocols decouple the contract and the implementation very well, and can be reified for various profiles (unit tests, scenario tests, integration etc.) For constructing the graph I have found Prismatic graph with reifying-functions to be very apt -- you may still need to borrow the lifecycle idea from Stuart Sierra's component (maybe define IStoppable) for things that need shutdown, such as custom thread-pools.

Shantanu

Colin Yates

unread,
Feb 25, 2015, 9:59:10 AM2/25/15
to clo...@googlegroups.com
Thanks Shantanu. I am using component but not graph. Although based on
their schema library and om-tools I expect it to be pretty nifty :).
> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@googlegroups.com
> Note that posts from new members are moderated - please be patient with your
> first post.
> To unsubscribe from this group, send email to
> clojure+u...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en
> ---
> You received this message because you are subscribed to the Google Groups
> "Clojure" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to clojure+u...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

James Reeves

unread,
Feb 25, 2015, 12:45:39 PM2/25/15
to clo...@googlegroups.com
The question you should be asking is: do I need polymorphism?

If the services you're passing in are distinct and not interchangeable, then you can just use functions:

    (defn my-service [cache queue payload]
      (cache/store cache payload)
      (queue/push queue payload))

On the other hand, if the services have some common functionality that can be abstracted, use protocols:

    (defn my-service [listeners payload]
      (doseq [l listeners]
        (listener/deliver l payload)))

It's worth noting that protocols methods and functions have the same syntax, so you can easily convert a function into a protocol.

For instance, let's say you've written the first example using functions:

    (defn my-service [cache queue payload]
      (cache/store cache payload)
      (queue/push queue payload))

But for testing purposes, you want to be able to overload the queue with a stubbed version. Well, that's no problem; you can just change the queue/push function into a method on a protocol. Any code that uses queue/push will act the same, but now we have different behaviour depending on the configuration.

- James

--

Colin Yates

unread,
Feb 25, 2015, 1:00:19 PM2/25/15
to clo...@googlegroups.com
Hi James,

Agreed, and I did feel a little dirty using a protocol simply to
satisfy the swap out for testing (when in fact redef isn't *that*
bad).

I had made a flawed assumption all along that accessing a protocol
method was different than a normal function (I though the 'cache' and
'queue' carried their functions with them so (store cache payload) and
(push queue payload)).

Now I understand they symmetrical it makes a lot more sense, and
provides a lot more freedom.
Reply all
Reply to author
Forward
0 new messages