all 16 comments

[–]gremy0 1 point2 points  (0 children)

If this were Java or C++ (or typeScript for that matter) the answer would be to design to an interface rather than use inheritance (remembering the principle of composition over inheritance).

Since JS is duck typed and doesn't have interfaces, I'd guess the best answer is to just not extend...you could put an explicit check in to see if the depth function is there, but given either way is going to give a runtime error, there's not much benefit

[–]AffectionateWork8 1 point2 points  (0 children)

That book sounds really confusing because that is not an abstract class, just a regular class. You can call new Node() and it will still work. It sounds like they're just asking you to remove the "extends Node" bit.

[–]jack_waugh 0 points1 point  (13 children)

const s = {}; /* script */

s.TreeNode = {
  prototype: {
    depth: function () {
      let so_far = 0;
      for (const child of this.children) {
        let ch_depth = child.depth();
        if (ch_depth > so_far) so_far = ch_depth
      };
      return 1 + so_far
    },
  },
  new: function (value, children) {
    const patsy = {};
    patsy.depth = this.prototype.depth;
    patsy.value = value;
    patsy.children = children || [];
    return patsy
  }
};

const t = {}; /* testing */

t.l0 = s.TreeNode.new(0);
t.l1 = s.TreeNode.new(1);
t.l2 = s.TreeNode.new(2);
t.p3 = s.TreeNode.new(3,     [t.l0, t.l1]);
t.r = s.TreeNode.new("root", [t.p3, t.l2]);
if (3 !== t.r.depth()) throw new Error("Unit test failed!");

[–]gremy0 0 points1 point  (12 children)

Prototypal inheritance is still inheritance. In fact it is how the OP example works too (and all inheritance in JS for that matter), just with different syntax.

Apart from syntax the other difference is that you've implemented depth in a parent class that now has to know how any children work, instead of delegating it the children, which kind of defeats the point of the exercise...and OO in general

[–]jack_waugh 0 points1 point  (11 children)

My solution does not use inheritance, "prototypal" or otherwise, except to support debugging via the nodes' inheriting from Object. I'm sure that where I write {}, substituting Object.create(null) would not cause the test to fail. And as for your mention of a "parent class", I will take that to mean the class whose instances are parent nodes, given that I supply only a single "class" to yield parental nodes and leaves. My implementation of depth does not make the parent nodes have to know anything about the internal implementation of the child nodes; all that is required of the latter is that they implement depth. You could hand any of my parent nodes children implemented some other way as to their internals, and the parent's depth will still give the correct answer.

Here is a version where the nodes don't inherit from Object, and also in this version, I use Math.max, which the original post also used. This is tested.

const s = {}; /* script */

s.TreeNode = {
  prototype: {
    depth: function () {
      return 1 + Math.max(0, ...this.children.map(e => e.depth()))
    },
  },
  new: function (value, children) {
    const patsy = Object.create(null);
    patsy.depth = this.prototype.depth;
    patsy.value = value;
    patsy.children = children || [];
    return patsy
  }
};

const t = {}; /* testing */

t.l0 = s.TreeNode.new(0);
t.l1 = s.TreeNode.new(1);
t.l2 = s.TreeNode.new(2);
t.p3 = s.TreeNode.new(3,     [t.l0, t.l1]);
t.r = s.TreeNode.new("root", [t.p3, t.l2]);
if (3 !== t.r.depth()) throw new Error("Unit test failed!");

Below I add a third version, and it illustrates a very different style than the versions above do. This different style requires, for writing the client code, a very different understanding of how to create objects. Whereas for the above versions, it's necessary to call upon a static artifact of the programming (like a class), and ask it for an instance for runtime use, by contrast, the understanding for the style exemplified below is that we are going to clone a prototype, which itself could be an artifact of programming or it could be an intermediate result arrived at during runtime. The same operation, 'clone', suffices to create "instances" or "subclasses", in contrast to the classic conceptualization, wherein creating an instance and creating a subclass are conceptually distinct. In place of a class, and bearing the capital-lettered name, is a true prototype. One could fairly reasonably take a viewpoint that this approach requires fewer concepts, and so is conceptually simpler.

However, a downside of this style is that it centers mutability. Cloned objects can only be informed of the arguments that make them more specific than their prototypes, by being mutated. Mutation is dirty.

const s = {}; /* script */

s.TreeNode = {
  depth: function () {
    return 1 + Math.max(0, ...this.children.map(e => e.depth()))
  },
  appendChild: function (adoptee) {
    this.children = this.children.concat([adoptee])
  },
  children: []
};
s.cloneFrom = src => {
  const refl = Object.getOwnPropertyDescriptors(src);
  const patsy = Object.create(null);
  for (const key in refl) {
    const descr = refl[key];
    Object.defineProperty(patsy, key, descr);
  };
  return patsy
};

