Object-Oriented Programming: Developing an E-commerce Website, Node.js Print

  • 2

Practical Node.js, JavaScript Object-Oriented Programming: Developing an E-commerce Website

Object-Oriented Programming is centered around the concept of objects, which are instances of classes, where classes are like blueprints. Objects often represent real-world entities and encapsulate both data (attributes) and methods (functions).

JavaScript is a prototype-based language, which means it does not have classes in the way that traditional object-oriented languages like C++, Java, or Python do. However, with the introduction of ECMAScript 2015 (also known as ES6), JavaScript introduced a syntax that allows developers to write in a style similar to other OOP languages, although it's still prototype-based under the hood. This syntax involves "class" and "constructor" keywords, among others.

Node.js is a runtime environment for executing JavaScript code. Since it's based on JavaScript, it also supports the object-oriented programming style.

Understanding Node.js OOP: Classes and Objects

In JavaScript, the Object-Oriented Programming (OOP) paradigm is implemented differently than in other languages. Let's dive into the creation of classes and objects.

First, define a class using the class keyword.


class Product {
constructor(name, price) {
this.name = name;
this.price = price;
}

display() {
console.log(`Product: ${this.name}, Price: ${this.price}`);
}
}

Next, let's create an object of the `Product` class.


let product1 = new Product('Book', 25);
product1.display(); // Outputs: Product: Book, Price: 25

Utilizing Inheritance

In Node.js, we use the `extends` keyword to implement inheritance.


class DiscountProduct extends Product {
constructor(name, price, discountRate) {
super(name, price);
this.discountRate = discountRate;
}

display() {
super.display();
console.log(`Discount: ${this.discountRate}%`);
}
}

let product2 = new DiscountProduct('Book', 25, 10);
product2.display();
// Outputs: Product: Book, Price: 25
// Discount: 10%

Exploring Polymorphism

Polymorphism is the OOP concept where a method can have multiple forms. In Node.js, we can achieve it through method overriding or overloading.

In the DiscountProduct class above, we already have an example of method overriding where `display()` method was overridden.

Encapsulating Data

Encapsulation ensures that the data is hidden from outside scopes, and can be accessed via methods only.


class EncapsulatedProduct {
#price; // Private variable

constructor(name, price) {
this.name = name;
this.#price = price;
}

getPrice() {
return this.#price;
}
}

let product3 = new EncapsulatedProduct('Book', 25);
console.log(product3.getPrice()); // Outputs: 25

Exploring Constructors and Destructors

The constructor method is a special method for creating and initializing objects of a class. We already saw examples of constructors above. JavaScript, unlike some other OOP languages, does not have built-in support for destructors. However, you can create a method that performs clean up tasks.


class DestructibleProduct extends Product {
destroy() {
// Perform cleanup tasks
}
}

Defining Static Members

Static keyword defines a static method for a class. Static methods aren't called on instances of the class. Instead, they're called on the class itself.


class StaticProduct {
static getDescription() {
return 'This is a product';
}
}

console.log(StaticProduct.getDescription()); // Outputs: This is a product

Embracing the Power of Abstract Classes

In traditional OOP languages like Java, abstract classes are classes that contain one or more abstract methods - methods declared but not implemented. However, JavaScript, and by extension Node.js, does not directly support abstract classes. Nonetheless, we can emulate abstract classes using ES6 classes and methods.


class AbstractProduct {
constructor() {
if (new.target === AbstractProduct) {
throw new TypeError("Cannot construct AbstractProduct instances directly");
}
}

display() {
throw new Error("Must override method");
}
}

class ConcreteProduct extends AbstractProduct {
constructor(name, price) {
super();
this.name = name;
this.price = price;
}

display() {
console.log(`Product: ${this.name}, Price: ${this.price}`);
}
}

let product4 = new ConcreteProduct('Book', 25);
product4.display(); // Outputs: Product: Book, Price: 25

Implementing Interfaces


Interfaces, like abstract classes, are a concept not native to JavaScript. However, we can use a similar approach to enforce classes to implement certain methods.

class ImplementsDisplay {
display() {
throw new Error("Must override method");
}
}

class DisplayProduct extends ImplementsDisplay {
constructor(name, price) {
super();
this.name = name;
this.price = price;
}

display() {
console.log(`Product: ${this.name}, Price: ${this.price}`);
}
}

let product5 = new DisplayProduct('Book', 25);
product5.display(); // Outputs: Product: Book, Price: 25

Using the Final Keyword


The `final` keyword is used in other OOP languages to make a class non-inheritable or to prevent method overriding. JavaScript does not have a `final` keyword. However, you can design your classes to effectively behave as if they were "final" by not providing any methods for subclassing or overriding.

Calling Parent Constructors

Calling the parent's constructor function can be done using the `super` keyword, which we have seen in the examples above.


class SuperProduct extends Product {
constructor(name, price, category) {
super(name, price);
this.category = category;
}
}

let product6 = new SuperProduct('Book', 25, 'Education');

In this article, we have introduced how to apply OOP principles in Node.js. However, it's important to remember that JavaScript is fundamentally a prototype-based language. ES6 classes and the syntax used in this article are mostly syntactic sugar over JavaScript's existing prototype-based inheritance.

As a developer, you should aim to understand both this classical-looking OOP and the underlying prototypal inheritance in JavaScript. It will give you a robust foundation for mastering not just Node.js but JavaScript as a whole.

Working with Protected Members

Like private members, protected members are also not a native feature of JavaScript. Still, by convention, developers use an underscore `_` before the name of a variable to indicate it is intended to be protected.


class ProtectedProduct {
constructor(name, price) {
this._name = name;
this._price = price;
}

display() {
console.log(`Product: ${this._name}, Price: ${this._price}`);
}
}

Understanding the Concept of Constants


Constants are defined with the keyword `const`. These are block-scoped, much like variables defined using the `let` statement. The value of a constant cannot change through re-assignment, and it can't be redeclared.


class ConstantProduct {
constructor() {
const TAX_RATE = 0.10; // Cannot be changed
this._taxRate = TAX_RATE;
}

getTaxRate() {
return this._taxRate;
}
}

let product7 = new ConstantProduct();
console.log(product7.getTaxRate()); // Outputs: 0.10

Practical Node.js OOP: Building the Basic E-commerce System


Now, let's combine all these concepts to create a basic e-commerce system. For simplicity, let's say our e-commerce system has `Products` and `Users`.

A `Product` has a name, price, and category. A `User` has a name, and they can view a product.


class Product {
constructor(name, price, category) {
this._name = name;
this._price = price;
this._category = category;
}

getDetails() {
return `Product: ${this._name}, Price: ${this._price}, Category: ${this._category}`;
}
}

class User {
constructor(name) {
this._name = name;
}

viewProduct(product) {
console.log(`${this._name} is viewing ${product.getDetails()}`);
}
}

let book = new Product('Book', 25, 'Education');
let user1 = new User('John');
user1.viewProduct(book);
// Outputs: John is viewing Product: Book, Price: 25, Category: Education

This example is a simple one, but it should give you a practical understanding of how you can use OOP principles in Node.js to model real-world entities and their behaviors.

Remember, understanding the theory behind these principles is crucial, but it's also important to get your hands dirty with coding to really internalize these concepts. So don't hesitate to modify the examples, break them, and then fix them. That's how we all learn to code!

Exploring the Concept of Mixins

Mixins provide a way to use a class’s functionality in multiple independent classes. A mixin class is typically small and encapsulated, so it's easier to understand and use.

Let's introduce a `Discounted` mixin, which a product class can use if it can be discounted.


let Discounted = Base => class extends Base {
constructor(...args) {
super(...args);
this._discount = 0;
}

applyDiscount(discount) {
this._discount = discount;
}

getPrice() {
return super.getPrice() * (1 - this._discount / 100);
}
}

class DiscountedProduct extends Discounted(Product) {
constructor(name, price, category) {
super(name, price, category);
}
}

let discountedBook = new DiscountedProduct('Book', 50, 'Education');
discountedBook.applyDiscount(10);
console.log(discountedBook.getPrice()); // Outputs: 45

Creating a Shopping Cart

Let's extend our e-commerce system further with a `ShoppingCart` class. The `ShoppingCart` class will maintain a list of products. It'll have methods to add a product, remove a product, and calculate the total price of the products in the cart.


class ShoppingCart {
constructor() {
this._products = [];
}

addProduct(product) {
this._products.push(product);
}

removeProduct(productName) {
this._products = this._products.filter(product => product._name !== productName);
}

calculateTotal() {
return this._products.reduce((total, product) => total + product.getPrice(), 0);
}
}

let cart = new ShoppingCart();
cart.addProduct(new Product('Book', 25, 'Education'));
cart.addProduct(new DiscountedProduct('Notebook', 10, 'Education'));
console.log(cart.calculateTotal()); // Outputs: 35

In this stage of our e-commerce system, we have different types of products and a shopping cart where users can add these products and calculate the total price.

It's a rudimentary system, but it provides a starting point. From here, you can extend it further by adding more features like ordering, user authentication, payment processing, and so forth. As you add more features, try to apply the OOP principles we discussed in this article, such as inheritance, encapsulation, polymorphism, and others.

