Object-oriented programming
Object-oriented programming is based on four core principles:
- Abstraction: abstract behavior of objects is generalized into classes.
- Data encapsulation: properties and methods are encapsulated in classes and hidden from external access.
- Inheritance: properties and methods can be inherited by one class from another.
- Polymorphism: multiple forms – objects can take different forms depending on their use.
In object-oriented programming, a class represents a kind of blueprint for objects, a template from which individual instances (objects) can be created during program execution. Within a class, the developer defines the properties and methods that individual instances of an object should possess. Properties represent the state of the object instances, their methods, and their behavior.
Another analogy can be drawn. We all have some idea of a person, with a name, age, and other characteristics. The collection of such characteristics can be called a person template or class. The specific embodiment of this template may vary; for example, some people have one name, while others have another. And a real person (effectively an instance of this class) will represent an object of this class.
There are classic object-oriented languages, such as Java or C#. Some languages utilize OOP to varying degrees but are not purely object-oriented, such as JavaScript.
Let’s briefly consider OOP using Java and JavaScript as examples.
Abstraction.
In object-oriented programming, classes provide the foundation, or abstraction, for objects. Classes contain the shared state and behavior of objects. For example, we might want to represent a person in a program. Most programming languages use the class keyword to define classes. For example, in Java, we might define the following Person class, which represents a person:
|
1
|
class Person{ } |
A similar definition of the Person class in JavaScript:
|
1
|
class Person{ } |
When defining an abstraction of objects—a class—we abstract from the specific attributes of objects and identify their common characteristics and behaviors. The set of common attributes of objects constitutes the state of the class. For example, we might identify attributes such as a person’s name and age. These attributes represent the state. Classes typically use fields or variables to define state (in some languages, these are called properties; in others, properties and class fields are separated). For example, in Java, we might define state as follows:
|
1
2
3
4
5
|
// class of personclass Person{ String name; // person's name int age; // person's age} |
Here, the variable name is a String and stores the person’s name. The variable age is an int or number and stores the person’s age.
Similar definition in JavaScript:
|
1
2
3
4
5
|
class Person{ name; //Name age; // age} |
A person can perform certain actions. For example, a person can walk, sleep, eat, and so on. This is what’s called an object’s behavior. In the context of our program, let’s say a person’s behavior is limited to what they say, their name, and their age. Methods are defined to define the behavior/actions of a class. For example, let’s add a say method to the Person class, which allows a person to communicate information about themselves. Example in Java:
|
1
2
3
4
5
6
7
8
9
10
|
// class of personclass Person{ String name; // person's name int age; // person's age // a person provides information about himself public void say(){ System.out.printf("My name is %s \n", name); System.out.printf("To me%d years \n", age); }} |
In the say method, we simply print the class field data to the console using the System.out.printf() method.
A similar example for JavaScript:
|
1
2
3
4
5
6
7
8
9
|
class Person{ name; // Name age; // age say(){ console.log("My name is", this.name); console.log("To me", this.age, "years"); }} |
Thus, we’ve defined state (the name and age fields) and behavior (the say method) in the Person class. Now we can create objects of the Person class—specific people who will have similar states and behaviors. Typically, a constructor—a special method that initializes an object—is used to create objects. In many languages, you can use the default constructor, or you can define your own. For example, we want objects (specific people) to have their name and age pre-assigned when they’re created. To do this, we’ll add a constructor to the class. In Java, the constructor method is named after the class:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class Person{ String name; // person's name int age; // person's age public Person(String name, int age){ this.name=name; this.age = age; } public void say(){ System.out.printf("My name is %s \n", name); System.out.printf("To me%d years \n", age); }} |
Here, the constructor receives values for the variables of the same name from the outside via two parameters, name and age.
In JavaScript, a method with the special name constructor is used to define a constructor:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Person{ name; // Name age; // age constructor(name, age){ this.name = name; this.age = age; } say(){ console.log("My name is", this.name); console.log("To me", this.age, "years"); }} |
Thus, we have a ready-made abstraction of a person in the form of the Person class, and we can use it to create an object of this class. For example, creating objects in Java:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public class Program{ public static void main(String[] args) { // create an object of the Person class Person tom = new Person("Tom", 39); // we call the object's say method tom.say(); // we create a second object of the Person class Person sam = new Person("Sam", 25); // we call the object's say method sam.say(); }}//class of personclass Person{ String name; // person's name int age; // person's age public Person(String name, int age){ this.name=name; this.age = age; } public void say(){ System.out.printf("My name is %s \n", name); System.out.printf("To me%d years \n", age); }} |
A similar program in JavaScript:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class Person{ name; //Name age; // age constructor(name, age){ this.name = name; this.age = age; } say(){ console.log("My name is", this.name); console.log("To me", this.age, "years"); }}// create an object of the Person classconst tom = new Person("Tom", 39);// we call the object's say methodtom.say();// we create a second object of the Person classconst sam = new Person("Sam", 25);// we call the object's say methodsam.say(); |
In both versions, two objects are created—Tom and Sam—and the say method is called. In both languages, object creation is essentially the same: the new keyword is used, followed by a constructor call, passing in values for the parameters. In other languages, object creation may differ, but in principle it will be the same.
In both cases, we’ll get the same console output.
My name is Tom
I’m 39 years old
My name is Sam
I’m 25 years old
Encapsulation
Data encapsulation (or information hiding) is the grouping of properties and methods into classes, keeping implementation details hidden and protected from unwanted modification. Encapsulation often includes object state, as well as methods that should only be used within the class. For example, in the example above, the Person class has an age field, which stores age. When accessing the age field directly, it can be assigned any value, including an incorrect age value, for example:
|
1
2
|
const tom = new Person("Tom", 39);tom.age = 12345; |
Instead of direct access, a class provides special methods for setting and getting field values. These access methods can protect against assigning invalid values to fields.
In programming languages such as Java, special keywords—access modifiers—can be used to control access to class fields from outside. For example, the private keyword, when applied to class fields/methods, makes them inaccessible from outside:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
class Person{ private String name; // private field is accessible only within the class private int age; //private field is accessible only within the class public Person(String name, int age){ this.name=name; this.age = age; } public void say(){ System.out.printf("My name is %s \n", name); System.out.printf("To me %d years \n", age); }}public class Program{ public static void main(String[] args) { Person tom = new Person("Tom", 39); tom.age = 12345; // ! Error - The age field is not accessible outside the Person class }} |
Other languages may use different tools to hide class state and behavior from the outside. For example, in JavaScript, you can close fields and methods within a class by prefixing the field/method names with the hash symbol #:
class Person{
#name; // name
#age; // age
constructor(name, age){
this.#name = name;
this.#age = age;
}
say(){
console.log(“My name is”, this.#name);
console.log(“I am”, this.#age, “years old”);
}
}
const tom = new Person(“Tom”, 39);
tom.#age = 12345; // ! Error – the #age property is not accessible outside the Person class
However, even an encapsulated state may need to be accessed. For example, we might want to set new values for the age property if they represent a valid age. Or we might want to retrieve the value of the name property.
For this purpose, a class typically defines special accessor methods that mediate access to private fields. For example, in Java:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public class Program{ public static void main(String[] args) { Person tom = new Person("Tom", 39); System.out.println(tom.getName()); // Tom System.out.println(tom.getAge()); // 39 tom.setAge(22); System.out.println(tom.getAge()); // 22 tom.setAge(1222); System.out.println(tom.getAge()); // 22 }}// class of personclass Person{ private String name; // person's name private int age; // person's age public Person(String name, int age){ this.name=name; this.age = age; } // method to get name public String getName(){ return name; } // method to get age public int getAge(){return age;} // method for setting age public void setAge(int value){ // If the passed value represents a valid age, we change age if(value > 0 && value < 110) age = value; } public void say(){ System.out.printf("My name is %s \n", name); System.out.printf("To me %d Years \n", age); }} |
Here, the getName method is defined to retrieve the name, the getAge method to retrieve the age, and the setAge method to set the age. The setAge method changes the age if it represents a valid value (from 1 to 109). A setter method could also be defined for the name field, but in this case, let’s assume the name property will be read-only (in real life, names aren’t changed very often).
In this case, the getName/getAge/setAge methods are also called accessor methods. The getName/getAge methods are called getters because they retrieve a value, and the setAge method is called a setter because it sets a value.
Different languages may have special constructs for defining getters and setters. For example, JavaScript has special get and set constructs:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
class Person{ #name; // имя #age; // возраст constructor(name, age){ this.#name = name; this.#age = age; } get name(){ // getter for name return this.#name; } set age(value){ // setter for age if(value > 0 && value < 110) this.#age = value; } get age(){ // age getter return this.#age; } say(){ console.log("My name is", this.#name); console.log("To me", this.#age, "Years"); }}const tom = new Person("Tom", 39);console.log(tom.name); // Tom console.log(tom.age); // 39 tom.age = 22;console.log(tom.age); // 22tom.age =1222;console.log(tom.age); // 22 |
Inheritance
The third principle of OOP is inheritance, which assumes that one class can inherit fields, properties, and methods from another. For example, above we defined a Person class. Now, let’s define a class for an employee of a certain company. An employee essentially has the same characteristics as a Person—name, age, and behavior—with the addition of its own attributes, such as the name of the company where the employee works. In this case, we can create an employee class and inherit from the Person class. Different languages can use different operators to establish inheritance between classes. In Java, the extends operator is used for this:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
public class Program{ public static void main(String[] args) { Employee sam = new Employee("Sam", 25, "Google"); sam.say(); }}// class of personclass Person{ private String name; // person's name private int age; // person's age public Person(String name, int age){ this.name=name; this.age = age; } public void say(){ System.out.printf("My name is %s \n", name); System.out.printf("To me %d years \n", age); }}// Employee is inherited from the Person class.class Employee extends Person{ private String company; // employee's company public Employee(String name, int age, String company){ super(name, age); // we call the constructor of the Person class this.company = company; } //Override the say method public void say(){ super.say(); // we call the say method from the Person class System.out.printf("I work in %s \n", company); }} |
Here, the Employee class is defined, which inherits from the Person class and adds a field called “company” to store the company name. In this regard, the Person class is also called a base class, parent class, or superclass. The Employee class is also called a descendant class, derived class, or subclass.
To access this property externally when creating an employee, the Employee class defines its own constructor, which accepts three parameters: name, age, and company. In a number of common object-oriented languages, the descendant class’s constructor must call the base class’s constructor. In Java, this is done using the super keyword.
|
1
|
super(name, age); |
Furthermore, a child class can not only inherit functionality but also override it if needed. In this case, we’re overriding the say method, which is accomplished by using the @Override annotation in Java. For example, we want to display the employee’s company in addition to their name and age:
|
1
2
3
4
5
|
@Overridepublic void say(){ super.say(); // we call the say method from the Person class System.out.printf("Я работаю в %s \n", company);} |
Here, we first access the “say” method of the Person base class, super.say(). This, firstly, avoids unnecessary code duplication (no need to write lines to display the name and age to the console). Secondly, the “name” and “age” fields in Person are private and therefore inaccessible in derived classes. The keyword “super” is also used to access base class functionality in Java.
A similar program in JavaScript:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
class Person{ #name; // имя #age; // возраст constructor(name, age){ this.#name = name; this.#age = age; } say(){ console.log("My name is", this.#name); console.log("To me", this.#age, "Years"); }}// the Employee class inherits from Personclass Employee extends Person{ #company; // employee's company constructor(name, age, company){ super(name, age); // we call the constructor of the Person base class this.#company = company; } say(){ super.say(); // calling the implementation of the say method from the Person class console.log("I work in", this.#company); }}const sam = new Employee("Sam", 25, "Google");sam.say(); |
The console output of programs in both languages will be identical:
My name is Sam.
I’m 25 years old.
I work at Google.
Polymorphism
Polymorphism represents the ability of objects to assume a different type (form) or present themselves as a different type depending on the context or usage. Applying inheritance to classes allows for the establishment of an “is-a” relationship. That is, an employee is a person, or an Employee object is a Person. Accordingly, if, for example, a function expects a Person object, both Person and Employee objects can be passed as arguments:
So, let’s define the following Java program:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
public class Program{ public static void main(String[] args) { Person tom = new Person("Tom", 39); Employee sam = new Employee("Sam", 25, "Google"); printPerson(tom); printPerson(sam); } public static void printPerson(Person person){ person.say(); System.out.println(); }}// класс человекаclass Person{ private String name; // person's name private int age; // person's age public Person(String name, int age){ this.name=name; this.age = age; } public void say(){ System.out.printf("My name is %s \n", name); System.out.printf("To me%d Years\n", age); }}//Employee is inherited from the Person class.class Employee extends Person{ private String company; // employee's company public Employee(String name, int age, String company){ super(name, age); // we call the constructor of the Person class this.company = company; } // Override the say method public void say(){ super.say(); //we call the say method from the Person class System.out.printf("I work in %s \n", company); }} |
Here, the printPerson method takes a Person object:
|
1
2
3
4
|
public static void printPerson(Person person){ person.say(); System.out.println();} |
But since the Employee object is also a Person object, you can also pass an Employee object to this function.
|
1
2
3
4
|
Person tom = new Person("Tom", 39);Employee sam = new Employee("Sam", 25, "Google");printPerson(tom);printPerson(sam); |
For JavaScript, due to weak typing, the principle of polymorphism is less characteristic, since we can pass any object to a function parameter:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
class Person{ #name; // Name #age; // age constructor(name, age){ this.#name = name; this.#age = age; } say(){ console.log("My name is", this.#name); console.log("To me", this.#age, "Years"); }}class Employee extends Person{ #company; // employee's company constructor(name, age, company){ super(name, age); // we call the constructor of the Person base class this.#company = company; } say(){ super.say(); // calling the implementation of the say method from the Person class console.log("I work in", this.#company); }}function printPerson(person){ person.say(); console.log("\n");}const tom = new Person("Tom", 39);const sam = new Employee("Sam", 25, "Google");printPerson(tom);printPerson(sam); |
Console output of programs in both languages:
My name is Tom
I’m 39 years old
My name is Sam
I’m 25 years old
I work at Google