const - JS | lectureJavaScript OOP

What is Object Oriented Programming (OOP)?

JavaScript OOP

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects. And by paradigm, we mean how code is written and organized. We use objects to model or describe aspects of the real world like a user, or even more abstract features.

As we already know, objects can contain data that we call properties and code, which we call methods. So we can say that by using objects, we pack all the data and the corresponding behavior all into one big block.

index.js
const user = {
  // data
  username: 'jack65',  email: 'jack.arnold@gmail.com',  password: 'A2Su4sSeX9',
  // behavior
  login() {    // do something  },  greet() {    // do something  },};

This makes it easy to act directly on the data. And speaking of blocks, that's exactly what objects are supposed to be. So, in OOP, objects are self-contained pieces or blocks of code like small applications on their own. And we then use these objects as building blocks of our applications and make objects interact with each other.

These interactions happen through a so-called public interface, which we also call API. This interface is basically a bunch of methods that a code outside of the object can access and that we can use to communicate with the object.

The question now is why does OOP actually exist? Well, this paradigm was developed with the goal of organizing code. i.e., to make it flexible and easier to maintain. So before OOP, we might have a bunch of codes gathered across multiple functions or even in the global scope without any structure. And this particular kind of crazy style of code is what we usually call spaghetti code. Spaghetti code makes it hard to maintain large code bases, and it's also hard to reuse code.

However, note that OOP is certainly not the only way of writing organized and maintainable code. There are many other paradigms that have become increasingly popular, like functional programming. And functional programming allows us to achieve the same goal.

Using objects is nothing new for us at this point. We have been using them all the time. However, we've only used objects as loose collections of data without making them interact with one another. Also, we didn't have a way to generate objects programmatically. All we ever did was use simple object literals, but in OOP, we actually need a way to generate or create new objects from our code. And to do that in traditional OOP, we use something called classes.

You can think of a class as a blueprint which can then be used to create new objects based on the rules defined in the class. It's just like architecture, where the architect creates a blueprint to exactly plan and describe a house. But the blueprint is really just an abstract plan, like a set of rules but nothing tangible that you can actually touch. However, from that blueprint, many real houses can then be built in the real world. And with classes, it's just the same.

Let's take a look at the below fictional class called User.

User {
  // data
  username  email  password
  // behavior
  login(password) {    // do something  }  greet() {    // do something  }}

And I say fictional because this is NOT actual JavaScript syntax. Because JavaScript does not actually support real classes like I'm explaining here. We do have a class syntax in JavaScript too, but it still works a bit differently from what I'm going to show you here. However, the idea of creating objects from a kind of blue print is still a very useful mental model to have because in general terms, this is how OOP works across all programming languages including JavaScript.

Back to our above fictional class, we can see that it describes a user with a username, an email, and a password. So it is a description of a user's data, not the actual data itself. Remember that a class is just a blueprint or a plan, and a plan doesn't contain the real world data just yet. On the other hand, we then have the behavior that is associated with the data, and in this case, we have the login, and greet methods

Just as we learned earlier, this class has everything related to a user. That is, data and behavior all packed into one nice, self-contained block. Let's now use this class to create a new object from it.

// create a new object
new User('@walters');
// Result
{  username: '@walters',  email: 'wal_knight@hotmail.com',  password: '8NOBBNbvBlFka6',  login(password) {    // do something  },  greet() {    // do something  },}

From the result, you can see that we now have real data about the user and not just a description of the data like we have in the class. And all objects created through a class are called instances of that class. An instance is a real object that we can use in our code, which was created from a class. And a class itself is NOT an object.

Back to the blueprint analogy from earlier, the instance (above result) is like a real house that was created from the blueprint (fictional class). The beauty of this is that we can now create as many instances as we need in our application, just like we can build as many houses as we need from the same blueprint. And all the instances can have different data in them, but they all share the same behavior or functionality, which is to log in and greet.

Now that we know how classes work and that we can create objects from them, the next logical question is, how do we actually design a class? Or, in other words, how do we model real-world data into classes? Well, this is a very important question, and it's also a very difficult one to answer. Because it really depends on the application that you're building. And there is not a single correct way of designing classes. However, there are four fundamental principles that can guide us toward a good class implementation. And these principles are: abstraction, encapsulation, inheritance, and polymorphism.

Abstraction


Abstraction means to ignore or hide details that don't matter. This allows us to have an overview perspective of whatever it is that we are implementing instead of messing with details that don't really matter to our implementation. For example, let's say we are implementing a phone for a user to use. Without abstraction, we could design our class to include everything that there is about the phone, including all the internal stuff like the phone's temperature and voltage. But as a user, we don't really care about these details. All we care about is, for example, going to the home screen, making a call, or increasing the volume. So, we can abstract all the internal details and only include the things that matter to the user.

So, the phone then operates kind of as a black box without us seeing what is happening inside. Of course, internally, the phone still needs to measure the temperature and voltage, but we can hide these details from the user. And that is exactly what abstraction is all about.

Abstraction is really important and not just in OOP but in programming in general. In fact, we create and use abstractions all the time. For example, let's take the addEventListener method that we use all the time. Do we actually know how it works behind the scenes? No, we don't. Do we care? No, we don't. And we don't have to because, once more, the low-level details of how it works are abstracted away from us.

Encapsulation


Encapsulation means keeping some properties and methods private inside the class so they are not accessible from outside the class. However, some methods can, of course, be exposed as a public interface, which we call API. And this is exactly what I meant when I said that interactions between objects happen through a public interface. Considering our fictional user class, this is what private properties and methods might look like conceptually.

User {
 // data
 username

 private email private password
 // behavior
 login(password) {
 this.password = password; }
 greet() {
 // do something
 }

 private sendMoney(amount) { // do something
 }
}

Once more, this is conceptual because the private keyword does NOT exist in JavaScript. As we already know, outside this class now, we can't access the properties email and password. However, inside the class, they are still accessible. So, having these critical properties nicely encapsulated like this, we prevent external code from accidentally manipulating this internal data. Hence, preventing bugs and security issues.

We also have a private method in our class- sendMoney. Again, it is not accessible from outside the class but is used internally to send money to other users, for example. So we want no one else outside of the class to be able to use this method. Hence, we do not make it part of the public API.

Inheritance


Let's assume we have the following classes.

// First class
Animal {
 speed
 name

 run(speed) {
 // speed logic
 }
 stop() {
 // stop logic
 }
}

// Second class
Rabbit {
 speed
 name
 type

 run(speed) {
 // speed logic
 }
 stop() {
 // stop logic
 }
 jump() {
 // jump logic
 }
}

As we can see, they actually have a lot in common. In fact, Rabbit has all the properties and methods that Animal has. An that actually makes sense because if you think about it, a rabbit is also an animal. However, if we design our classes like this, .i.e., as two separate identities, we will end up with a lot of duplicate code. But well, that's where inheritance comes in.

In OOP, when we have two classes that are closely related, like Rabbit and Animal above, we can have one class inherit from the other. So, we will have one parent and one child class. And the child class extends the parent class.

inheritance

Yeah, that's great, but what does it actually mean? Well, I think it's quite intuitive. Just like you, as a child, probably inherited some features from your parents, a child class inherits all the properties and methods from the parent class.

inheritance

In more formal terms, inheritance makes all properties and methods available to a child class which, of course, then forms a hierarchy between these two classes. The goal of this is to allow us to reuse logic that is common to both classes. In this case both classes need to run. So instead of having to write the same code twice, we can just inherit it from the parent class. Now, of course, a child class can also have its own unique properties and methods. Hence, at the end of the day, the child class ends up being a combination of the parent class and its own unique properties and methods.

inheritance

Polymorphism


Polymorphism, in simple terms, means that a child class can overwrite a method it inherited from the parent class. Considering our Animal and Rabbit classes, we can see that both classes have a run method. But now, let's say that a rabbit requires a different logic for running; how can we implement that? Well, it's actually quite simple. In the Rabbit class, we can simply just write a new method which is also called run. And then, according to polymorphism, this new method will overwrite the one inherited from the parent class.

To finish, I know there was a lot to take in here, so make sure you understand everything here before moving on. In the next lecture, we are going to talk about how object-oriented programming actually looks like in JavaScript.