Writing JavaScript Using OOP Concepts in ES6
ECMAScript 6 (ES6) was a relatively recent and major revision of JavaScript (JS). ES6
helped make JS more Object Oriented. Below is a simple working example of OOP concepts
in JS.
The below list of constants will be the data used for this example.
Since constants cannot be changed, they are usually written in snake-case with all capitals.
/**
* Data & storage for this example
*/
const EXAMPLE_USER_ID_1 = 1234;
const NEW_DATA = {
firstName: "Bob",
lastName: "Smith",
email: "bob.smith@email.com",
phone: "18004365555"
};
const BAD_NEW_DATA = {
firstName: "Bill",
lastName: "",
email: "bill@.com",
phone: "18004365555"
};
const EDITED_DATA = {
id: 9876,
firstName: "Mary",
lastName: "Thomas",
email: "mary.thomas@email.com",
phone: "18004345555"
};
const VIEW_DATA = {
id: 9999,
firstName: "Lisa",
lastName: "Stevens",
email: "lisa.Stevens@email.com",
phone: "18002325555"
};
In this example, the Users class contains methods to perform CRUD (i.e. create, read, update, and delete) actions,
similar to a database-driven web application. Each CRUD action in the class is called once the class has been instantiated.
The general worklflow for each action is as follows:
- Users instantiated
- Action called
- Component called
- Data validated
- Data processed
- Response returned
The action then returns a payload that includes a boolean value for successful or unsuccessful, a message, and output, if applicable.
Each action takes a parameter which is either an id and/or a constant. In a real-world example, this would be equivalent to
a server request passing in parameters.
If you are using a modern browser (i.e. no more IE shenanigans), you can also see the below console.log outputs in your own
browser's inspect > console, since this is a working and tested example.
/**
* Class Instantiation
*/
let users = new Users();
console.log(users.add(NEW_DATA)); //output [true, "Saved", ""]
console.log(users.add(BAD_NEW_DATA)); //output [false, "Email format is incorrect", ""]
console.log(users.edit(9876, EDITED_DATA)); //output [true, "Edited", ""]
console.log(users.edit('badId', EDITED_DATA)); //output [false, "System error", ""]
console.log(users.delete(9876)); //output [true, "Deleted", ""]
console.log(users.delete('badId')); //output [false, "System error", ""]
console.log(users.view(9999)); //output [true, "Success", {email: "lisa.Stevens@email.com", firstName: "Lisa", id: 9999, lastName: "Stevens", phone: "18002325555"}]
console.log((new Users()).view('badId')); //output [false, "System error", ""]
The Values class holds the successful and failed boolean variables that are used in conditionals throughout
this example. Typically, you would want to make these constants, since they will never change; however, this is an
example of static variables. Static variables in a class do not require class instantiation and they can be accessed from
any other class.
/**
* Static values
*/
class Values {
static failed = false;
static successful = true;
}
The Base class is a parent class, because it should always be called when the Users class is instantiated.
In this example, the Base class is used to get the user's ID. In a real-world example, the Base class would be used
to get and make readily-available internal data, such as the user's information, permissions, etc...
/**
* This class should get internal data, permissions, etc...
* For example, Base class gets the user's id
* This class is extended because it should always be called
*/
class Base {
/**
* All constructors do not need to be explicitly called,
* instead they automatically initialize.
*/
constructor() {
// Here the class that gets the user's id from storage
this.userId = EXAMPLE_USER_ID_1;
}
}
The Users class is a child class of the Base class. You can see this relationship by the "extends" keyword.
Users acts as a controller class that directs the process workflow to the correct component. If there are
any errors within the process workflow of each action, the error will get caught and thrown in the action.
/**
* Users contains all methods to manage users
*/
class Users extends Base {
/**
* Public method
*/
add(data) {
try {
return (new AddComponent(this.userId)).addAction(data);
} catch (error) {
throw new Error(error);
}
}
/**
* Public method
*/
edit(id, data) {
try {
return (new EditComponent(this.userId)).editAction(id, data);
} catch (error) {
throw new Error(error);
}
}
/**
* Public method
*/
delete(id) {
try {
return (new DeleteComponent(this.userId)).deleteAction(id);
} catch (error) {
throw new Error(error);
}
}
/**
* Public method
*/
view(id) {
try {
return (new ViewComponent(this.userId)).viewAction(id);
} catch (error) {
throw new Error(error);
}
}
}
The DataComponent class performs the actual data queries and computations.
It is also a parent class to each of the component classes.
class DataComponent {
/**
* Protected method
*/
saveData(data) {
let success = Values.failed;
/* Some type of save into storage
* would happen here. For this example,
* I'll just use a true statement.
*/
if (1 === 1) {
success = Values.successful;
}
return success;
}
/**
* Protected method
*/
editData(id, data) {
let success = Values.failed;
/* Some type of update record in storage
* would happen here. For this example,
* I'll just use a true statement.
*/
if (1 === 1) {
success = Values.successful;
}
return success;
}
/**
* Protected method
*/
deleteData(id) {
let success = Values.failed;
/* Some type of delete record in storage
* would happen here. For this example,
* I'll just use a true statement.
*/
if (1 === 1 && !isNaN(id)) {
success = Values.successful;
}
return success;
}
/**
* Protected method
*/
viewData(id) {
let success = Values.failed;
let data = {};
/* Some type of record to view in storage
* would happen here. For this example,
* I'll just use a true statement.
*/
if (1 === 1 && !isNaN(id)) {
success = Values.successful;
data = VIEW_DATA;
}
return [
success,
data
];
}
}
The AddComponent class holds and calls all methods related to adding a record.
This class is a child class to the DataComponent. The "super" keyword in the constructor
allows the AddComponent access to the Base class object. This allows the AddComponent class
to gain access to the user id. In a real-world example, methods that manipulate or create records should
know who (the user) is making the queries.
class AddComponent extends DataComponent {
constructor(userId) {
// Call the parent constructor to get the object
super();
this.userId = userId;
}
/**
* Public method
*/
addAction(data) {
let success = Values.failed;
let [valid, message] = (new ValidationComponent()).validateAdd(data);
if (Values.successful === valid) {
success = this.saveData(data);
if (Values.successful === success) {
message = "Saved";
}
}
return [
success,
message,
""
];
}
}
The EditComponent class holds and calls all methods related to editing a record.
This class is a child class to the DataComponent. The "super" keyword in the constructor
allows the EditComponent access to the Base class object. This allows the EditComponent class
to gain access to the user id. In a real-world example, methods that manipulate or create records should
know who (the user) is making the queries.
class EditComponent extends DataComponent {
constructor(userId) {
// Call the parent constructor to get the object
super();
this.userId = userId;
}
/**
* Public method
*/
editAction(id, data) {
let success = Values.failed;
let [valid, message] = (new ValidationComponent()).validateEdit(id, data);
if (Values.successful === valid) {
success = this.editData(id, data);
if (Values.successful === success) {
message = "Edited";
}
}
return [
success,
message,
""
];
}
}
The DeleteComponent class holds and calls all methods related to deleting a record.
This class is a child class to the DataComponent. The "super" keyword in the constructor
allows the DeleteComponent access to the Base class object. This allows the DeleteComponent class
to gain access to the user id. In a real-world example, methods that manipulate or create records should
know who (the user) is making the queries.
class DeleteComponent extends DataComponent {
constructor(userId) {
// Call the parent constructor to get the object
super();
this.userId = userId;
}
/**
* Public method
*/
deleteAction(id) {
let message = "System error";
let success = this.deleteData(id);
if (Values.successful === success) {
message = "Deleted";
}
return [
success,
message,
""
];
}
}
The ViewComponent class holds and calls all methods related to viewing a record.
This class is a child class to the DataComponent. The "super" keyword in the constructor
allows the ViewComponent access to the Base class object. This allows the ViewComponent class
to gain access to the user id. In a real-world example, methods that access records should
know who (the user) is making the queries.
class ViewComponent extends DataComponent {
constructor(userId) {
// Call the parent constructor to get the object
super();
this.userId = userId;
}
/**
* Public method
*/
viewAction(id) {
let message = "System error";
let [success, data] = this.viewData(id);
if (Values.successful === success) {
message = "Success";
}
return [
success,
message,
data
];
}
}
The ValidationComponent class validates all data before it gets saved, deleted, edited, etc...
In a real-world example, valdiation should be done on both the client and server. Client-side validation
helps to assure data is ready to be sent to the server, without inundating the server with many requests.
Server-side validation assures that the data is valid and protects the server from fraudulent or dirty data, since
client-side validations can easily be circumvented.
class ValidationComponent {
constructor() {
this.valid = Values.successful;
this.message = "";
}
/**
* Public method
*/
validateAdd(data) {
this.firstName(data.firstName);
this.lastName(data.lastName);
this.email(data.email);
this.phone(data.phone);
return [
this.valid,
this.message
];
}
/**
* Public method
*/
validateEdit(id, data) {
this.id(id, data.id);
this.firstName(data.firstName);
this.lastName(data.lastName);
this.email(data.email);
this.phone(data.phone);
return [
this.valid,
this.message
];
}
/**
* Protected method
*/
id(id, value) {
this.validateId("System error", value, id);
}
/**
* Protected method
*/
firstName(value) {
this.validateEmpty("First name", value);
}
/**
* Protected method
*/
lastName(value) {
this.validateEmpty("Last name", value);
}
/**
* Protected method
*/
email(value) {
this.validateEmpty("Email", value);
this.validateEmailStructure(value);
}
/**
* Protected method
*/
phone(value) {
this.validatePhoneStructure(value);
}
/**
* Private method
*/
validateId(type, value, id) {
if (Number(id) !== Number(value)) {
this.valid = Values.failed;
this.message = type;
}
}
/**
* Private method
*/
validateEmpty(type, value) {
if ("" === value) {
this.valid = Values.failed;
this.message = type + ' is empty';
}
}
/**
* Private method
*/
validateEmailStructure(value) {
if (Values.failed === /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(value)) {
this.valid = Values.failed;
this.message = 'Email format is incorrect';
}
}
/**
* Private method
*/
validatePhoneStructure(value) {
if (Values.failed === /^\d{10}$/) {
this.valid = Values.failed;
this.message = 'Phone number format is incorrect';
}
}
}