3
\$\begingroup\$

This question is an improvement I did based on recommendations from these other questions:

Simple object-oriented calculator

Simple object-oriented calculator - follow-up

To those looking this question first I'm trying to find a correct OOP model I can use in JS. I initially found a very different solution from what's normally done and I received lots of help from some of the good guys here.

And I guess I'm finally coming to grips with it. Please take a look.

JSFiddle

 function ViewBase() {

	Object.defineProperty(this, "parse", { 
		value: function() {
			for(prop in this) 
				this[_.camelCase(prop)] = $('#' + this[prop]);
		},
		writable: true,
		enumerable: false
	       
	});

};

// View class mirroring html components
function ViewCalc () {

	this.IPT_X = 'x';
	this.IPT_Y = 'y';
	this.IPT_RES = 'res';
	this.BTN_SUM =  'sum';
	this.BTN_SUBTRACT =  'subt';
	this.BTN_MULTIPLY =  'mult';
	this.BTN_DIVISION =  'div';
	this.BTN_CLEAN =  'clean';
	this.BTN_RAND =  'rand';
	this.parse();

};
ViewCalc.prototype = new ViewBase();

function Operands() {

    // connect view to the base business class
    this.view = new ViewCalc();

    //public
    this.x = 0;
    this.y = 0;

    //public
    this.showOperands = function() {

        //use of a private property (IPT_yyX) and a public property (this.x)
        this.view.iptX.val(this.x);
        this.view.iptY.val(this.y);
    };

    this.clean = function() {
        this.x = 0;
        this.y = 0;

        // call to a local public method 
        this.showOperands();
    };

    this.updateOperands = function(x, y) {
        // use of a public property
        this.x = x;
        this.y = y;
    };

    this.clean();

};

function Randomizer() {

    // private
    function getRandomNumber() {
        return Math.round(Math.random() * 1000);
    };

    this.updateOperands = function(x, y) {
        // call to superior class's method
        Randomizer.prototype.updateOperands.call(this, x, y);
        // call to method of superior object
        this.showOperands();
    };

    this.populateRandomNumbers = function() {
        // call to public local method (this.updateOperands())
        // and to a local private method (getRandomNumber()))
        this.updateOperands(getRandomNumber(), getRandomNumber());
    };

    // init
    this.populateRandomNumbers();

};
Randomizer.prototype = new Operands();

function Operations() {
    //public
    this.sum = function() {
        // call to 2 local private methods
        showRes(doSum());
    };

    this.subtract = function() {
        showRes(doSubtraction());
    };

    this.multiply = function() {
        showRes(doMultiplication());
    };

    this.division = function() {
        showRes(doDivision());
    }; 

    var self = this;

    // private
    function doSum() {
        return self.x + self.y;
    };

    function doSubtraction() {
        return self.x - self.y;
    };

    function doMultiplication() {
        return self.x * self.y;
    };
 
    function doDivision() {
        return self.x / self.y;
    };

    function showRes(val) {
        self.view.iptRes.val(val);
    };

    // init
    this.view.btnSum.on('click', function() { self.sum() });
    this.view.btnSubtract.on('click', function() { self.subtract() });
    this.view.btnMultiply.on('click', function() { self.multiply() });
    this.view.btnDivision.on('click', function() { self.division() });   
    this.view.btnClean.on('click', function() { self.clean() });
    this.view.btnRand.on('click', function() { self.populateRandomNumbers()     });

};
Operations.prototype = new Randomizer();

var o = new Operations();
<html>
<body>
X: <input id='x'>
<br>
Y: <input id='y'>
<br>
Res: <input id='res'>
<br>
<input id='sum' type='button' value='+'>
<input id='subt' type='button' value='-'>
<input id='mult' type='button' value='*'>
<input id='div' type='button' value='/'>
<input id='clean' type='button' value='C'>
<input id='rand' type='button' value='Rand'>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>

What I did this time:

From @tkellehe's and @SirPython's answers:

  • I'm realizing that using this self variable is causing more harm than does good. So I reverted to this, new and prototype usage.

From @tkellehe's answer:

  • I remove all _vars from parameters, all private vars will use it from now on. Just there aren't any at the example at the moment.
  • I used Object.define in the View classes so I don't need to use Object.keys anymore.
  • Because I reverted to this, new and prototype usage, override function doesn't work/it's not needed anymore. So I had to use Class.prototype.method.call instead and I don't need a base class anymore or a salvage to not using new. At least at this point.
  • I know loading within context is important and I'll use it. I just don't think it's needed for this example. But that library loading function was really cool.

