Skip to content

Instantly share code, notes, and snippets.

@polotek
Created May 28, 2012 07:45
Show Gist options
  • Save polotek/2817894 to your computer and use it in GitHub Desktop.
Save polotek/2817894 to your computer and use it in GitHub Desktop.

This was just a little exercise to explore inheritance vs. composition in javascript. I've been reading and thinking about this topic for a bit and got sick of all the contrived examples. But this opportunity presented itself randomly, so I took advantage. Here's some background.

This is a diff produced by forking @mikeal's couchie library and changing it to be built on top of my little storage lib. For this example I took the approach of having Couchie inherit from the Storage lib (as opposed to using composition). You may want to familiarize yourself with my storage library and @mikeal's couchie library. The repo for the above changes is in a branch called "inherited" on my couchie fork.

Findings

Not significantly shorter - In fact it's just a bit longer because we had to adjust for some differences in the 2 libs when inheriting. We have to override the get method. The other methods of Couchie don't exist on Storage so we couldn't get rid of anything.

In fact the truth of is we also picked up all the storage code. Depending on what you're looking for, this might seem unnecessary. But both libs are really small, especially minified and gzipped. And adding storage gives us some other nice things.

Gained some functionality - By deriving Couchie from Storage, we get all of the underlying functionality of Storage "for free". So we don't have to worry about serializing and unserializing everywhere. We don't have to worry about prefixing everywhere.

Because we're actually inheriting from Storage, we also get a bunch of free methods on Couchie, e.g. has, del, etc. These weren't available on Couchie, and arguably should be.

Unintended freebies - I say "arguably" because depending on the requirements of Couchie, we may not want these extra features. Couchie models a couchdb client. So the Storage methods may not make as much sense in that context. For instance, the original couchie has no way to delete a document. But in the new version, del is freely available.

You may want to avoid exposing these additional features. You could remove these methods from the inherited Storage. But that has other implications. As we'll see below.

Harder to get the api we want - We also have to keep this in mind if we chose to "hide" some methods that we gained from Storage. We could delete the methods we don't strictly need and don't want to be a part of our Couchie api. But we have to be sure that these methods aren't used internally by Storage methods that we want to keep. Otherwise bad things happen. We also have to preserve these methods if we still want to use them inside of Couchie, but don't want them to be public.

Couchie.prototype = new Storage()

// delete methods that we don't want to be public
delete Couchie.prototype.del
delete Couchie.prototype.getKey

Couchie.prototype.clear = function (cb) {
    var key
    if (this.has('_revs')) {
      for (var i in this.revs()) {
        Storage.prototype.del.call(this, i)
      }
      key = Storage.prototype.getKey.call(this, '_revs')
      Storage.prototype.del.call(this, '_revs')
      setTimeout(cb, 0)
    } else {
      setTimeout(cb, 0)
    }
}

Harder to take advantage of all features - Another problem arises if we want to take advantage of the compound key feature of Storage. Because the Couchie methods expect a callback in the last position. We would have to do surgery on our arguments each time in order to pass the variable list of keys to the underlying Storage methods.

Changed some functionality - Something way less obvious that's happening here, is that when we inherit from Storage, we introduce subtle changes to how Couchie works.

The storage lib prefixes keys with this._prefix + '::'. This is a change from the Couchie prefixing. It's doubtful that the "::" will make a difference, but it's something to be aware of. You would not be able to use this lib with data stored by old versions of Couchie. You could fix this, but you'd have to change the storage lib.

Inheritance requires knowledge of implementation - The idea was to preserve the current functionality of Couchie, but take advantage of Storage as an "implementation detail". But in order for Storage to continue to work as expected, we have to know a few things about it's implementation.

Overriding the get method isn't bad. It's just that we want to add some functionality, so we wrap it. This is standard js monkey patching. and doesn't require any knowledge about Storage internals. But overriding get causes problems in other methods of Storage. Specifically the del method uses this.get() internally. But now get refers to the Couchie version. This caused a problem because the Couchie version expects a callback function.

