Working with composition in JavaScript

As previously explained, JavaScript doesn't provide support for interfaces or multiple inheritance. JavaScript allows you to add properties and methods on the fly; therefore, we might create a function that takes advantage of this possibility to emulate multiple inheritance and generate an object that combines two existing objects, a technique known as mix-in.

However, instead of creating functions to create a mix-in, we will create constructor functions and use compositions to access objects within objects. We want to create an application by taking advantage of the feature provided by JavaScript.

Declaring base constructor functions for composition

The following lines show the code for the ComicCharacter constructor function in JavaScript:

function ComicCharacter(nickName) {
  this.nickName = nickName;
}

The constructor function receives the nickName argument and uses this value to initialize the nickName field.

The following lines show the code for the GameCharacter constructor function in JavaScript:

function GameCharacter(fullName, initialScore, x, y) {
  this.fullName = fullName;
  this.initialScore = initialScore;
  this.x = x;
  this.y = y;
}

The constructor function receives four arguments: fullName, score, x, and y and uses these values to initialize fields with the same name.

The following lines show the code for the Alien constructor function in JavaScript:

function Alien(numberOfEyes) {
  this.numberOfEyes = numberOfEyes;
}

The constructor function receives the numberOfEyes argument and uses this value to initialize the numberOfEyes field.

The following lines show the code for the Wizard constructor function in JavaScript:

function Wizard(spellPower) {
  this.spellPower = spellPower;
}

The constructor function receives the spellPower argument and uses this value to initialize the spellPower field.

The following lines show the code for the Knight constructor function in JavaScript:

function Knight(swordPower, swordHeight) {
  this.swordPower = swordPower;
  this.swordHeight = swordHeight;
}

The constructor function receives two arguments: swordPower and swordHeight. The function uses these values to initialize fields with the same name.

We declared five constructor functions that receive arguments and initialize fields with the same name used for all the arguments. We will use these constructor functions to create instances that we will save within fields of other objects.

Declaring constructor functions that use composition

Now, we will declare a constructor function that saves the ComicCharacter instance in the comicCharacter field. The following lines show the code for the AngryDog constructor function:

function AngryDog(nickName) {
  this.comicCharacter = new ComicCharacter(nickName);
  
  Object.defineProperty(this, "nickName", {
    get: function() {
      return this.comicCharacter.nickName;
    }
  });
  
  this.drawSpeechBalloon = function(message, destination) {
    var composedMessage = "";
    if (destination) {
      composedMessage = destination.nickName + ", " + message;
    } else {
      composedMessage = message;
    }
    console.log(this.nickName + ' -> "' + composedMessage + '"'),
  }
  
  this.drawThoughtBalloon = function(message) {
    console.log(this.nickName + ' ***' + message + '***')
  }
}

The AngryDog constructor function receives nickName as its argument. The function uses this argument to call the ComicCharacter constructor function in order to create an instance and save it in the comicCharacter field.

The preceding code defines a nickName read-only property whose getter function returns the value of the nickName field of the previously created ComicCharacter object, which is accessed through this.comicCharacter.nickName. This way, whenever we create the AngryDog object and retrieve the value of its nickname property, the instance will use the saved ComicCharacter object to return the value of its nickName field.

The AngryDog constructor function declares the drawSpeechBalloon method. This method composes a message based on the value of the message and destination parameters and prints a message in a specific format that includes the nickName value as its prefix. If the destination parameter is specified, the preceding code uses the value of the nickName field or property.

In addition, the constructor function declares the code for the drawThoughtBalloon method. This method also prints a message with the nickName value as its prefix. So, the AngryDog constructor function defines two methods that use the ComicCharacter object to access its nickName field through the nickName property.

Now, we will declare another constructor function that saves the ComicCharacter instance in the comicCharacter field. The following lines show the code for the AngryCat constructor function:

function AngryCat(nickName, age) {
  this.comicCharacter = new ComicCharacter(nickName);
  this.age = age;
  
  Object.defineProperty(this, "nickName", {
    get: function() {
      return this.comicCharacter.nickName;
    }
  });
  
  this.drawSpeechBalloon = function(message, destination) {
    var composedMessage = "";
    if (destination) {
      composedMessage = destination.nickName + ' === ' +
      this.nickName + ' ---> "' + message + '"';
    } else {
      composedMessage = this.nickName + ' -> "';
      if (this.age > 5) {
        composedMessage += "Meow";
      } else {
        composedMessage += "Meeeooow Meeeooow";
      }
      composedMessage += ' ' + message + '"';
    }
    console.log(composedMessage);
  }
  
  this.drawThoughtBalloon = function(message) {
    console.log(this.comicCharacter.nickName + ' ***' + message + '***'),
  }
}

The AngryCat constructor function receives two arguments: nickName and age. This function uses the nickname argument to call the ComicCharacter constructor function in order to create an instance and save it in the comicCharacter field. The code initializes the age field with the value received in the age argument.

As happened in the AngryCat constructor function, the preceding code also defines the nickName read-only property whose getter function returns the value of the nickName field of the previously created ComicCharacter object, which is accessed through this.comicCharacter.nickName. This way, whenever we create an AngryCat object and retrieve the value of its nickname property, the instance will use the saved ComicCharacter object to return the value of its nickName field.

The AngryDog constructor function declares the drawSpeechBalloon method that composes a message based on the value of the age attribute and the values of the message and destination parameters. The drawSpeechBalloon method prints a message in a specific format that includes the nickName value as its prefix. If the destination parameter is specified, the code uses the value of the nickName field or property.

In addition, the constructor function declares the code for the drawThoughtBalloon method. This method also prints a message including the nickName value as its prefix. So, as happened with AngryDog, the AngryCat constructor function defines two methods that use the ComicCharacter object to access its nickName field through the nickName property.

Working with an object composed of many objects

We want the previously coded AngryCat constructor function to be able to work as both the comic and game character. In order to do so, we will add some arguments to the constructor function and save the GameCharacter instance in the gameCharacter field (among other changes). Here is the code for the new AngryCat constructor function:

function AngryCat(nickName, age, fullName, initialScore, x, y) {
  this.comicCharacter = new ComicCharacter(nickName);
  this.gameCharacter = new GameCharacter(fullName, initialScore, x, y);
  this.age = age;

We added the necessary arguments: fullName, initialScore, x, and y to create the GameCharacter object. This way, we have access to the GameCharacter object through this.gameCharacter. The following code adds the same read-only nickName property that we defined in the previous version of the constructor function and four additional properties:

  Object.defineProperty(this, "nickName", {
    get: function() {
      return this.comicCharacter.nickName;
    }
  });
  
  Object.defineProperty(this, "fullName", {
    get: function() {
      return this.gameCharacter.fullName;
    }
  });
  
  Object.defineProperty(this, "score", {
    get: function() {
      return this.gameCharacter.score;
    },
    set: function(val) {
      this.gameCharacter.score = val;
    }
  });
  
  Object.defineProperty(this, "x", {
    get: function() {
      return this.gameCharacter.x;
    },
    set: function(val) {
      this.gameCharacter.x = val;
    }
  });
  
  Object.defineProperty(this, "y", {
    get: function() {
      return this.gameCharacter.y;
    },
    set: function(val) {
      this.gameCharacter.y = val;
    }
  });

The preceding code defines the fullName read-only property whose getter function returns the value of the fullName field of the previously created GameCharacter object, which is accessed through this.gameCharacter.fullName. This way, whenever we create the AngryCat object and retrieve the value of its fullName property, the instance will use the saved GameCharacter object to return the value of its fullName field. The other three new properties (score, x, and y) use the same technique with the difference that they also define setter methods that assign a new value to the field with the same name defined in the GameCharacter object.

In this case, the GameCharacter object uses fields and doesn't define properties with a specific code—such as validations—in the setter method. However, imagine a more complex scenario in which the GameCharacter object requires many validations and defines properties instead of fields. We would be reusing these validations by delegating the getter and setter methods to the GameCharacter object just by reading and writing to its properties.

The following code defines the methods that we defined in the previous version of the constructor function:

this.drawSpeechBalloon = function(message, destination) {
  var composedMessage = "";
  if (destination) {
    composedMessage = destination.nickName + ' === ' +
    this.nickName + ' ---> "' + message + '"';
  } else {
    composedMessage = this.nickName + ' -> "';
    if (this.age > 5) {
      composedMessage += "Meow";
    } else {
      composedMessage += "Meeeooow Meeeooow";
    }
    composedMessage += ' ' + message + '"';
  }
  console.log(composedMessage);
}

this.drawThoughtBalloon = function(message) {
  console.log(this.nickName + ' ***' + message + '***'),
}

The following code declares three methods: draw, move, and isIntersectingWith. These methods access the previously defined properties of fullName, x, and y:

this.draw = function(x, y) {
  this.x = x;
  this.y = y;
  console.log("Drawing AngryCat " + this.fullName +
  " at x: " + this.x +
  ", y: " + this.y);
}

this.move = function(x, y) {
  this.x = x;
  this.y = y;
  console.log("Drawing AngryCat " + this.fullName +
  " at x: " + this.x +
  ", y: " + this.y);
}

this.isIntersectingWith = function(otherCharacter) {
  return ((this.x == otherCharacter.x) &&
  (this.y == otherCharacter.y));
}

Tip

JavaScript allows you to add attributes, properties, and methods to any object at any time. We take advantage of this feature to extend the object created with the AngryCat constructor function to AngryCat + Alien, AngryCat + Wizard, and AngryCat + Knight. We will create and save an instance of the Alien, Wizard, or Knight objects and add the necessary methods and properties to extend our AngryCat object on the fly.

The following code declares the createAlien method that receives the numberOfEyes argument. The method calls the Alien constructor function with the numberOfEyes value received in the argument and saves the Alien instance in the alien field. The next few lines add the numberOfEyes property. This works as a bridge to the this.alien.numberOfEyes attribute. The following code also adds two methods: appear and disappear:

this.createAlien = function(numberOfEyes) {
  this.alien = new Alien(numberOfEyes);
  
  Object.defineProperty(this, "numberOfEyes", {
    get: function() {
      return this.alien.numberOfEyes;
    },
    set: function(val) {
      this.alien.numberOfEyes = val;
    }
  });
  
  this.appear = function() {
    console.log("I'm " + this.fullName +
    " and you can see my " + this.numberOfEyes +
    " eyes.");
  }
  
  this.disappear = function() {
    console.log(this.fullName + " disappears.");
  }
}

The following code declares the createWizard method that receives the spellPower argument. The method calls the Wizard constructor function with the spellPower value received in the argument and saves the Wizard instance in the wizard field. The next few lines add the spellPower property. This works as a bridge to the this.wizard.spellPower attribute. The following code also adds the disappearAlien method that receives the alien argument and uses its numberOfEyes field:

this.createWizard = function(spellPower) {
  this.wizard = new Wizard(spellPower);
  
  Object.defineProperty(this, "spellPower", {
    get: function() {
      return this.wizard.spellPower;
    },
    set: function(val) {
      this.wizard.spellPower = val;
    }
  });
  
  this.disappearAlien = function(alien) {
    console.log(this.fullName + " uses his " +
    this.spellPower + " to make the alien with " +
    alien.numberOfEyes + " eyes disappear.");
  }
}

Finally, the following code declares the createKnight method. This method receives two arguments: swordPower and swordHeight and calls the Knight constructor function with the swordPower and swordHeight values received in all the arguments and saves the Knight instance in the knight field. The next few lines add the swordPower and swordHeight properties that work as a bridge to the this.wizard.swordPower and this.wizard.swordHeight attributes. The following code also adds the unsheathSword method. This method receives the target argument and uses its numberOfEyes field and calls another new method: writeLinesAboutTheSword. Note that with the following lines, we finish the code for the AngryCat constructor function:

  this.createKnight = function(swordPower, swordHeight) {
    this.knight = new Knight(swordPower, swordHeight);
    
    Object.defineProperty(this, "swordPower", {
      get: function() {
        return this.knight.swordPower;
      },
      set: function(val) {
        this.knight.swordPower = val;
      }
    });
    
    Object.defineProperty(this, "swordHeight", {
      get: function() {
        return this.knight.swordHeight;
      },
      set: function(val) {
        this.knight.swordHeight = val;
      }
    });
    
    this.writeLinesAboutTheSword = function() {
      console.log(this.fullName + " unsheaths his sword.");
      console.log("Sword Power: " + this.swordPower +
      ". Sword Weight: " + this.swordWeight);
    };
    
    this.unsheathSword = function(target) {
      this.writeLinesAboutTheSword();
      if (target) {
        console.log("The sword targets an alien with " +
        target.numberOfEyes + " eyes.");
      }
    }
  }
}

