08 Deep Understanding of Inheritance Delegation and Composition

08 Deep Understanding of Inheritance Delegation and Composition #

Hello, I am Ishikawa.

When it comes to object-oriented programming, one of the most famous books is Design Patterns: Elements of Reusable Object-Oriented Software, written by the Gang of Four (GoF). This book provides a total of 23 different design patterns, but today we won’t go into detail about them. Instead, we will focus on one core concept of object-oriented programming, which is composition over inheritance.

In the JavaScript community, there has been a lot of debate about inheritance and composition. Regardless of whether it is inheritance or composition, we should not forget to think critically. The essence of critical thinking is not criticism itself, but rather the ability to make judgments through deep thinking about core issues.

Therefore, whether it is inheritance or composition, they are just methods or approaches. The core problem they aim to solve is how to make code more reusable.

Image

Next, let’s follow this line of thinking and explore the methods used in JavaScript to solve the problem of code reuse. We will also examine the problems each method solves and the issues it may cause. This way, in real-world business scenarios, we will know how to judge and choose the most suitable solution.

Inheritance #

In traditional OOP, we often mention inheritance and polymorphism. Inheritance is used to create a subclass based on a superclass, inheriting the attributes and methods of the superclass. Polymorphism allows us to call the constructor of the parent class within the subclass and override the methods in the parent class.

Now let’s take a look at how to do inheritance using constructor functions in JavaScript.

How to reuse through inheritance and polymorphism? #

In fact, starting from ES6, we can use the extends keyword to do inheritance. Here is an example:

class Widget {
  appName = "Core Widget";
  getName() {
    return this.appName;
  }
}

class Calendar extends Widget {}

var calendar = new Calendar();
console.log(calendar.hasOwnProperty("appName")); // returns true
console.log(calendar.getName()); // returns "Core Widget"

calendar.appName = "Calendar App";
console.log(typeof calendar.getName); // returns function
console.log(calendar.getName()); // returns "Calendar App"

Next, let’s look at polymorphism. Starting from ES6, we can use super to call the constructor of the parent class within the constructor of the subclass and override the attributes in the parent class. In the following example, you can see that we use super to change the appName attribute of Calendar from “Core Widget” to “Calendar App”.

class Widget {
  constructor() {
    this.appName = "Core Widget";
  }
  
  getName() {
    return this.appName;
  }
}

class Calendar extends Widget {
  constructor(){
    super();
    this.appName = "Calendar App";
  }
}

var calendar = new Calendar();
console.log(calendar.hasOwnProperty("appName")); // returns true
console.log(calendar.getName()); // returns "Calendar App"
console.log(typeof calendar.getName); // returns function
console.log(calendar.getName()); // returns "Calendar App"

In some practical examples, such as in third-party libraries like React, we often see examples of inheritance, such as creating a subclass WelcomeMessage by inheriting React.Component.

class WelcomeMessage extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

Authorization #

After discussing inheritance, let’s take a look at the delegation method.

What is delegation? Let me give you an analogy. Delegation here does not mean a leader (parent class) authorizing a subordinate (child class), but rather an individual object authorizing a platform or another person to work together on something.

For example, when I collaborate with Geek Time, my personal energy and professional abilities only allow me to focus on creating good content. However, I don’t have the energy and experience to handle editing, post-production, and promotion, etc., so I delegate those tasks to the teachers at Geek Time. In this process, I only concentrate on creating content.

How to achieve reuse through delegation? #

In the previous examples, combining what we mentioned in Lesson 1 about prototype-based inheritance, we found that whether it’s using function constructors or the syntactic sugar for classes in JavaScript to simulate general object-oriented languages like Java with classes and inheritance, it can be counterintuitive for some developers. It requires a lot of mental conversions to transform the underlying logic of JavaScript into the actual implementation.

So is there a way to make the code more intuitive? This approach is actually more intuitive to use the prototype itself for delegation. Starting from ES5, JavaScript has supported the method Object.create(). Let’s take a look at an example:

var Widget = {
  setCity: function (City) { this.city = City; },
  outputCity: function () { return this.city; }
};

var Weather = Object.create(Widget);

Weather.setWeather = function (City, Temperature) {
  this.setCity(City);
  this.temperature = Temperature;
};

