Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?

Here is my general opinion from a PLT perspective:

##Faking Ad-Hoc Polymorphism

JavaScript does not support Ad-Hoc Polymorphism (One method name with multiple implementations based on parameters). As a result, many developers (including jQuery authors) try to fake it by using double-dispatch, or an inlined Great Maze of If-Else-dom/Switch-dom. To clarify what that is in the abstract here is an example of it (in TypeScript for clarity):

abstract class Shape extends Object {
    abstract onCollisionWith(shape: Shape): void
}

class Circle extends Shape {
    constructor(
        public cx: number,
        public cy: number,
        public r: number
    ) { super() }

    // Great Maze of If-Else-dom
    onCollisionWith(shape: Shape) {
        if (shape instanceof Circle)
            alert("Circle-Circle collision")
        else if (shape instanceof Square)
            alert("Square-Square collision")
        else if (shape instanceof Rect)
            alert("Circle-Rect collision")
        else
            alert("Circle-Shape collision")
    }
}

class Square extends Shape {
    constructor(
        public cx: number,
        public cy: number,
        public size: number
    ) { super() }

    // Great Maze of Switch-dom
    onCollisionWith(shape: Shape) {
        switch (shape.constructor.name) {
            case "Circle":
                alert("Square-Circle collision")
            break;
            case "Square":
                alert("Square-Square collision")
            break;
            case "Rect":
                alert("Square-Rect collision")
            break;
            default:
                alert("Square-Shape collision")
        }
    }
}

class Rect extends Shape {
    constructor(
        public cx: number,
        public cy: number,
        public width: number,
        public height: number
    ) { super() }

    // Great Maze of If-Else-dom + double-dispatch
    onCollisionWith(shape: Shape) {
        if (shape instanceof Circle)
            this._onCircleCollision(shape)
        else if (shape instanceof Square)
            this._onSquareCollision(shape)
        else if (shape instanceof Rect)
            this._onRectCollision(shape)
        else
            this._onShapeCollision(shape)
    }
    
    private _onCircleCollision(circle: Circle) {
        alert("Rect-Circle collision")
    }

    private _onSquareCollision(square: Square) {
        alert("Rect-Square collision")
    }

    private _onRectCollision(rect: Rect) {
        alert("Rect-Rect collision")
    }

    private _onShapeCollision(shape: Shape) {
        alert("Rect-Shape collision")
    }
}

The key thing you'll notice in these "workaround" approaches is the amount of code spent just trying to find the part (The Great Maze) that actually does useful work (the alert). Not to mention that in a more real-world example, much of the "useful work" is probably duplicated along with the "maze". Imagine the maintenance and evolutionary problems with this code as well.

One way to avoid this issue is to push the condition logic to the caller and to expose the implementation more directly:

abstract class Shape extends Object {
    abstract onShapeCollision(shape: Shape): void
    abstract onCircleCollision(circle: Circle): void
    abstract onSquareCollision(square: Square): void
    abstract onRectCollision(rect: Rect): void   
}

class Circle extends Shape {
    constructor(
        public cx: number,
        public cy: number,
        public r: number
    ) { super() }

    onCircleCollision(circle: Circle) {
        alert("Circle-Circle collision")
    }

    onSquareCollision(square: Square) {
        alert("Circle-Square collision")
    }

    onRectCollision(rect: Rect) {
        alert("Circle-Rect collision")
    }

    onShapeCollision(shape: Shape) {
        alert("Circle-Shape collision")
    }
}

class Square extends Shape {
    constructor(
        public cx: number,
        public cy: number,
        public size: number
    ) { super() }

    onCircleCollision(circle: Circle) {
        alert("Square-Circle collision")
    }

    onSquareCollision(square: Square) {
        alert("Square-Square collision")
    }

    onRectCollision(rect: Rect) {
        alert("Square-Rect collision")
    }

    onShapeCollision(shape: Shape) {
        alert("Square-Shape collision")
    }
}

class Rect extends Shape {
    constructor(
        public cx: number,
        public cy: number,
        public width: number,
        public height: number
    ) { super() }

    onCircleCollision(circle: Circle) {
        alert("Rect-Circle collision")
    }

    onSquareCollision(square: Square) {
        alert("Rect-Square collision")
    }

    onRectCollision(rect: Rect) {
        alert("Rect-Rect collision")
    }

    onShapeCollision(shape: Shape) {
        alert("Rect-other collision")
    }
}

The issue now is that while the code is far simpler for you as a library author, it is now more complicated for the user:

//before
myShape.onCollisionWith(anotherShape)

//after
if (anotherShape instanceof Circle)
	myShape.onCircleCollision(anotherShape)
else if(anotherShape instanceof Square)
	myShape.onSquareCollision(anotherShape)
else if (anotherShape instanceof Rect)
	myShape.onRectCollision(anotherShape)
else
	myShape.onShapeCollision(anotherShape)

Another issue with the first example and in the one above: Let's say I want to add a Triangle class, I now have to update every object in the my library or else every single switch/if statement depending on the approach I used. In other words: Every shape has to be aware of every other related shape...

So now the challenge is, how to provide the author with the usability of a single method while avoiding The Great Maze and maintability problems when we're restricted to Single-Dispatch in JavaScript...

##Multiple Paradigm Pain

JavaScript is a multi-paradigm language. It supports: Functional, Procedural, Object Oriented (Prototypical), Imperative, and others. Not all of these paradigms are compatible with each other and choosing to use the wrong combination leads to cognitive dissonance and impedance mismatch.

jQuery in particular utilizes all of these in places which require extra code to make up for the dissonance, or causes one to overlook other problems as it can distract from higher level issues. The issue you pointed to is one example of this with all of the code duplication made invisible due to helper methods (the function is duplicated with the only difference being the name of the type)

##Sound Architecture is key

Safe upon the solid rock the ugly houses stand:

Come and see my shining palace built upon the sand!

-- Edna St. Vincent-Millay, Second Fig

Since jQuery is closest to an Object Oriented architecture, I would suggest they actually commit to it properly and eliminate/reduce the contradictory paradigms. By doing so, many problems become clearer and a methodology for refactoring and extension become clear.

Now the climax: A solution to the problems above. By choosing OOP, the answer to our dilemma of duplicate code, extraneous conditionals, and maintainability is straightforward: Subtype Polymorphism. With this approach, the implementation is different based on the current type (this) instead of different based on the parameters. With implementation inheritance as well, the duplication is also eliminated:

abstract class Shape extends Object {
    onCollision(shape: Shape) {
        //common implementation
        alert(`${this.constructor.name}-${shape.constructor.name}`)
    }
}

class Circle extends Shape {
    constructor(
        public cx: number,
        public cy: number,
        public r: number
    ) { super() }

    onCollision(shape: Shape) {
        //...Circle specific code...
        super.onCollision(shape)
    }
}

class Square extends Shape {
    constructor(
        public cx: number,
        public cy: number,
        public size: number
    ) { super() }

    onCollision(shape: Shape) {
        //...Square specific code...
        super.onCollision(shape)
    }
}

class Rect extends Shape {
    constructor(
        public cx: number,
        public cy: number,
        public width: number,
        public height: number
    ) { super() }

    onCollision(shape: Shape) {
        //...Rect specific code...
        super.onCollision(shape)
    }
}

var c = new Circle(10, 5, 3),
    r = new Rect(16, 33, 12, 4)

c.onCollision(r) //Circle-Rect

Adding a Triangle is now so obvious that an example is not even needed. Every Shape is aware of its immediate parent and the parent is unaware of its children. Loose coupling has also been achieved

#Summary

So this response may not be as straightforward in answering your request for feedback, but I hope it answers more questions in general.

@david-mark

This comment has been minimized.

Copy link

david-mark commented Dec 11, 2016

jQuery is hell and gone from any sort of architecture like this. They need to start by avoiding ill-advised "overloading" and the crutch methods (e.g. isPlainObject, isWindow), etc. that come with it.

It's about plug-ins, not "subclassing". Quotes indicate there are no classes in ECMAScript (just as there is no real concept of "overloading" in a loosely typed language), though TypeScript provides for such semantics. I prefer to implement such schemes in pure ECMAScript, despite a few warts in associated support functions (e.g. inherit in My Library). YMMD.

@mlhaufe

This comment has been minimized.

Copy link
Owner Author

mlhaufe commented Dec 11, 2016

#1 - ECMAScript 6 supports classes: Ref. Though it is still prototypical inheritance.

#2 - I'm not sure what you mean by "plug-ins"

#3 - Being "loosely typed" does not preclude overloading. It's simply syntactic sugar. Mozilla has a non-standard implementation of JavaScript which supports a form of this: Conditional Catch Clauses Also, you can see an example in Lua

I'd be curious to see what Jessie/MyLibrary looks like after you migrate to ES6 and modules.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.