So we have to add a check for a callback, and if there isn't one, fallback to the synchronous version that returns a value. This feels dirty. Couchie isn't synchronous and most of the methods aren't expected to return values. This is a code smell. But there's no help for it with this technique.

Conclusion

This was pretty simple. But not as straight forward as composition. All the Couchie tests passed on my first try with composition. But I got tripped up a couple of times here. It's also not great how the apis get mixed when you inherit, and you have to be conscious of what you do and don't want. There is also much more ambiguity added here with the interplay between methods.

This exercise has made me feel like it's not a great idea to use inheritance between code libraries at all. Unless you fully control the whole inheritance tree, there are too many variables here.

The changes to backward compatibility are also problematic. They are arguably already a problem in Couchie. You can't change the key generation method without breaking back compat. This may be the deal-breaker. Considering the circumstances, I don't think re-using storage was necessary at all for something like Couchie. But still an interesting exercise.

You can checkout the composition-based version over here.

diff --git a/couchie.js b/couchie.js
index 57e5f6e..b48effa 100644
--- a/couchie.js
+++ b/couchie.js
@@ -7,13 +7,15 @@
function Couchie (name) {
if (name.indexOf('__') !== -1) throw new Error('Cannot have double underscores in name')
this.name = name
- this.n = '_couchie__'+name+'__'
+ this._prefix = '_couchie__'+name+'__'
}
+ Couchie.prototype = new Storage()
Couchie.prototype.clear = function (cb) {
- if (localStorage[this.n+'_revs']) {
- for (i in this.revs()) {
- localStorage.removeItem(this.n+i)
+ if (this.has('_revs')) {
+ for (var i in this.revs()) {
+ this.del(i)
}
+ this.del('_revs')
setTimeout(cb, 0)
} else {
setTimeout(cb, 0)
@@ -23,7 +25,7 @@
Couchie.prototype.post = function (obj, cb) {
if (!obj._id || !obj._rev) return cb(new Error('Document does not have _id or _rev.'))
var revs = this.revs()
- localStorage.setItem(this.n+obj._id, JSON.stringify(obj))
+ this.set(obj._id, obj)
revs[obj._id] = obj._rev
this.setrevs(revs)
cb(null)
@@ -33,29 +35,34 @@
for (var i=0;i<docs.length;i++) {
var obj = docs[i]
if (!obj._id || !obj._rev) return cb(new Error('Document does not have _id or _rev.'))
- localStorage.setItem(this.n+obj._id, JSON.stringify(obj))
+ this.set(obj._id, obj)
revs[obj._id] = obj._rev
}
this.setrevs(revs)
cb(null)
}
+ Couchie.prototype._get = Couchie.prototype.get;
Couchie.prototype.get = function (id, cb) {
- var doc = localStorage.getItem(this.n+id)
+ var doc = this._get(id)
+
+ // the Storage lib calls get() without a cb
+ if(typeof cb !== 'function') { return doc; }
+
if (!doc) return cb(new Error('No such doc.'))
- cb(null, JSON.parse(doc))
+ cb(null, doc)
}
Couchie.prototype.all = function (cb) {
var self = this
var revs = self.revs()
- cb(null, Object.keys(revs).map(function (id) {return JSON.parse(localStorage.getItem(self.n+id))}))
+ cb(null, Object.keys(revs).map(function (id) {return self._get(id)}))
}
Couchie.prototype.revs = function () {
- return JSON.parse(localStorage.getItem(this.n+'_revs') || '{}')
+ return this._get('_revs') || {}
}
Couchie.prototype.setrevs = function (obj) {
- localStorage.setItem(this.n+'_revs', JSON.stringify(obj))
+ this.set('_revs', obj)
}
window.couchie = function (name) { return new Couchie(name) }
-}(window));
\ No newline at end of file
+}(window));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment