4
\$\begingroup\$

In continuation with this question,

Animal class has four fields:

  • name

  • sound

  • owner

  • favFood

Cat derived class has one field

  • mode

Derived class Cat is created such that every instance of Cat inherits instance members of Animal. In addition, Cat adds a new instance member mode.

Constructor pattern is used amidst construction and modification of objects.

Criteria is, Animal and Cat class should enforce encapsulation and proper inheritance.

Solution

/*   example.js  */
function Animal() {
    var _name = "";
    var _sound = "";
    var _owner = "";
    var _favFood = "";

    Object.defineProperties(this, {
        'name': {
            get: function () {
                return _name;
            },
            set: function (name) {
                //check conditions before setting the name
                _name = name;
            }
        },
        'owner': {
            get: function () {
                return _owner;
            },
            set: function (ownerName) {
                //check conditions before setting the owner
                _owner = ownerName;
            }
        },
        'sound': {
            get: function () {
                return _sound;
            },
            set: function (sound) {
                //check conditions before setting the sound
                _sound = sound;
            }
        },
        'favFood': {
            get: function () {
                return _favFood;
            },
            set: function (favFood) {
                //check conditions before setting the name favFood
                _favFood = favFood;
            }
        }
    });
}

//Below code is the interface for Animal and its subclass type objects
Animal.prototype.getName    =   function(){ return name;} // calls get property of name
Animal.prototype.setName    =   function(newName){name = newName;} //calls set property of name
Animal.prototype.getOwner   =   function(){ return owner;}
Animal.prototype.setOwner   =   function(newOwner){owner = newOwner;}
Animal.prototype.getSound   =   function(){ return sound;}
Animal.prototype.setSound   =   function(voice){ sound = voice;}
Animal.prototype.getFavFood =   function(){ return favFood;}
Animal.prototype.setFavFood =   function(food){ favFood = food;}


/* dog.__proto__ points to Animal.prototype */  
var animal = new Animal();

animal.setName("Spot");
animal.setOwner("Paul");
animal.setSound();

document.write("Animal properties:" + "<br>");
document.write(animal.getName() + "<br>");
document.write(animal.getOwner() + "<br>");
document.write(animal.getSound() + "<br><br><br>");




/* Cat.__proto__ points to Function.prototype */    
function Cat() {
    /*
        Below line will add definePropertiess name/sound/owner/favFood 
        to an instance i.e., Cat()
    */
    Animal.call(this); 

    var _mode = "";
    Object.defineProperties(this, {
        'mode': {
            get: function () {
                return _mode;
            },
            set: function (newMode) {
                //check conditions before setting the sound
                _mode = newMode;
            }
        }
    });
}

/*
    After executing, below line of code,
    Cat.prototype.__proto__ will point to  Animal.prototype;
*/
Cat.prototype = Object.create(Animal.prototype);

/*
    In the above line, when Cat.prototype.__proto__ points to Animal.prototype,
    Cat.prototype.constructor automatically points to Animal, so this below line    
*/
Cat.prototype.constructor = Cat;

Cat.prototype.getMode   =   function(){ return mode;} 
Cat.prototype.setMode   =   function(Mode){ mode = Mode;}

/* sophie.__proto__ points to Cat.prototype */
var sophie = new Cat();

sophie.setName("Sophie");
sophie.setOwner("Derek");
sophie.setSound("Meow");

document.write("Cat properties:" + "<br>");
document.write(sophie.getName() + "<br />");
document.write(sophie.getOwner() + "<br />");
document.write(sophie.getSound() + "<br />");
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>Constructor pattern</title>
        <script type="text/javascript" src="example.js"></script>
    </head>
    <body>
        <noscript>
            Requires JS
        </noscript>
    </body>
</html>

As per debugging, Animal.call(this) gets invoked on new Cat() instance, private members _name/_owner/_sound/_favFood are created separately for Cat instance.


Using constructor pattern, Does this design enforce encapsulation and inheritance?

Note: Constructor pattern reference

\$\endgroup\$
  • 5
    \$\begingroup\$ I hope you are aware that although JavaScript is an object-oriented language, it isn't a good language for learning about class-based inheritance. \$\endgroup\$ – 200_success Dec 28 '15 at 5:43
  • \$\begingroup\$ @200_success Above code is supposed to follow prototypical inheritance. I see all interfaces are inherited using prototype property. Are you referring to terminology(Animal class) used in this question? \$\endgroup\$ – overexchange Dec 28 '15 at 6:10
  • 2
    \$\begingroup\$ your setXXX and getXXX methods are currently broken as they reference an implicit global object (in this case, window). Your code will break once you have two instances of your class. \$\endgroup\$ – Dan Pantry Dec 28 '15 at 7:22
  • \$\begingroup\$ @DanPantry yes you are right!!! window['name'] = 'Spot' is defined, on calling Animal.prototype.setName('Spot'); \$\endgroup\$ – overexchange Dec 28 '15 at 7:38
  • 1
    \$\begingroup\$ Brittle, brittle. Usual fragile base class issues. For the love of all that is Holy, use mixins and composition instead of this pattern. \$\endgroup\$ – Jared Smith Dec 28 '15 at 16:18
7
\$\begingroup\$

You have the right basic idea, but it feels like you are trying to mold JavaScript into Java. You would write a lot less boilerplate code if you loosened up and embraced the fluidity of JavaScript. If you are using the language support for getters/setters, then all of those getSomeProperty() and setSomeProperty(value) functions are missing the point of the language feature. You should drop them entirely in favour of obj.someProperty for getters and obj.someProperty = value for setters. Write the getters and setters explicitly only when you need to do some unusual processing, such as validation or calculation.

Note that the abstraction is still preserved despite the simplification. The object (such as kitty in the example below) is indifferent to whether the property is just a plain variable or is backed by a getter/setter.

function Animal() {
    this.sound =
    this.owner =
    this.favFood = "";

    var name = "";
    Object.defineProperty(this, 'name', {
        get: function() { return name; },
        set: function(n) {
            if (n == 'Mohammed') {
                throw new Exception('Blasphemous names not allowed');
            }
            name = n;
        },
    });
}

function Cat() {
    // Chain initialization
    Animal.call(this); 

    this.mode = undefined;
}

// Inheritance
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

/* animal.__proto__ points to Animal.prototype */  
var animal = new Animal();
animal.name = "Spot";
animal.owner = "Paul";
animal.sound = undefined;

/* kitty.__proto__ points to Cat.prototype */
var kitty = new Cat();
kitty.name = "Sophie";
kitty.owner = "Derek";
kitty.sound = "Meow";

document.write("Animal properties:" + "<br>");
document.write(animal.name + "<br>");
document.write(animal.owner + "<br>");
document.write(animal.sound + "<br><br><br>");
   
document.write("Cat properties:" + "<br>");
document.write(kitty.name + "<br>");
document.write(kitty.owner + "<br>");
document.write(kitty.sound + "<br><br><br>");

try {
    kitty.name = "Mohammed";
} catch (e) {
    document.write("Blasphemy averted!<br>");
}
document.write("The name again: " + kitty.name + "<br />");
<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Constructor pattern</title></head><body></body></html>

\$\endgroup\$
  • \$\begingroup\$ This is a pretty good answer, although I would have liked to have seen a nod towards ES6 classes and properties given they are The Future (tm) \$\endgroup\$ – Dan Pantry Dec 28 '15 at 7:33
  • \$\begingroup\$ so after Dan pantry's answer, correction is: get: function() { return this.name; } wherever name is referred \$\endgroup\$ – overexchange Dec 28 '15 at 7:56
  • \$\begingroup\$ @overexchange But don't mix up our answers when commenting. My get: function() { return name; } works just as intended. \$\endgroup\$ – 200_success Dec 28 '15 at 8:03
  • \$\begingroup\$ Do you mean, as name is in local scope of function Animal, name does not refer to global context? \$\endgroup\$ – overexchange Dec 28 '15 at 8:06
  • \$\begingroup\$ Yes, that's right. My get: function() { return name; } refers to the var name = ""; that occurs just above it. \$\endgroup\$ – 200_success Dec 28 '15 at 8:08
10
\$\begingroup\$

200's answer is more or less on point and covers things from an idiomatic point of view, so I'd like to just address a bug in your code. It's actually a fairly major one:

Your getXXX/setXXX methods are going to break your code because they reference globals. We'll use this one as an example:

Animal.prototype.getOwner   =   function(){ return owner;}
Animal.prototype.setOwner   =   function(newOwner){owner = newOwner;}

Let's, for example, use this code as a starting point (because it's simpler to put into a REPL):

var animalPrototype = {
  getOwner: function() {
    return owner;
  },
  setOwner: function(newOwner) {
    owner = newOwner;
  }
};
var animal = Object.create(animalPrototype);

This code does practically the same thing you've got above, just without the class nonsense. I'm going to execute all of this in a REPL first of all with strict mode off.

animal.setOwner('foo');
console.log(animal.getOwner());
> foo

This seems good so far. Now, let's create another animal. I'm going to give it a name - Thor. Because that was the name of my dog, and I miss him. Also, because Thor invokes the thought of fury and lightning, which is exactly what the V8 engine is about to do to me.

var thor = Object.create(animalPrototype);
thor.setOwner('loki'); // We all know it is true.
console.log(thor.getOwner());
> loki

Again, all seems fine, until..

console.log(animal.getOwner())
> loki

Crap! Massive bug. What's happened here?

In your getXXX/setXXX methods you're assigning and returning a global. Now, if this was executing in strict mode this should procure a ReferenceError. But because this is not being executed in strict mode, the browser sort of goes with it and in order to make the code work, it will simply assume that when you say owner, you actually mean window.owner (it's quite a bit more complicated than this, but this is the nutshell version).

Congratulations! You've just created a stateful global variable, which is one of the worst evils in the world, right up there next to kicking puppies and depriving people of Fallout 4.

This 'quirk' exists for (unfortunate) legacy reasons with JavaScript and is one of the reasons why we enable strict mode. Enabling strict mode is super simple. The above code sample looks like this:

'use strict';
var animalPrototype = {
  getOwner: function() {
    return owner;
  },
  setOwner: function(newOwner) {
    owner = newOwner;
  }
};
var animal = Object.create(animalPrototype);

Now your code will yell at you before you end up with nasty leakages..

animal.getOwner()
> Uncaught ReferenceError: owner is not defined
animal.setOwner('foo')
> Uncaught ReferenceError: owner is not defined

Oh, and be careful when using strict mode. You don't want to mix strict code with non-strict code, so if you aren't using a bundler like webpack, you'll want to wrap your entire code in an IIFE and then use the 'use strict' directive in there instead. This prevents code that relies on that weird quirk from going bananas. Example:

(function() {
  'use strict';
  var animalPrototype = {
    getOwner: function() { return this.owner; },
    setOwner: function(newOwner) { this.owner = newOwner; }
  }
  var animal = Object.create(animalPrototype);
}());
\$\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.