Weather.outputWeather = function () {
  console.log(this.outputCity() + ", " + this.temperature);
};

var weatherApp1 = Object.create(Weather);
var weatherApp2 = Object.create(Weather);

weatherApp1.setWeather("Beijing", "26 degrees");
weatherApp2.setWeather("Nanjing", "28 degrees");

weatherApp1.outputWeather(); // Beijing, 26 degrees
weatherApp2.outputWeather(); // Nanjing, 28 degrees

As you can see, we created the Weather object for weather forecasting and delegated it to Widget, allowing Widget to assist Weather in setting and returning the city with authorization. In this case, the Widget object is more like a platform that empowers Weather after being authorized. The Weather object can then focus on implementing its own properties and methods, and generate instances like weatherApp1 and weatherApp2.

Of course, some developers may not find the class approach counterintuitive. Delegation can also be achieved using classes. For example, if we want to add counting functionality to the set mentioned in the previous lesson, we can achieve it by inheriting from Set. But we can also do the opposite, by delegating some functionality to Map and focus on implementing interfaces similar to Set.

class SetLikeMap {
  // Initialize the dictionary
  constructor() { this.map = new Map(); }
  // Custom set interfaces
  count(key) { /*...*/ }
  add(key) { /*...*/ }
  delete(key) { /*...*/ }
  // Iterate and return the keys in the dictionary
  [Symbol.iterator]() { return this.map.keys(); }
  // Delegate some functionality to the dictionary
  keys() { return this.map.keys(); }
  values() { return this.map.values(); }
  entries() { return this.map.entries(); }
}

Combination #

After talking about composition, let’s take a look at combination. Of course, the authorization we mentioned earlier is actually a kind of combination in a broad sense. However, this combination is more like “the cooperation between individuals and platforms”, while another combination is more like “the cooperation within a team”. It also has many applications and implementation methods, let’s learn about it.

Image

How to achieve reuse through borrowing? #

In JavaScript, functions have built-in apply and call methods. We can “borrow” a function’s functionality through apply or call. This method is also called implicit mixin. For example, in an array, there is a native slice method, and we can borrow this native method using call.

In the code example below, we are borrowing this functionality and using the function’s arguments as an array to slice.

function argumentSlice() {
    var args = [].slice.call(arguments, 1, 3);
    return args;
}
// example
argumentSlice(1, 2, 3, 4, 5, 6); // returns [2,3]

How to achieve reuse through copying? #

Apart from borrowing, what other combination methods can we use instead of inheritance? This brings us to “copying”. As the name suggests, it is copying other people’s properties and methods to our own. This method is also called explicit mixin.

Before ES6, people used to “steal” methods in a sneaky way. After ES6, JavaScript introduced the Object.assign() method, which allows us to legitimately “assign” an object’s “traits and abilities” to another object.

Now, let’s first see how JavaScript achieves copying in a legitimate way after ES6.

Firstly, using the assign() method of an object, we can assign the properties of the Widget object to the calendar object. Of course, in the calendar object, we can also have its own properties. Similar to borrowing, neither borrowing nor assigning creates a prototype chain. The code below demonstrates this:

var widget = {
  appName : "Core Widget"
}

var calendar = Object.assign({
  appVersion: "1.0.9"
}, widget);

console.log(calendar.hasOwnProperty("appName")); // returns true
console.log(calendar.appName); // returns "Core Widget"
console.log(calendar.hasOwnProperty("appVersion")); // returns true
console.log(calendar.appVersion); // returns "1.0.9"

Next, let’s see how people copied methods before ES6.

This can be divided into two concepts: “shallow copy” and “deep copy”. “Shallow copy” is similar to the assigning method mentioned above, where it iterates over the properties of the parent class and copies them to the subclass. We can use the for..in loop in JavaScript to iterate over the properties of an object.

Observant readers may have noticed that in Lesson 2, we learned about using the spread operator to achieve shallow copy when discussing immutability by copying.

// Shallow copy of an array
var a = [ 1, 2 ];
var b = [ ...a ];
b.push( 3 );
a;  // [1,2]
b;  // [1,2,3]

// Shallow copy of an object
var o = {
    x: 1,
    y: 2
};
var p = { ...o };
p.y = 3; 
o.y;  // 2
p.y;  // 3

