first published: 09/10/2023
In order to understand encapsulation, we're going to have to understand a bit about access modifiers - don't worry if you don't know what they are, all will be explained. If you're anything like me and your first language was Javascript (followed by Typescript) you've probably wondered what the point of getters and setters actually is - is it really just to save us having to type `()`? Well, no... sort of. The thing is, javascript used to not have a notion of private class fields, and even though Typescript did, they weren't _really_ private. For example, you could access a private property on a class in Typescript like so:
class Person {
private name: string;
constructor(name: string) {
this.name = name;
}
}
const person = new Person("sarah")
console.log(person.name) // Property 'name' is private and only accessible within class 'Person'. - ok, this works as we expect
console.log(person["name"]) // Oh no! this logs "sarah" to the console
But with the introduction of private class fields in ES2019, you can now have _truly_ private fields in your javascript/typescript classes. Let's look at how:
class Person {
#name: string;
constructor(name: string) {
this.#name = name;
}
}
const person = new Person("sarah")
console.log(person.#name) // Property '#name' does not exist on type 'Person'
console.log(person["#name"]) // // Property '#name' does not exist on type 'Person' - this is the same as above, yay \o/
So, the `#` symbol or the `private` keyword are `access modifiers` - that is, they determine what properties can be accessed by other code outside of the class (but only the `#` really makes fields private).
Encapsulation is the idea that the internal workings of a class should be hidden from the outside world. This is a good thing because it means that the internal workings of a class can be changed without breaking any code that uses that class. Let's go back to our `Person` class, you may want to give the person an age, which sounds reasonable enough. Without private properties, it would look something like this:
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const person = new Person("sarah", 30)
person.age = "thirty" // typescript doesn't let us compile because we've tried to assign a string to a number, that's good
person.age = -100 // Oh no, that can't be right!
person.age = 30000 // also shouldn't be possible! (unless you're a tortoise...)
As you can see, if we allow our class fields to be public then anyone can set them to _anything_ (unless you're using typescript, in which case at least the type is enforced). This doesn't seem like a good idea, so let's make them private:
class Person {
#name: string;
#age: number;
constructor(name: string, age: number) {
this.#name = name;
this.#age = age;
}
}
const person = new Person("sarah", 30)
person.#age = "thirty" // Property '#age' is not accessible outside class 'Person' because it has a private identifier.
person.#age = -100 // Property '#age' is not accessible outside class 'Person' because it has a private identifier.
person.#age = 30000 // Property '#age' is not accessible outside class 'Person' because it has a private identifier.
Nice! Ok so now we have a class that can't be broken by anyone who uses it. But what if we want to be able to change the age of the person? Well, we can do that by adding a method to the class that allows us to do that (this is where getters and setters come in to the picture):
class Person {
#name: string;
#age: number;
constructor(name: string, age: number) {
this.#name = name;
this.#age = age;
}
get age() {
return this.#age;
}
set age(newAge: number) {
if (newAge < 0 || newAge > 150) {
throw new Error("Invalid age")
}
this.#age = newAge;
}
}
const person = new Person("sarah", 30)
person.setAge(-100) // Nope, not allowed due to our business logic in the setter!
person.setAge(30000) // Also not allowed due to our business logic in the setter!
This, my friends, is encapsulation. And it's why getters and setters exist =) By making our class fields private, we can ensure that they can't be set to invalid values, and by adding a setter method, we can ensure that they can only be set to valid values.
For what it's worth, java (a more "traditional" OOP language than JS or TS) has always been able to do this. I won't make this a java tutorial, but in java a private field has always been private:
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return this.age;
}
public void setAge(int newAge) {
if (newAge < 0 || newAge > 150) {
throw new Error("Invalid age")
}
this.age = newAge;
}
}
Person sarah = new Person("sarah", 30);
sarah.age = "thirty"; // Compiler says no
sarah.setAge(-100); // Compiler says no
sarah.setAge(52); // Compiler says yes \o/
Lots of people seem to hate java, but it reads quite nicely to me. I'll be talking more about java in other posts, but this series is about OOP, not java specifically, so I'll leave it there for now.