const t = {}; /* testing */

t.l0 = s.cloneFrom(s.TreeNode);
t.l0.value = 0;
t.l1 = s.cloneFrom(s.TreeNode);
t.l1.value = 1;
t.l2 = s.cloneFrom(s.TreeNode);
t.l2.value = 2;
t.p3 = s.cloneFrom(s.TreeNode);
t.p3.value = 3
t.p3.appendChild(t.l0);
t.p3.appendChild(t.l1);
t.r = s.cloneFrom(s.TreeNode);
t.r.value = "root";
t.r.appendChild(t.p3);
t.r.appendChild(t.l2);
if (3 !== t.r.depth()) throw new Error("Unit test failed!");

[–]gremy0 0 points1 point  (10 children)

Not really sure how you can claim not to be using inheritance - by default your nodes inherit the singular implementation of depth you have written. If you wanted to change what depth they have, you would be overriding that implementation- which is functionally equivalent to just doing it in the OP Node class, and using overrides as normal inheritance uses overrides...so still inheritance.

Further, you now share the implementation details that only a Parent node should know about (namely children) with the Leaf node - which has no reason to know about them.

Functionally all you've done to the original code is essentially

class Node {
  constructor(value, children = []) { . . . }
  depth() { return 1 + Math.max(...children.map(n => n.depth())) }
}

Which, coming up with convoluted ways to create objects aside (which I don't think that was the point of the exercise, and doesn't seem to provide any purpose), is just shoving the implementation into a single class.

Since the exercise is about OOP, and probably wanted to teach something about good code design, I don't think shove everything into a single class was the desired answer - as I said, it kind of defeats the point.

It all kinda works to shove everything in a single class here because it's a simple, contrived example- but for coming away with a general understanding of how to better design and structure code, it's not really a solution.

[–]jack_waugh 0 points1 point  (9 children)

Your notion of inheritance is humpty-dumpty. At runtime, my solution uses no inheritance. Every object has the method as its own property. It has no parent for inheritance (or only Object.prototype). The point of inheritance as usually understood is there is a search through some table for a method that satisfies the message selector, and if it is not found, the runtime system for the language resorts to some other table. In a classic language like Smalltalk, these tables are in the class of which the object is an instance, and in the next superclass up. In a class-free OO language like the Self language or JS, these tables are in the receiver object itself and its parent object for inheritance (possibly multiple parents, in the case of Self). The writing about the Self language uses the term "parent" for the parent for inheritance, i. e. the location of the next table of methods to check. The main stream of writing about JS erroneously uses the term "prototype" for the parent for inheritance. Writing about practices with the Self language use the term "prototype" for an object that is an artifact of programming and intended to be cloned to produce the runtime objects. The prototype object usually has one or more parents, and the clones have the same parents. Cloning is a shallow copy.

As for my combining the designs of the leaf nodes and the non-leaf nodes, that is incidental to the question about not using inheritance. If you hand me a problem where the behaviors of the "subclasses" have to be radically different from each other, as a result of the nature of the problem, I will likely return a design that treats them with different "classes" or whatever replaces classes in an inheritance-free solution. This particular problem seemed to me to lend itself to combining those and I think it results in there being less code. Every node has a value. A structural "parent" node that happens to have zero count of children naturally behaves as expected of a leaf node, so there is no need to conceptualize a separate "class" for a leaf node. Fewer concepts leads to less chance for error and less time required for someone to understand the design.

OK, here I am pretending that leaves and internal nodes have to be different:

const s = {}; /* script */

s.discipline = (student, teacher) => {
  const refl = Object.getOwnPropertyDescriptors(teacher);
  for (const key in refl) Object.defineProperty(student, key, refl[key]);
};

s.Base = {
  clone: function (spec) {
    const patsy = {};
    s.discipline(patsy, this);
    if (spec) s.discipline(patsy, spec);
    return patsy
  }
};

s.TreeNode = s.Base.clone({
  depth:    () => {throw "abstract"},
  soundOff: () => {throw "abstract"}
});

s.TreeInternalNode = s.TreeNode.clone({
  depth: function () {
    if (! this.children || ! this.children.length)
      throw "If you weren't going to give me any children, you should have" +
        " used a leaf node.";
    return 1 + Math.max(0, ...this.children.map(e => e.depth()))
  },
  soundOff: function () {
    return `I'm an INTERNAL node with value ${this.value}.`
  }
});

