4

I am trying to find a way to create an simili-abstract class in ES6. So far, everything I try always hits the limitations of the langage and/or its syntax (also my limited knowledge about prototyping).

Basic oop; We declare a class and extend it. The final class has to access some fields and methods from its superclass, but not all of them. It also morphs public methods...

The class declarations should be in a perfect encapsulation, so nothing else than this code is be able to reach it (something similar to a namespace).


So far, my experiments in ES5 are wrong... I would really appreciate some advice and help.

(function(){

    // ==================================

    function AbstractClass(params) {
        var _myParams = params;
        var _privateField = "Only AbstractClass can see me";
        this.publicField = "Everybody can see me";

        function privateFunc() {
            // Does private stuff
        }
    }
    AbstractClass.prototype.publicFunc = function() {
        // Does public stuff
        privateFunc(); // Works?
    }

    // ==================================

    function FinalClass(params) {
        // How to pass the params to the superclass?
    }
    FinalClass.prototype.publicFunc = function() {
        // Override and calls the superclass.publicFunc()?
        // How can I touch _privateField ? publicField ?
    }
    FinalClass.prototype = Object.create(AbstractClass.prototype);

    // ==================================

    var foo = new FinalClass("hello world!");
    foo.publicFunc();
})();

Can you tell me what is wrong with this code and how to fix it?
Bonus question: How to do this in ES6 properly?
Triple bonus: What about protected fields and methods?

Thank you.