What still bothers me:

  • I don't like using Class.prototype.method.call. It's too long and too clumsy. Is there a better solution for calling the superior method? Can you make override function work again in this format?
  • In the ViewCall and ViewBase classes, it would be better if I could put this.parse() in the ViewBase class in such a manner that it get called in the moment ViewClass was instatiated. But I couldn't find a way to do it. Does anyone have an idea on how I could do this?

Edit

I improved the code again and created another question (the final one):

Simple object-oriented calculator - part 4

\$\endgroup\$
3
\$\begingroup\$

Current Design

The implementation you have here uses less code than before and has readability for those who are accustomed to JavaScript. The simplest way to add readability, say for you blog, would be to add helper functions. But, here is where you must be careful, because you may add too many to the point where you create your own syntax. You can start to see that from what I made below pertaining to the superioritize function where it presents its own mini API for interfacing with the chain of overridden methods.


My Attempt

I tried to pull everything together and have some OOP looking concepts present.


Inherit

The first is inherit_to_from:

function inherit_to_from(child, parent) {
     child.prototype = new parent();
     return child;
}

I know what you are thinking... He is a genius! (JK)

The point of this function was merely to make it easier for when doing inheritance that it looks somewhat straight forward. Also, made the child first to look "closer" to what _C++ does. When I use it I place it before the declaration of the child that way it looks "closer" to C++.

function Parent() {};

inherit_to_from(Child, Parent);
function Child() {};

Overriding

For the overriding, I left the Base class such that every class used will have the same basic functions.

// To make things easier added this function.
function is_function(obj) { return obj instanceof Function };

function Base() {};

// Inheritable property 'override' used to handle overriding methods.
Base.prototype.override = function(method, body) {
    // Made it a little bullet proof.
    if(is_function(this[method]) && is_function(body)) {
        var overridden = this[method];
        body.superior  = overridden;
        this[method]   = body;
    }
    return this;
};

// Inheritable property 'def_prop' used to add some OOP.
// This way instances can go " instance.def_prop(...) "
// rather than " Object.defineProperty(instance, ...) "
Base.prototype.def_prop = function(property, descriptor) {
    Object.defineProperty(this, property, descriptor);
    return this;
};

Now the tricky part was when you override the method, the context of the superior method becomes the method that had overridden it.

 var base = new Base();
 base.foo = function() { return this };
 base.override("foo", base.foo);
 console.log(base.foo.superior() === base);     // => false
 console.log(base.foo.superior() === base.foo); // => true

So, I created this strange fix which probably is not the best take but it keeps somewhat of the original design.

// A helper function for determining if an object is SuperiorChainer (Not bullet proof...)
function is_SuperiorChainer(obj) { return is_function(obj) && _SuperiorChainer.prototype === obj.prototype };

// Idea would be to make this private...
function _SuperiorChainer(context, propertyObject) {
    var chain = function() {
        return propertyObject.apply(context, arguments);
    };

    // Used to help with verification to whether it is a _SuperiorChainer.
    chain.prototype = _SuperiorChainer.prototype;

    Object.defineProperty(chain, "context", { value: context });
    // Gets the real object... Not the best name...
    Object.defineProperty(chain, "real", { value: propertyObject });

    // Creates the links to the chain.
    Object.defineProperty(chain, "superior", 
        propertyObject.superior === undefined ? 
        { value: undefined } : 
        { 
            get: function() {
                return _SuperiorChainer(context, propertyObject.superior);
            }
        }
    );

    return chain;
}

// The public function that starts the chain. (What an amazing name... *sarcasm*)
function superioritize(context, propertyName) {
    if(is_function(context[propertyName]))
    return _SuperiorChainer(context, context[propertyName]);
}

The use is a little strange, but it works...

 var base = new Base();
 base.foo = function() { return this };
 base.override("foo", base.foo);
 console.log(superioritize(base, "foo").superior() === base);     // => true
 console.log(superioritize(base, "foo").superior() === base.foo); // => false

What you can do is edit the override function connected to Base to make it create the chain similar to what superioritize does.

EDIT

So, after revisiting this... I realized I could have just used bind to handle this problem... So, the new override function would look like this:

// Inheritable property 'override' used to handle overriding methods.
Base.prototype.override = function(method, body) {
    // Made it a little bullet proof.
    if(is_function(this[method]) && is_function(body)) {
        var overridden = this[method];
        body.superior  = overridden.bind(this);
        this[method]   = body;
    }
    return this;
};

