I'm writing a JS webapp client. User can edit list/tree of text items (say, a todo list or notes). I manipulate DOM with jQuery a lot.
User can navigate the list up and down using keyboard (similar to J/K keys in GMail), and perform several other operations. Many of these operations have mirror "up" / "down" functionality, e.g.
$.fn.moveItemUp = function() {
var prev = this.getPreviousItem();
prev && this.insertBefore(prev);
// there's a bit more code in here, but the idea is pretty simple,
// i.e. move the item up if there's a previous item in the list
}
$.fn.moveItemDown = function() {
var next = this.getNextItem();
next && this.insertAfter(next);
// ....
}
Now this pattern of having two almost identical functions is repeated in several places in my code because there are many operations on the list/tree of items that are pretty much symmetrical.
QUESTION: How do I refactor this elegantly to avoid code duplication?
The trivial way I came up with so far is to use .apply()...
$.fn.moveItem = function(direction) {
var up = direction === 'up',
sibling = up ? this.getPreviousItem() : this.getNextItem(),
func = up ? $.fn.insertBefore : $.fn.insertAfter;
// ...
if (! sibling) { return false; }
func.apply(this, [ sibling ]);
// ...
};
The benefit is easier maintenance when structure of other elements of the code requires changing moveUp/moveDown. I've already needed to slightly change the code multiple times, and I always need to keep in mind that I need to do it in two places...
But I'm not happy with the "unified" version, because:
How do you solve those "almost identical code" situations when working with DOM or similar structure?
One thing that you can do is to use closures to hide the configuration parameter passing from the user:
var mk_mover = function(direction){
return function(){
//stuff
};
};
var move_up = mk_mover("up");
var move_down = mk_mover("down");
If you want to continue in this direction, it becomes basically a matter of deciding what is the best way to pass the parameters to the common implementation, and there is no single best solution for this.
One possible direction to go is to use an OO style approach, passing a configuration "strategy" object to the implementation function.
var mk_mover = function(args){
var get_item = args.getItem,
ins_next = args.insNext;
return function(){
var next = get_item.call(this);
next && ins_next.call(this, next);
};
};
var move_up = mk_mover({
get_item : function(){ return this.getPrevious(); },
get_next : function(x){ return this.insertBefore(x); }
});
var move_down = mk_mover({/**/});
I prefer doing this when the strategy interface (the methods that differ between directions) is small and relatively constant and when you want to open up the possibility of adding new kinds of direction in the future.
I also tend to use this when I know that neither the set of directions nor the kinds of methods will need to change much, since JS has better support for OO then for switch statements.
The other possible direction to go is the enum approach that you used:
var mk_mover = function(direction){
return function(){
var next = (
direction === 'up' ? this.getNextItem() :
direction === 'down' ? this.getPreviousItem() :
null );
if(next){
if(direction === 'up'){
this.insertAfter(next);
}else if(direction === 'down'){
this.insertBefore(next);
}
}
//...
};
}
Sometimes you can use neat objects or arrays instead of switch statements to make things prettier:
var something = ({
up : 17,
down: 42
}[direction]);
While I have to confess that this is kind of clumsy, it has the advantage that if your set of directions is fixed early on, you now have lots of flexibility to add a new if-else or switch statement whenever you need to, in a self-contained manner (without needing to go add some methods to the strategy object somewhere else...)
BTW, I think that the approach you suggested sits in an uncomfortable middle ground between the two versions I suggested.
If all you are doing are switch inside your function depending on the value direction tag passed, its better to just do the switches directly in the spots you need to without having to refactor things into separate functions and then having to do lots of annoying .call and .apply stuff.
On the other hand, if you went through the trouble of defining and separating the functions you can make it so that you just receive them directly from the caller (in the form of a strategy object) instead of doing the dispatching by hand yourself.