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.
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.
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