a question about Chris Zheng's "Abstract Container Pattern"

322 views
Skip to first unread message

Lawrence Krubner

unread,
Jul 27, 2015, 1:37:41 PM7/27/15
to Clojure
I have a question about this: 

"Servers that are running on a particular port can be tracked and stopped. I have to say, this was the feature that I wanted the most, which motivated the framework's design. The annoying thing about development in emacs is that I have to be careful of not losing the reference to the server. Since there was no way of stopping it unless the repl is restarted. I wanted to implement a registery for references to running servers to be saved."


I have the impression that he's going over the same territory as that covered by Stuart Sierra, though Zheng doesn't mention "Component" nor "Sierra". But he offers this as an example of what he's after: 

(defprotocol IRunnable (start! [system]) (stop! [system]) (restart! [system]) (started? [system]) (stopped? [system]))

That much seems similar to Sierra's system. Zheng seems to add an additional layer by simulating an abstract class above his protocols. As he says: 

  • A single deftype acts as the abstract container, extending one or more protocols
  • A set of methods defined through defmulti that is used within the deftype form act as abstract methods
  • The abstract methods all dispatch on map keys (usually :type).

I am curious if others have found this useful? 

Most of the people who work with Clojure have backgrounds with Object Oriented Programming, so that paradigm sometimes seems natural, or at least familiar. I often think these designs are more verbose than they need to be, but I am curious how others feel. 









Lawrence Krubner

unread,
Jul 27, 2015, 1:58:33 PM7/27/15
to Clojure, lawr...@rollioforce.com


I guess I'm wondering why take Zheng's approach, versus something more direct? I'm especially reacting to this: 

One way to look at design using abstract classes (or any language feature for that matter) is that it is a programming contract that is strictly enforced by the language itself. When we inherit functionality from an abstract class, we make a binding contract with the compiler.

…Essentially, it mimics the structure of how abstract classes are defined and used in object orientated design through simple use of protocols and multimethods.

But why mimic such a contract? Why not just clearly write a contract, using a library that is meant to write contracts? I mean, why not use something like Typed Clojure? 

Colin Yates

unread,
Jul 27, 2015, 2:12:21 PM7/27/15
to clo...@googlegroups.com
I think his last sentence gives you the answer:

"A warm shoutout to Tushar, Lyndon, Dean, Alan, Hank, Derek, and all the guys at clj-melb that gave feedback and helped flesh out this rehash of OO design.” (my emphasis)

He wanted an OO approach and has implemented one; specifically behaviour and state coupled together. I think neither Typed Clojure nor Contracts would have achieved this guy’s goal as they are about enforcing a contract (either the shape of data or effects of a fn) in the ‘functional’ paradigm; this guy clearly wanted something in the OO paradigm. 

Is there a ‘functional’ implementation which gives the same benefits; sure, but that isn’t what he wanted. Are there a bunch of ‘upgrades’ that I am sure we could all apply; sure, but again it seems like he was setting out with a very specific goal in mind and has achieved that. 

--
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.

zcaudate

unread,
Jul 28, 2015, 5:09:22 AM7/28/15
to Clojure, colin...@gmail.com
Hey guys,

Thanks for the feedback and your very insightful comments.

Yep... this is OO alright =)

I realised only after I wrote the article that I was implementing a Lifecycle clone with IRunnable example. However, the concept I am mentioning is much more general than components in terms of its scope:

A similar `abstract class` for a reflective functional dispatch mechanism is defined here:

and extended by the `concrete classes' here:

In the case of iroh... if I had used strictly multimethods, I would have been very confused. If I had used strictly protocols... well I couldn't for a number of reasons.. but if I did, I would have been even more confused because of the number of subconditions that I had to implement. iroh was the first library that I had built that used this pattern and it was so successful that I've been repeating the process over and over again since.

What I wanted to achieve was to have the equivalent of an `abstract class` - the concept of code with abstract functionality that provides a framework for the heavy lifting to be done. `Concrete classes` can just extend aforementioned `abstract class` with minimal code and get all of the benefits. 

I've used this pattern with great success in many, many times and it provides a counter balance to the functional paradigm in terms of packaging up functionality. Clojure doesn't force us into one paradigm or the other and sometimes it is just more stylish to use the OO paradigm. The whole point of OO, multimethods and protocols is to do polymorphic dispatch, which is just a way to break a large cond statement into pieces that can then also be further extended. 

Please also note that not all OO frameworks are equal. Java uses a class inheritence approach whereas javascript uses a prototype model. The `abstract container` pattern that I was describing is probably closer to the JS model but to be honest, I don't really know what it is. Ultimately, it adds a middle tier of functionality in systems that have a plugin type mechanism. I'm sure there are equivalent functional contructs... but that was not the point of the pattern. This pattern has been very useful for me; clojure's protocols and multimethods were not enough to do what I needed to do - but combination of the two works wonders =)

Since the article, the pattern has been codified here:

Hope that helps in clarifying the motivation behind the article and the pattern


Chris

James Reeves

unread,
Jul 28, 2015, 12:04:46 PM7/28/15
to clo...@googlegroups.com, Colin Yates
What are the benefits of designing an "abstract class" in this way, compared to, say, using a protocol and normal functions? Could you provide a small example?

- James

zcaudate

unread,
Jul 28, 2015, 8:07:48 PM7/28/15
to Clojure, colin...@gmail.com, ja...@booleanknot.com
The example in the article is probably the smallest example I can come up with.


The code for the concrete implementations speaks for itself I think, it is about 10 lines to hook in jetty and http-kit to the framework.

if we think about how much code an interface/abstract/concrete design pattern can potentially reduce over a strict interface/concrete design pattern in the java, then it's exactly the same with clojure.

One particular benefit of this particular pattern that I am exploiting is the fact that you can then specify exactly what you want in the config. I'm a big fan of making everything explicitly clear within the config itself so if we have a system with some a component-style dependency injection model (see http://docs.caudate.me/hara/hara-component.html), you can easily do something like this to get a jetty server up:

{:server {:type :jetty
               :port 8080}

and if you want a :http-kit server, you just change the config as such:

{:server {:type :http-kit
               :port 8080}

I've done this quite a bit with mocking... for example, here - https://github.com/MyPost/cassius/blob/master/src/cassius/component.clj

changing

{:db {:type :database}}

to

{:db {:type :mock}}

will work exactly the same way, irrespective of dependency injection framework - in this case, I'm using the stuartsierra/component framework.
....

James Reeves

unread,
Jul 28, 2015, 10:20:17 PM7/28/15
to zcaudate, Clojure, Colin Yates
On 29 July 2015 at 01:07, zcaudate <z...@caudate.me> wrote:
The example in the article is probably the smallest example I can come up with.


The code for the concrete implementations speaks for itself I think, it is about 10 lines to hook in jetty and http-kit to the framework.

I'm afraid I don't see the benefit of having two tiers of abstraction in your example. I think you could achieve the same result with a simpler architecture.

For example, consider a protocol:

  (defprotocol Lifecycle
    (-start [service])
    (-stop [service]))

We can add additional functionality around this simply by wrapping the protocol methods with functions:

  (defn start [service]
    (if (:started? service)
      service
      (-> service -start (assoc :started? true))))

  (defn stop [service]
    (if-not (:started? service)
      service
      (-> service -stop (dissoc :started?))))

So in this case we're adding idempotence and a key to determine whether or not the service has been started. We assume that a service is a record that implements Lifecycle.

We could also conceive of a function that globally registers services for convenience:

  (def running-services (atom #{}))

  (defn register! [service]
    (swap! running-services conj service))

  (defn deregister! [service]
    (swap! running-services disj service))

Then work that into the start and stop functions:

  (defn start [service]
    (if (:started? service)
      service
      (-> service -start (assoc :started? true) (doto register!))))

  (defn stop [service]
    (if-not (:started? service)
      service
      (-> service (doto deregister!) -stop (dissoc :started?))))

Now, if we actually want to create a service for an adapter like Jetty:

  (defrecord JettyService [handler options]
    Lifecycle
    (-start [service]
      (assoc service :instance (run-jetty handler (assoc options :join? false)))
    (-stop [service]
      (.stop (:instance service))
      (dissoc service :instance)))

Unless I've missed something, that seems like broadly equivalent functionality, but with arguably less complexity. We stick with functions for the most part, and only use polymorphism where it's necessary.

- James

Lawrence Krubner

unread,
Jul 29, 2015, 6:44:58 PM7/29/15
to Clojure, colin...@gmail.com, z...@caudate.me
If I understood your original article, you were saying something that amounted to these 3 assertions: 

1.) to future-proof our code against changes, and to avoid being verbose, we need polymorphism.

2.) we need some way to establish constraints (contracts) on that polymorphism, or else it will become difficult to understand, and it might be extended in dangerous and unintended ways. 

3.) the Abstract Container gives us an excellent way to both achieve the polymorphism we are after, while also making clear what the limits on this polymorphism should be. The Abstract Container indicates to future developers how we expect them to extend this code. 

I agree with #1 and #2 but I have my doubts about #3. While that Pattern can clearly work (it's been implemented a million times, so clearly it can be made to work) it strikes me as being more verbose than necessary. I would ask if there is a less verbose way of achieving the goals of #1 and #2?

Also, I was intrigued by this: 

> In the case of iroh... if I had used strictly multimethods, I would have 
> been very confused. If I had used strictly protocols... well I couldn't for 
> a number of reasons.. but if I did, I would have been even more 
> confused because of the number of subconditions that I had to implement. 

This made me curious about iroh, so I went and looked here: 


and I notice this: 

(defn multi-element [m v]
(element {:tag :multi
:name (get-name v)
:array v
:lookup m
:cache (atom {})}))


(defmethod to-element clojure.lang.APersistentMap [m]
(let [v (to-element-array m)]
(multi-element m v)))


(defmethod to-element clojure.lang.APersistentVector [v]
(let [m (reduce
(fn [m ele]
(assoc-in m (to-element-map-path ele) ele))
{} v)]
(multi-element m v)))



It strikes me that you could acheive a high level of polymorphism (satisfying #1) if multi-element was a function that was passed in as an argument to to-element. That would be flexible, although, to satisfy #2, you would want to establish some kind of contract to set some limits on that flexibility. There are 3 ways that I might do this:

a.) I might use run-time checks, such as pre-conditions, on the arguments given to multi-element.

b.) I might use something like Typed Clojure to have compile time warnings regarding the arguments given to multi-element.

c.) I might hard-code multi-element, as you have done, but the map that you've hard-coded could become a Record that I pass into multi-element as an argument, thus making multi-element more polymorphic.

These approaches let me achieve #1 and #2 without the complexity of the Abstract Container Pattern. Have I missed something? If the Abstract Container Pattern was really the the best strategy for iroh, can you make clear why that is? 

zcaudate

unread,
Jul 29, 2015, 10:01:43 PM7/29/15
to Clojure, colin...@gmail.com, lawr...@rollioforce.com

James: For sure. To be honest, I haven’t really thought too much about best practices. The code just naturally evolved in this way. Let me have a think about this over the weekend and come up with something.


Lawrence: I see what you are saying… and again, I need some time to think about the implications. It’s interesting for me to be able look at this in hindsight and really question the value of why I did this in a certain way.. 

As context, I didn’t start with the pattern and tried to fit things to it… I wrote a code bunch of code, refactored it a couple of times, and found commonalities in design that I was able to extract into what is now being discussed. So it’s really a case of tomaYtos, toMAAtos - a question of taste - and I think we are really lucky that clojure allows for all types =)



Reply all
Reply to author
Forward
0 new messages