Guillaume F.
  • 5,905
  • 2
  • 31
  • 59
  • 1
    "*The class declarations should be in a perfect encapsulation*" - the only perfect encapsulation that JS provides are scopes (and ES6 doesn't change anything about this). – Bergi Mar 28 '17 at 23:05
  • I'm kinda missing what your question has to do with the class being abstract? – Bergi Mar 28 '17 at 23:07
  • @Bergi : For the encapsulation I used `(function() {} )();`, which should scope the code. About the abstract class, it has to do with the project I am working on, the question is the path which will lead to a simili-abstract class in the project (JS can't do abstract classes, right?). – Guillaume F. Mar 28 '17 at 23:10
  • 1
    "The class declarations should be in a perfect encapsulation" --- that's not reachable (?). You can always monkeypatch runtime to expose anything you want. It might be reasonable to use the strong parts of the language, instead of fighting with it and bringing idioms from other languages that don't fit well. – zerkms Mar 28 '17 at 23:11
  • @zerkms : Not reachable from outside the scope, but not for security purposes, so I don't mind 'monkeypatches'. I don't mind to be reasonable, if only I am shown the right way :) – Guillaume F. Mar 28 '17 at 23:14
  • 1
    "the right way" in JS is to not try to simulate abstract classes, final classes or methods and private/protected members. – zerkms Mar 28 '17 at 23:16
  • 1
    @GuillaumeF. I don't know what you mean by "simili-abstract", but JS [can do abstract classes](http://stackoverflow.com/a/30560792/1048572) – Bergi Mar 28 '17 at 23:17
  • @GuillaumeF. An IIFE does not help you with the scope of prototype methods. If you want instance-specific variables ("private properties"), you need to put them - and everything that accesses them - in the constructor. – Bergi Mar 28 '17 at 23:19
  • @Bergi : So... a public function, declared in the prototype, won't be able to access the private properties? Does it mean everything has to be public when you use prototypes? // before this issue, I used `function Class() { this.publicFunc = [...] }` to declare public functions in the private scope of the constructor, but I don't think you can extend it if you do this... – Guillaume F. Mar 28 '17 at 23:24
  • 1
    @GuillaumeF. Yes, yes. No, you can still extend them, although it's quite ugly. – Bergi Mar 28 '17 at 23:26
  • After a tons of fiddling, I actually managed to do it in ES5 (private/protected/public). The code is fairly clean. I will post an answer when it's ready. Thank you for your comments, it was really helpful. – Guillaume F. Mar 29 '17 at 12:10
  • 1
    As it was probably said above, the fundamental problem with emulating protected props with local vars is that you can access them only in current module/IIFE/constructor. Because local variables are *not* private/protected props. Encapsulation is supposed to help developers, not to disturb them. In a good high-level language you can reflect private props. You can't reflect local vars. I tried to do encapsulation before, and I can honestly say that underscored props are the only good and reasonable way to do this in JS. If you need visibility control, stick to TS. – Estus Flask Mar 29 '17 at 12:21

1 Answers1

5

This is actually a very good question and I will try to give you an insightful answer...

As I already explained somewhere on Stack Overflow, JavaScript is not really a class-based language. It is based on prototypes. This is a completely different programming paradigm and you should take this into consideration. So when you write something in Vanilla JS, this is generally a good idea to forget (just a tad) what you know about Java or C++.

However, JavaScript is a very flexible language and you can program as you wish. In my opinion, there are two main styles when it comes to JavaScript programming: an idiomatic style and a classic style.

  • The idomatic style makes intensive use of object literals, duck typing, factory functions and composition.
  • The classic style tries to mimic the behavior of class-based languages with constructor functions for classes and IIFEs (Immediately-Invoked Function Expressions) for encapsulation. It lays stress on inheritance and polymorphism.

What you want is an abstract class. An abstract class is a class that cannot be instantiated and is only useful as a model for derived classes. If you care about strict encapsulation, this is how you could implement it in ES5:

// ==============================
// ABSTRACT "CLASS"
// ==============================

var OS = (function (n) {
  // Here "name" is private because it is encapsulated in the IIFE
  var name = "";

  // Constructor
  function OS (n) {
    // If "OS" is called with "new", throw an error
    if (this.constructor === OS) {
      throw new Error('You cannot instantiate an abstract class!');
    }
    name = n;
  }

  // We cannot call this method directly (except with "call" or "apply") because we cannot have direct instances of "OS"
  OS.prototype.boot = function () {
    return name + ' is booting...';
  };

  // This is an abstract method. It will be in the prototype of derived objects but should be overriden to work
  OS.prototype.shutdown = function () {
    throw new Error('You cannot call an abstract method!');
  };

  // Getter for "name"
  OS.prototype.getName = function () {
    return name;
  };

  // The constructor must be returned to be public
  return OS;
})();

// ==============================
// CONCRETE "CLASS"
// ==============================

var LinuxDistro = (function (name) {
  // Constructor
  function LinuxDistro(name) {
    // Here we call the constructor of "OS" without "new", so there will not be any error
    OS.call(this, name);
  }
  // Here "Linux Distro" inherits from "OS"
  LinuxDistro.prototype = Object.create(OS.prototype);
  LinuxDistro.prototype.constructor = LinuxDistro;

  // Private function/method
  function textTransform(str, style) {
    return style === 'lowercase' ? str.toLowerCase() : str.toUpperCase();
  }

  // The parent method is used and overriden
  LinuxDistro.prototype.boot = function () {
    return OS.prototype.boot.call(this) + ' Welcome to ' + textTransform(this.getName());
  };

  // The abstract method is implemented
  LinuxDistro.prototype.shutdown = function () {
    return 'Shutting down... See you soon on ' + textTransform(this.getName());
  };
  
  // The constructor must be returned to be public
  return LinuxDistro;
})();

// ==============================
// CLIENT CODE
// ==============================

var arch = new LinuxDistro('Arch Linux');

console.log(arch.getName());
console.log(arch.boot());
console.log(arch.shutdown());

Now you want the same thing with ES6. The good point is that ES6 provides nice syntactic sugar to work with classes. Again, if you care about strict encapsulation, you could have the following implementation:

// ==============================
// ABSTRACT "CLASS"
// ==============================

const OS = (n => {
  // Here "name" is private because it is encapsulated in the IIFE
  let name = "";

  class OS {
    constructor(n) {
      // If "OS" is called with "new", throw an error
      if (new.target === OS) {
        throw new Error('You cannot instantiate an abstract class!');
      }
      name = n;
    }

    // We cannot call this method directly (except with "call" or "apply") because we cannot have direct instances of "OS"
    boot() {
      return `${name} is booting...`;
    }

    // This is an abstract method. It will be in the prototype of derived objects but should be overriden to work
    shutdown() {
      throw new Error('You cannot call an abstract method!');
    }

    // Getter for "name"
    get name() {
      return name;
    }
  }

  // The class must be returned to be public
  return OS;
})();

// ==============================
// CONCRETE "CLASS"
// ==============================

const LinuxDistro = (name => {

  // Private function/method
  function textTransform(str, style) {
    return style === 'lowercase' ? str.toLowerCase() : str.toUpperCase();
  }
  
  class LinuxDistro extends OS {
    constructor(name) {
      // Here we call the constructor of "OS" without "new", so there will not be any error
      super(name);
    }

    // The parent method is used and overriden
    boot() {
      return `${super.boot()} Welcome to ${textTransform(this.name)}`;
    }

    // The abstract method is implemented
    shutdown() {
      return `Shutting down... See you soon on ${textTransform(this.name)}`;
    }
  }
  
  // The class must be returned to be public
  return LinuxDistro;
})();

// ==============================
// CLIENT CODE
// ==============================

const arch = new LinuxDistro('Arch Linux');

console.log(arch.name); // This is not a direct access to "name". The getter is used...
console.log(arch.boot());
console.log(arch.shutdown());

Of course, these snippets are not perfect and may look a bit scary. But I think this is the best we can do, due to the prototypal nature of JavaScript.

As you probably see, class members are either private (thanks to IIFEs and closures) or public (thanks to how objects are created, with their own properties and prototype chain). If you really want protected members, this is another story...

When you have in mind an OOP model for your JavaScript code, I would recommend you to use TypeScript. This is much more convenient, readable and maintainable than the code presented above.

Finally, if you want to go further and see how you could implement all traditional OOP design patterns in JavaScript (especially GoF patterns), I invite you to take a look at a project of mine on GitHub: PatternifyJS

Badacadabra
  • 8,043
  • 7
  • 28
  • 49
  • Just an FYI, on this line `const LinuxDistro = (name => {`, if you're passing in multiple parameters, you'll need an extra set of () as such: `const LinuxDistro = ((name,param2,param3) => {` – jlewkovich Jun 14 '19 at 17:05