And it looks like it did before!!!

 var base = new Base();
 base.foo = function() { return this };
 base.override("foo", base.foo);
 console.log(base.foo.superior() === base);     // => true
 console.log(base.foo.superior() === base.foo); // => false

ViewBase

For this I created a unique function that will automatically define the camel case property and create the JQuery object.

inherit_to_from(ViewBase, Base);
function ViewBase() {};

ViewBase.prototype.def_view_prop = function(property, id) {
    this[property]              = id;
    this[_.camelCase(property)] = $("#" + id);
    return this;
};

ViewCalc

Did not really have to do anything for this except use the def_view_prop function. I did not attach the properties to the prototype. So, you cannot inherit from ViewCalc and have those properties be unique. Essentially, all objects of the inherited class would have the same values as to when the inheritance was made (an instance of ViewCalc was placed into the prototype so anything going down the prototype chain would get the same values).

inherit_to_from(ViewCalc, ViewBase);
function ViewCalc() {
    this.def_view_prop("IPT_X",        'x');
    this.def_view_prop("IPT_Y",        'y');
    this.def_view_prop("IPT_RES",      'res');
    this.def_view_prop("BTN_SUM",      'sum');
    this.def_view_prop("BTN_SUBTRACT", 'subt');
    this.def_view_prop("BTN_MULTIPLY", 'mult');
    this.def_view_prop("BTN_DIVISION", 'div');
    this.def_view_prop("BTN_CLEAN",    'clean');
    this.def_view_prop("BTN_RAND",     'rand');
};

NOTE: I probably should have added another function of some sort that would update the JQuery objects in case they do not get loaded correctly or the HTML was to be dynamically created later (or something weird like that). But, instead what I did was just place the script at the end of the HTML file to make sure all of the HTML elements had been created before the JQuery objects attempted to be created.


Operands & Randomizer

For Operands I added two smart getters and setters x and y. These allow you to directly set x and y which will automatically set the input. Also, this allows you to always get the most recent values in the inputs.

inherit_to_from(Operands, Base);
function Operands() {};

// Because this is in the prototype, all share the same ViewCalc.
Operands.prototype.def_prop("view", { value: new ViewCalc() });

Operands.prototype.def_prop("x", { 
    get: function() { return +this.view.iptX.val() },
    set: function(v) { this.view.iptX.val(v) },
    enumerable: true
});

Operands.prototype.def_prop("y", { 
    get: function() { return +this.view.iptY.val() },
    set: function(v) { this.view.iptY.val(v) },
    enumerable: true
});

Operands.prototype.clean = function() {
    this.x = 0;
    this.y = 0;
    return this;
};

inherit_to_from(Randomizer, Operands);
function Randomizer() {};

Randomizer.prototype.def_prop("randomize", { value: function() {
    this.x = Math.round(Math.random() * 1000);
    this.y = Math.round(Math.random() * 1000);
    return this;
}});

Operations

For Operations I merely copied and pasted what you had then made some minor changes.

inherit_to_from(Operations, Randomizer);
function Operations() {
    var self = this;

    function _doSum() {
        return self.x + self.y;
    };

    function _doSubtraction() {
        return self.x - self.y;
    };

    function _doMultiplication() {
        return self.x * self.y;
    };

    function _doDivision() {
        return self.x / self.y;
    };

    function _showRes(val) {
        self.view.iptRes.val(val);
    };

    self.sum = function() {
        _showRes(_doSum());
    };

    self.subtract = function() {
        _showRes(_doSubtraction());
    };

    self.multiply = function() {
        _showRes(_doMultiplication());
    };

    self.division = function() {
        _showRes(_doDivision());
    }; 

    self.override("clean", function() {
        superioritize(self, "clean").superior();
        self.view.iptRes.val("");
    });

    self.view.btnSum.on     ('click', self.sum);
    self.view.btnSubtract.on('click', self.subtract);
    self.view.btnMultiply.on('click', self.multiply);
    self.view.btnDivision.on('click', self.division);   
    self.view.btnClean.on   ('click', self.clean);
    self.view.btnRand.on    ('click', function() { self.randomize() });
};

Code Inaction

function is_function(obj) { return obj instanceof Function };

function inherit_to_from(child, parent) {
    child.prototype = new parent();
    return child;
};

function _is_SuperiorChainer(obj) { return is_function(obj) && _SuperiorChainer.prototype === obj.prototype }