Remember that practice is key in programming. So don't stop here. Extend the system, break it, and fix it. Keep coding and have fun!

User Authentication and Authorization

While our rudimentary e-commerce system currently allows for any user to view products and add them to a shopping cart, we need to incorporate a system of authentication and authorization to provide more personalized features, such as saved carts, order history, and exclusive discounts.

For now, let's create a basic User class with a login mechanism. For simplicity, this will not involve any real authentication but will serve as a blueprint for how you might approach it.


class User {
constructor(name, password) {
this._name = name;
this._password = password; // In a real system, never store passwords as plain text
this._authenticated = false;
}

login(password) {
if (this._password === password) {
this._authenticated = true;
} else {
console.log('Incorrect password');
}
}

isAuthenticated() {
return this._authenticated;
}
}

let user = new User('John', 'password123');
user.login('password123');
console.log(user.isAuthenticated()); // Outputs: true

With authenticated users, you can now implement personalized features. For example, you can modify the `ShoppingCart` class to be tied to a specific user. When this user logs in, their shopping cart is loaded, and they can view their products and proceed to checkout.

Expanding Product Categories

Until now, all products in our system have the same attributes: name, price, and category. But what if we want to introduce more specific types of products? For example, a `Book` might have an `author` and an `isbn`, while a `ClothingItem` might have `size` and `material`.

One way to handle this is through inheritance: each specific product type is a subclass of the general `Product`. This allows us to add more specific behavior and attributes to each product type.


class Book extends Product {
constructor(name, price, category, author, isbn) {
super(name, price, category);
this._author = author;
this._isbn = isbn;
}

getDetails() {
return `${super.getDetails()}, Author: ${this._author}, ISBN: ${this._isbn}`;
}
}

class ClothingItem extends Product {
constructor(name, price, category, size, material) {
super(name, price, category);
this._size = size;
this._material = material;
}

getDetails() {
return `${super.getDetails()}, Size: ${this._size}, Material: ${this._material}`;
}
}

let myBook = new Book('My Book', 25, 'Education', 'John Doe', '1234567890');
let myShirt = new ClothingItem('My Shirt', 20, 'Fashion', 'M', 'Cotton');

console.log(myBook.getDetails());
// Outputs: Product: My Book, Price: 25, Category: Education, Author: John Doe, ISBN: 1234567890
console.log(myShirt.getDetails());
// Outputs: Product: My Shirt, Price: 20, Category: Fashion, Size: M, Material: Cotton

With these changes, your e-commerce system is becoming more sophisticated. Remember to keep trying new things, experimenting, and learning. That's the heart of programming. Happy coding!

Handling Orders

We'll now extend our system to handle orders. When a user decides to purchase the items in their shopping cart, an order will be created. This `Order` will contain all the products in the shopping cart and will also track the order status.


class Order {
constructor(user, shoppingCart) {
this._user = user;
this._items = [...shoppingCart._products];
this._status = 'New';
}

getStatus() {
return this._status;
}

setStatus(status) {
this._status = status;
}
}

let order = new Order(user, cart);
console.log(order.getStatus()); // Outputs: New

In a more realistic system, you'd likely have a range of status options such as "Processing", "Shipped", "Delivered", etc. You might also include functionality to calculate shipping costs, handle different payment methods, and more.

Adding Reviews

Let's add the ability for users to leave reviews for products. We'll create a `Review` class and add a method to our `Product` class that allows adding a review.


class Review {
constructor(user, rating, comment) {
this._user = user;
this._rating = rating;
this._comment = comment;
}

getDetails() {
return `Rating: ${this._rating}, Comment: ${this._comment}, User: ${this._user._name}`;
}
}

class Product {
// ...existing code...

addReview(review) {
if (!this._reviews) {
this._reviews = [];
}
this._reviews.push(review);
}

displayReviews() {
if (this._reviews && this._reviews.length > 0) {
this._reviews.forEach(review => console.log(review.getDetails()));
} else {
console.log('No reviews for this product.');
}
}
}

let review1 = new Review(user, 5, 'Great product!');
let review2 = new Review(user, 4, 'I liked it!');
myBook.addReview(review1);
myBook.addReview(review2);
myBook.displayReviews();

As we add more features, our system becomes more complex, and it's crucial to keep the code organized and maintainable. Using OOP principles is one way to manage complexity, as it allows us to encapsulate related data and behavior within classes and use inheritance and polymorphism to share and override behavior where needed.

Remember that OOP is just one tool at your disposal, and while it can be powerful, it's not always the best approach for every problem. The more you code and experiment, the more you'll learn when to use different tools and techniques. Keep up the good work, and keep learning!


Was this answer helpful?

« Back