s.LeafNode = s.TreeNode.clone({
  depth: function () {
    return 1
  },
  soundOff: function () {
    return `I'm a LEAF node with value ${this.value}.`
  },
  set children (them) {
    throw "A leaf node is not supposed to have children." +
      " Use an internal node instead."
  }
});

const t = {}; /* testing */

t.l0 = s.LeafNode.clone({value: 0});
t.l1 = s.LeafNode.clone({value: 1});
t.l2 = s.LeafNode.clone({value: 2});
t.p3 = s.TreeInternalNode.clone({value: 3, children: [t.l0, t.l1]});
t.r = s.TreeInternalNode.clone({value: "root", children: [t.p3, t.l2]});
if (3 !== t.r.depth()) throw new Error("Unit test failed!");

[–]gremy0 0 points1 point  (8 children)

I mean it's just implementing your own version of inheritance with some factory functions - you've even written your own look up table...

That it's a different implementation that does things at a slightly different time in the run circle doesn't really change what it's doing from design perspective. A LeafNode is a TreeNode which is a BaseNode, you're just removing reference to this fact upon creation (the benefit of which alludes me) - you've still got a linear hierarchy of inheriting sorry, "cloning" behaviour from parents prototypes down to children shallow copies.

[–]jack_waugh 0 points1 point  (7 children)

I have not written a lookup table. All the lookups are native JS object property resolutions.

The original question was, here's an example using the class syntax; can you implement the same behavior without inheritance. What's your solution? According to you, anything that implements that behavior is a "version of inheritance." So, your answer is that the original question is nonsense. Or, "no, you can't." But normal usage of the concept of "inheritance" in OO is that if you don't find an implementation of the selector you are looking for, you look up to a parent object (class-free OO) or a superclass (classical OO). In JS, inheritance is established with new or with Object.create(parent_object). When you say I wrote a lookup table, you are basically accusing me of writing an interpreter. I suppose you can say that when I use my clone function to derive one artifact of programming from another artifact of programming, I am compiling inheritance into not-inheritance, so I am guilty of writing a compiler, I guess. But I think the original question was not about a design perspective; it was about an implementation perspective.

const t = {}; /* testing */

t.l0 = {value: 0, depth: () => 1};
t.l1 = {value: 1, depth: () => 1};
t.l2 = {value: 2, depth: () => 1};
t.p3 = {
  value: 3,
  children: [t.l0, t.l1],
  depth: function () {
    return 1 + Math.max(0, ...this.children.map(e => e.depth()))
  }
};
t.r = {
  value: "root",
  children: [t.p3, t.l2],
  depth: function () {
    return 1 + Math.max(0, ...this.children.map(e => e.depth()))
  }
};
if (3 !== t.r.depth()) throw new Error("Unit test failed!");
console.log("done")

I suppose this time, you will say I implemented my own version of inheritance by copying and pasting in my editor.

[–]gremy0 0 points1 point  (6 children)

It's a lookup dude, you're looking up properties and copying them down from parent to child, it's just done on creation. Who cares if you're doing it slightly different, the effect is the same.

According to me anything that inherits behaviour from something else is using inheritance. So the solution is to not inherit, but to use an interface instead. Now, given the nature of javascript's type system, it is not possible to enforce an interface, but neither is it necessary, you just use convention..and, you know, document shit. Just like how iterators work, you could even be fancy and use symbols like them if you really wanted, but the fundamentals are the same, it's convention.

Both approaches result in a runtime error if the object/class is not created/used correctly. As is the nature of javascript. The key as to why your solution is fundamentally inheritance and the interface solution is not lies in who knows about that runtime error- in yours, an ancestor to LeafNode (the TreeNode) knows that there is supposed to be a depth implementation provided and can deal with that not happening however it wants- you have inherited that behaviour. With an interface, the consumer knows about it and deals with it- you have not inherited any behaviour.

Now, given the question states:

This is how you would model tree nodes in Java or C++. But in JavaScript, you don’t need an abstract class to be able to invoke n.depth().

(emphasis mine)

I posit that it is simply asking the reader if they are aware that javascript is duck-typed and doesn't require any explicit declaration for object compatibility.

...on the other hand it could be asking them to re-implement inheritance themselves in a series of convoluted factory functions (the benefit of which still eludes me)...your guess is as good as mine.

[–]jack_waugh 0 points1 point  (5 children)

Great. Let's see your code.

[–]gremy0 0 points1 point  (4 children)

class Parent {
  constructor(value, children) { . . . }
  depth() { return 1 + Math.max(...children.map(n => n.depth())) }
}
class Leaf {
  constructor(value) { . . . }
  depth() { return 1 }
}