function _SuperiorChainer(context, propertyObject) {
    var chain = function() {
        return propertyObject.apply(context, arguments);
    };

    chain.prototype = _SuperiorChainer.prototype;

    Object.defineProperty(chain, "context", { value: context });
    Object.defineProperty(chain, "real", { value: propertyObject });

    Object.defineProperty(chain, "superior", 
        propertyObject.superior === undefined ? 
        { value: undefined } : 
        { 
            get: function() {
                return _SuperiorChainer(context, propertyObject.superior);
            }
        }
    );

    return chain;
}

function superioritize(context, propertyName) {
    if(is_function(context[propertyName]))
    return _SuperiorChainer(context, context[propertyName]);
}

function Base() {}

Base.prototype.override = function(method, body) {
    if(is_function(this[method]) && is_function(body)) {
        var overridden = this[method];
        body.superior  = overridden;
        this[method]   = body;
    }
    return this;
};

Base.prototype.def_prop = function(property, descriptor) {
    Object.defineProperty(this, property, descriptor);
    return this;
};

inherit_to_from(ViewBase, Base);
function ViewBase() {};

ViewBase.prototype.def_view_prop = function(property, id) {
    this[property]              = id;
    this[_.camelCase(property)] = $("#" + id);
    return this;
};

inherit_to_from(ViewCalc, ViewBase);
function ViewCalc() {
    this.def_view_prop("IPT_X",        'x');
    this.def_view_prop("IPT_Y",        'y');
    this.def_view_prop("IPT_RES",      'res');
    this.def_view_prop("BTN_SUM",      'sum');
    this.def_view_prop("BTN_SUBTRACT", 'subt');
    this.def_view_prop("BTN_MULTIPLY", 'mult');
    this.def_view_prop("BTN_DIVISION", 'div');
    this.def_view_prop("BTN_CLEAN",    'clean');
    this.def_view_prop("BTN_RAND",     'rand');
};

inherit_to_from(Operands, Base);
function Operands() {};

// Because this is in the prototype all have the same ViewCalc.
Operands.prototype.def_prop("view", { value: new ViewCalc() });

Operands.prototype.def_prop("x", { 
    get: function() { return +this.view.iptX.val() },
    set: function(v) { this.view.iptX.val(v) },
    enumerable: true
});

Operands.prototype.def_prop("y", { 
    get: function() { return +this.view.iptY.val() },
    set: function(v) { this.view.iptY.val(v) },
    enumerable: true
});

Operands.prototype.clean = function() {
    this.x = 0;
    this.y = 0;
    return this;
};

inherit_to_from(Randomizer, Operands);
function Randomizer() {};

Randomizer.prototype.def_prop("randomize", { value: function() {
    this.x = Math.round(Math.random() * 1000);
    this.y = Math.round(Math.random() * 1000);
    return this;
}});

inherit_to_from(Operations, Randomizer);
function Operations() {
    var self = this;

    function _doSum() {
        return self.x + self.y;
    };

    function _doSubtraction() {
        return self.x - self.y;
    };

    function _doMultiplication() {
        return self.x * self.y;
    };
 
    function _doDivision() {
        return self.x / self.y;
    };
    
    function _showRes(val) {
        self.view.iptRes.val(val);
    };
    
    self.sum = function() {
        _showRes(_doSum());
    };

    self.subtract = function() {
        _showRes(_doSubtraction());
    };

    self.multiply = function() {
        _showRes(_doMultiplication());
    };

    self.division = function() {
        _showRes(_doDivision());
    }; 

    self.override("clean", function() {
        superioritize(self, "clean").superior();
        self.view.iptRes.val("");
    });

    self.view.btnSum.on     ('click', self.sum);
    self.view.btnSubtract.on('click', self.subtract);
    self.view.btnMultiply.on('click', self.multiply);
    self.view.btnDivision.on('click', self.division);   
    self.view.btnClean.on   ('click', self.clean);
    self.view.btnRand.on    ('click', function() { self.randomize() });
};

var o = new Operations();
<script src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
<script src = "https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>

<body>
X: <input id='x'>
<br>
Y: <input id='y'>
<br>
Res: <input id='res'>
<br>
<input id='sum' type='button' value='+'>
<input id='subt' type='button' value='-'>
<input id='mult' type='button' value='*'>
<input id='div' type='button' value='/'>
<input id='clean' type='button' value='C'>
<input id='rand' type='button' value='Rand'>
</body>

<!-- <script src = "test.js"></script> -->

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.