Before the spread operator was introduced, people used a for..in loop like this to achieve similar shallow copy.

function shallowCopy(parent, child) {
  var i;
  child = child || {};
  for (i in parent) {
    if (parent.hasOwnProperty(i)) {
      child[i] = parent[i];
    }
  }
  return child;
}

As for deep copy, it means deeply traversing when there is an embedded object inside an object. However, this introduces a problem: if the object has multiple nested levels, should we traverse each level? How deep is considered deep enough? Also, if an object also references properties of other objects, should we also copy them over?

Therefore, compared to deep copy, shallow copy has fewer issues. But in the interactive comments section of Lesson 2, we also mentioned that if we want to ensure the immutability of an object’s deep levels, we still need deep copy. A relatively simple implementation of deep copy is to use JSON.stringify. Of course, this method assumes that the object is JSON-safe.

function deepCopy(o) { return JSON.parse(JSON.stringify(o)); }

In addition, in the comments section of Lesson 2, there is another recursive implementation mentioned, so we can roughly implement it using recursion as well.

function deepCopy(parent, child) {
  var i,
  toStr = Object.prototype.toString,
  astr = "[object Array]";
  child = child || {};
  for (i in parent) {
    if (parent.hasOwnProperty(i)) {
      if (typeof parent[i] === "object") {
        child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
        deepCopy(parent[i], child[i]);
      } else {
        child[i] = parent[i];
      }
    }
  }
  return child;
}

### How to achieve reuse through composition?

As we mentioned earlier, whether it's borrowing, assigning, deep or shallow copying, they are all one-to-one relationships. Finally, let's take a look at how to achieve composition and mixin of several object properties using `Object.assign` in ES6. The method is simple, here is a reference:

```javascript
var touchScreen = {
  hasTouchScreen : () => true
};

var button = {
  hasButton: () => true
};
var speaker = {
  hasSpeaker: () => true
};

const Phone = Object.assign({}, touchScreen, button, speaker);

console.log(
  hasTouchScreen: ${Phone.hasChocolate()}
  hasButton: ${Phone.hasCaramelSwirl()}
  hasSpeaker: ${Phone.hasPecans()}
);

Composition over inheritance in React #

In React, we can also see the presence of composition over inheritance, and it is also reflected in the two aspects we mentioned earlier: “team collaboration” and “individual collaboration with the platform”. Now, let’s first look at an example of “team collaboration”. In the example below, WelcomeDialog is a team member embedded in FancyBorder.

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

In addition, we can also see the shadow of “individual collaboration with the platform”. Here, WelcomeDialog is a “professional” Dialog. It grants permission to the Dialog platform and leverages the platform’s functionality to implement its own title and message. This is where composition is used.

function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
    </FancyBorder>
  );
}
function WelcomeDialog() {
  return (
    <Dialog
      title="Welcome"
      message="Thank you for visiting our spacecraft!" />
  );
}

Summary #

In this lesson, we have learned several core ideas and methods in JavaScript for achieving code reuse. We have analyzed traditional inheritance as well as JavaScript-specific approaches such as delegation and composition. Although I mentioned that delegation and composition are superior to inheritance, the relationship between them is not black and white.

In the frontend community, we can see that many experts, such as Douglas Crockford and Kyle Simpson, are enthusiastic advocates of delegation-based object creation. On the other hand, Dr. Axel Rauschmayer is a defender of class-based object construction.

As programmers, if we don’t have a deep understanding of objects and object-oriented principles, we may easily waver between different debates and viewpoints. The truth is, there is never just one truth. The “truth” we seek is simply a viewpoint formed from a particular perspective. It is through this perspective that we can analyze which approach is suitable for the problem we are currently trying to solve. This approach, however, only becomes the “truth” in the present moment. The purpose of the method we have organized in this unit is to help us achieve such observations.

Image

Reflection question #

In the previous lesson, we tried to look at how to achieve similar functionality by removing the syntactic sugar for object private properties and using lower-level language skills. So today, can you try to implement the super keyword in JavaScript’s class and inheritance, as well as Object.create() in prototypes and delegation?

Feel free to share your answers, exchange learning experiences or ask questions in the comments section. If you find it helpful, please share today’s content with more friends. See you in the next class!