Now, we will code three new constructor functions: AngryCatAlien, AngryCatWizard, and AngryCatKnight. These constructor functions allows you to easily create instances of AngryCat + Alien, AngryCat + Wizard, and AngryCat + Knight.

The following lines show the code for the AngryCatAlien constructor function, which receives all the necessary arguments to call the AngryCat constructor function. Then, it calls the createAlien method for the created object. Finally, the following code returns the object after the call to createAlien that added properties and methods:

var AngryCatAlien = function(nickName, age, fullName, initialScore, x, y, numberOfEyes) {
  var alien = new AngryCat(nickName, age, fullName, initialScore, x, y);
  alien.createAlien(numberOfEyes);
  return alien;
}

The following lines show the code for the AngryCatWizard constructor function, which receives all the necessary arguments to call the AngryCat constructor function. Then, it calls the createWizard method for the created object. Finally, the following code returns the object after the call to createWizard that added properties and methods:

var AngryCatWizard = function(nickName, age, fullName, initialScore, x, y, spellPower) {
  var wizard = new AngryCat(nickName, age, fullName, initialScore, x, y);
  wizard.createWizard(spellPower);
  return wizard;
}

The following lines show the code for the AngryCatKnight constructor function. This function receives all the necessary arguments to call the AngryCat constructor function. Then, it calls the createKnight method for the created object. Finally, the following code returns the object after the call to createKnight that added properties and methods:

var AngryCatKnight = function(nickName, age, fullName, initialScore, x, y, swordPower, swordHeight) {
  var knight = new AngryCat(nickName, age, fullName, initialScore, x, y);
  knight.createKnight(swordPower, swordHeight);
  return knight;
}

The following table summarizes the objects that are included in other objects after we create instances with all the different constructor functions:

Constructor function

Includes instances of

AngryDog

ComicCharacter

AngryCat

ComicCharacter and GameCharacter

AngryCatAlien

AngryCat, ComicCharacter, GameCharacter, and Alien

AngryCatWizard

AngryCat, ComicCharacter, GameCharacter, and Wizard

AngryCatKnight

AngryCat, ComicCharacter, GameCharacter, and Knight

Working with instances composed of many objects

Now, we will work with instances created using all the previously declared constructor functions. In the following code, the first two lines create two AngryDog objects named angryDog1 and angryDog2. Then, the code calls the drawSpeechBalloon method for angryDog1 twice with a different number of arguments. The second call to this method passes angryDog2 as the second argument because angryDog2 is an AngryDog object and includes the nickName property:

var angryDog1 = new AngryDog("Brian");
var angryDog2 = new AngryDog("Merlin");

angryDog1.drawSpeechBalloon("Hello, my name is " + angryDog1.nickName);
angryDog1.drawSpeechBalloon("How do you do?", angryDog2);
angryDog2.drawThoughtBalloon("Who are you? I think.");

The following code creates the AngryCat object named angryCat1. Its nickName is Garfield. The next line calls the drawSpeechBalloon method for the new instance to introduce Garfield in the comic character. Then, angryDog1 calls the drawSpeechBalloon method and passes angryCat1 as the destination argument because angryCat1 is the AngryCat object and includes the nickName property. Thus, we can also use AngryCat objects whenever we need the argument that provides the nickName property or field:

var angryCat1 = new AngryCat("Garfield", 10, "Mr. Garfield", 0, 10, 20);
angryCat1.drawSpeechBalloon("Hello, my name is " + angryCat1.nickName);
angryDog1.drawSpeechBalloon("Hello " + angryCat1.NickName, angryCat1);

The following code creates the AngryCatAlien object named alien1. Its nickName is Alien. The next few lines check whether the call to the isIntersectingWith method with angryCat1 as its parameter returns true. The method requires an instance that provides the x and y fields or properties as the argument. We can use angryCat1 as the argument because one of its included objects is ComicCharacter; therefore, it provides the x and y attributes. This method will return true because the x and y properties of both instances have the same value. The line within the if block calls the move method for alien1. Then, the following code also calls the appear method:

var alien1 = AngryCatAlien("Alien", 120, "Mr. Alien", 0, 10, 20, 3);
if (alien1.isIntersectingWith(angryCat1)) {
  alien1.move(angryCat1.x + 20, angryCat1.y + 20);
}
alien1.appear();

The first line in the following code creates the AngryCatWizard object named wizard1. Its nickName is Gandalf. The next lines call the draw method and then the disappearAlien method with alien1 as the parameter. The method requires an instance that provides the numberOfEyes field or property as the argument. We can use alien1 as the argument because one of its included objects is Alien; therefore, it includes the numberOfEyes field or property. Then, a call to the appear method for alien1 makes the alien with three eyes appear again:

var wizard1 = new AngryCatWizard("Gandalf", 75, "Mr. Gandalf", 10000, 30, 40, 100);
wizard1.draw(wizard1.x, wizard1.y);
wizard1.disappearAlien(alien1);
alien1.appear();

The first line in the following code creates the AngryCatKnight object named knight1. Its nickName is Camelot. The next lines call the draw method and then the unsheathSword method with alien1 as the parameter. The method requires an instance that provides the numberOfEyes field or property as the argument. We can use alien1 as the argument because one of its included objects is Alien; therefore, it includes the numberOfEyes attribute:

var knight1 = new AngryCatKnight("Camelot", 35, "Sir Camelot", 5000, 50, 50, 100, 30);
knight1.draw(knight1.x, knight1.y);
knight1.unsheathSword(alien1);

Finally, the following code calls the drawThoughtBalloon and drawSpeechBalloon methods for alien1. We can do this because alien1 is the AngryCatAlien object and includes the methods defined in the AngryCat constructor function. The call to the drawSpeechBalloon method passes knight1 as the destination argument because knight1 is the AngryCatKnight object. Thus, we can also use instances of AngryCatKnight whenever we need an argument that provides the nickName property or field:

alien1.drawThoughtBalloon("I must be friendly or I'm dead...");
alien1.drawSpeechBalloon("Pleased to meet you, Sir.", knight1);

After you execute all the preceding code snippets, you will see the following output on the JavaScript console (see Figure 2):

Brian -> "Hello, my name is Brian"
Brian -> "Merlin, How do you do?"
Merlin ***Who are you? I think.***
Garfield -> "Meow Hello, my name is Garfield"
Brian -> "Garfield, Hello undefined"
Drawing AngryCat Mr. Alien at x: 30, y: 40
I'm Mr. Alien and you can see my 3 eyes.
Drawing AngryCat Mr. Gandalf at x: 30, y: 40
Mr. Gandalf uses his 100 to make the alien with 3 eyes disappear.
I'm Mr. Alien and you can see my 3 eyes.
Drawing AngryCat Sir Camelot at x: 50, y: 50
Sir Camelot unsheaths his sword.
Sword Power: 100. Sword Weight: undefined
The sword targets an alien with 3 eyes.
Alien ***I must be friendly or I'm dead...***
Camelot === Alien ---> "Pleased to meet you, Sir."
Working with instances composed of many objects

Figure 2

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.49.190