Umar Lone

Object-oriented programming (OOP) is a programming paradigm that focuses on using objects as building blocks of software. This paradigm closely resembles how humans perceive objects in real life, thus reducing the complexity associated with software development.

Why object-oriented programming is valuable

Computer programs can be written in various programming languages. These languages used different concepts & paradigms to structure code in the program. In modern software development, object-oriented programming languages are commonly used for writing huge & complex software. Object-oriented programming was developed as a method for reducing complexity in software development, making it easier to build and scale large software programs.  

Object Oriented Analysis, Design & Programming with UML

Last Updated November 2020

Bestseller
  • 126 lectures
  • All Levels
4.3 (349)

Learn OO Analysis, Design & Programming using UML in C++, Java & C# | By Umar Lone

Explore Course

Software development is inherently complex. Some factors that contribute to complexity include:

It is next to impossible to completely resolve the complexity associated with software development, but it can be reduced to a large extent.

Resolving complexity: algorithmic decomposition vs object-oriented decomposition

Humans resolve complexity in different ways. When faced with a problem, we tend to simplify it. One process of simplification is called “divide et impera” in Latin, which means “divide and rule.” Under this principle, the problem is decomposed into smaller parts. For each part, we create a solution and link them together. This is quite similar to how we solve complex arithmetic problems.

This is a common way to resolve complexity. It’s no wonder that the early programming languages like Cobol, Pascal, and C are based on this concept. The solution is implemented as a set of algorithms in which each algorithm is represented by a function or a procedure. That is why these languages are called procedural languages. Such languages use functions as the basic building block of the program. This is also called top-down design.

These languages decompose the problem and its solution as a set of algorithms. This is called algorithm decomposition. Procedural languages use the process of algorithmic decomposition to resolve complexity.

For large scale software development, algorithmic decomposition does not scale well. It cannot manage the complexity of developing a large application. The application is made up of functions that are invoked one after the other. This makes them tightly coupled and dependent on each other, leading to the following issues:

Software using algorithmic decomposition can be represented graphically as follows:

This increases the complexity of software development through algorithmic decomposition.

However, there is another way through which humans deal with complexity. When we interact with things, we tend to think of them as objects that provide some functionality. Not only that, we create a simplified view of the objects, ignoring details that are irrelevant to us. This process is called object-oriented decomposition because the system is viewed as a set of collaborative objects, and each object has a role to play. This is also called a bottom-up design.

A system that is composed of objects can be represented by the following figure.

Each rounded block represents an object. The lines represent interactions between objects. Each object:

This shows that objects communicate with each other through messages (represented by lines). This closely matches how we perceive and interact with other objects in real life. We deal with the behaviors of objects without considering the internal implementation details. For example, when we drive a car, we don’t deal with its internal implementation. Instead, we drive it by sending messages, such as steer, accelerate, and brake. Thus the entire process of driving becomes simplified.

In the same way, object-oriented decomposition simplifies the process of software development. It helps manage the complexity of large software projects.

Let us compare algorithmic and object-oriented decomposition:

Algorithmic DecompositionObject-Oriented Decomposition
System is broken into algorithms implemented as functions/proceduresSystem is broken into collaborative objects
Basic building block is a functionBasic building block is an object
Functions rely on data which may be globalObjects have their own data
Data is global, thus can be modified by anyoneData is private to an object and only that object can modify it
Functions communicate by calling each other in a sequenceObjects interact with each other through message passing (method calls)
Functions are tightly coupled with each otherObjects are loosely coupled with each other
Functions cannot be reused easilyEasy to reuse objects
Adding new features is difficultNew features can be added through new objects
Cannot represent real-life entities or simulate their interactionReal-life entities can be represented through objects without unnecessary details
Uses top-down designUses bottom-up design
Doesn’t scale well for large softwareWell-suited for large software

This comparison clearly displays why object-oriented decomposition is a better way of resolving software complexity. At the heart of object-oriented decomposition is an object. We discuss this in depth in the following section.

Defining an object

An object is an identifiable item or entity that may be real or abstract and provides some functionality in the problem domain.

An object may be a physical object that exists in the real world. Examples of real-world objects are a tree, car, or a house. It may also be an abstract object without physical form. It may not be tangible, but still exists and represents something important. For example, a bank account is an abstract object. It does not have a physical form but represents the funds of an individual. In software systems, we use abstract objects to define specific entities in a program.

Every object has the following three characteristics:

Let us understand these characteristics using the example of a speaker system.

State 

The state of an object is made up of its properties and can be static or dynamic.

The state of the speaker system may include properties such as:

Volume is a dynamic property that can change over time, while the other properties listed are static and will never change.

The properties may have the following values:

Output Power100 Watts
ConnectivityBluetooth
ManufacturerLogitech
Serial numberABCD1234
Type5.1
Volume50 %

The property values represent the state of the object — in this case, the values above represent the state of a specific speaker system. 

Identity 

The identity of an object can be represented through a property that can uniquely identify that object. In our example of a speaker system, the unique identity is provided through the serial number.

Behavior 

Every object will provide some functionality as a part of its responsibility, and that functionality is represented by its behavior. The speaker system will provide the following behaviors:

Invoking these behaviors will cause the speaker system to respond in some way. Note that we don’t deal with the internal implementation while invoking the behaviors.

An object is created from a class 

In a programming language, an object comes from a class. Any number of class objects can be created. A class is a template or blueprint that lists the properties and behaviors of its objects. A class contains data and functions. Functions operate on the data through the object.

Objects are created by instantiating the class. This process initializes the properties with values that can be modified by invoking the object’s behaviors. The values in the properties constitute the state of the object. Every time a class is instantiated, it will create a new object with a new state.

Objects are therefore the basic building blocks in object-oriented programming.

Object-oriented programming

OOP is a programming paradigm that uses classes and objects as its building blocks. It is based on a conceptual framework called object model. This model contains four major elements that define the object-oriented programming concepts:

Languages that implement the elements of the object model are called object-oriented languages, such as Java, C++, C#, and Python. Even though these languages support these features, without understanding this conceptual framework, programming in these languages will still look like algorithms rather than classes and objects. If this happens in your work, most likely, you have not understood the problem or the domain correctly. Your design will still have a procedural feel and contain functions/procedures which will make it hard to extend, reuse, and maintain.

Let’s understand the four elements of the object model.

Abstraction 

Abstraction is one of the fundamental methods humans use to deal with complexity. It allows us to focus only on relevant details while ignoring or suppressing unwanted ones. This lets us form an abstraction that represents the entire essential behavior of an object.

In an object model, abstraction can refer to both a process and an entity. Abstraction as a process deals with extracting important details of an item. Abstraction as an entity refers to the outcome of the abstraction process.

Performing abstraction is the first step towards object-oriented programming. This allows us to focus on important aspects while ignoring the unimportant ones. A class represents an abstraction as an entity in object-oriented languages. Classes may represent a real-life object with required behaviors. Such classes are created through entity abstraction, as they directly represent objects that exist in the vocabulary of the problem domain, thereby simplifying our understanding of the system.

Take an example of a first-person-shooter computer game. The goal of the player is to reach the edge of a forest, which is inhabited by monsters. The player has a gun for defense and offense against the monsters. If we perform object-oriented decomposition, we find the following main objects in the game:

The next step is to represent these objects in an object-oriented programming language. However, we must ascertain the level of details we want in these objects. Let’s focus only on the player for simplicity. A player is a person that controls the game. A person has a long list of properties, such as:

Most of these are not required in a game and are irrelevant in this domain. The relevant details of a person controlling the player would be:

There could be more depending on the type of game, but for this example, these are the most important ones in our game.

This is the outcome of abstraction. We have created an entity with relevant details and ignored the rest because they are not required. This decreases the complexity associated with the object.

Let’s implement the abstraction in a Java program. It is a very popular language and easy to learn. There are innumerable Java tutorials & thousands of books available that will help you learn & write programs.

Now, we can represent the player through a class..

class Player {
     	public int health=100 ;
     	public String name ;
};

You can specify any data type that is relevant to your abstraction. In this abstraction, int & String type are relevant to the information we want to capture & represent the concept of data for the Player. These two data types become members of the class and are called fields or data members.

After creating the instance of Player, we can assign the name and health for this specific player. But there are a few issues with this class:

Abstraction in itself is not very useful, as the clients can see the internals. Therefore, we’ll hide its internal details through Encapsulation

Encapsulation 

Encapsulation is the logical step after abstraction. It hides the implementation details of an abstraction. It is also called information hiding, as the information on how the class is implemented is hidden from the external world. The abstraction, in turn, is responsible for providing its functionality to the clients.  

So, the next step in our example is to add implementation to Player class without exposing the details. Let us modify the Player class and encapsulate it.

class Player{
  	private int health = 100 ;
  	private String name ;
  	public void attack(Enemy enemy) {
        	//implementation
  	}
  	public void takeDamage(int damage) {
        	health -= damage ;
        	if(health < 0)
              	health = 0 ;
  	}
  	public int getHealth() {
        	return health ;
  	}
 	public boolean alive() {
        	return health != 0 ;
  	}
}

The abstraction is now encapsulated by adding methods (functions inside classes). Since these methods are part of the class, they can access the private members, including fields, of the class. 

With this, we’ve created a binding of fields and methods. These will work together to provide the functionality of the class.

The internal details of the class are now hidden through encapsulation. All the data members (fields) are private, making them inaccessible outside the class.

A class that encapsulates its implementation has several advantages: 

Assume we decide to add support for armor later in the game design. We can modify the implementation of takeDamage() method to account for less damage to the Player when it has an armor. The clients don’t have to see this change in the implementation of takeDamage() method.

We can also implement an Enemy class as follows:

class Enemy{
  	int health = 100 ;
  	int strength ;
  	public void takeDamage(int damage) {
        	health -= damage ;
  	  	if(health < 0)
              	health = 0 ;
  	}
  	public void attackPlayer(Player player) {
        	//Implementation
  	}
}

The collision system of the game will invoke the appropriate behaviors on the Player and Enemy objects depending on the gameplay.

Finally, remember that abstraction and encapsulation go hand in hand. Abstraction provides the implementation and encapsulation hides its implementation details. 

Hierarchy 

This refers to the relationship between classes and/or objects. Objects in software, just as in real life, do not exist in isolation. They interact with other objects and may exhibit some relationship. Assigning objects a hierarchy simplifies our understanding of how the objects collaborate with each other. There are two ways through which relationships can be defined – object composition & class inheritance.

If an object has the functionality that another object requires, we can use the composition relationship. In composition, an object will be composed inside another object. This is also referred to as a part-of relationship. This relationship is exhibited by objects.

In our example, the Player class may attack the monsters with a gun. The shooting behavior can be provided by another class called Gun. Instead of reimplementing this functionality, the Player class will simply reuse it from Gun. Consequently, the Player class will compose the Gun instance. 

class Gun{
  	int bulletDamage = 10 ;
  	int bulletCount = 50 ;
  	public int shoot() {
        	//reduce bullet count
        	return bulletDamage;
  	}
  	//Other methods, such as reload, pickammo, etc
}
 
class Player{
  	private Gun gun = new Gun() ;
  	private int health = 100 ;
  	private String name ;
  	public void attack(Monster monster) {
        	int damage = gun.shoot() ;
        	monster.takeDamage(damage);
  	}
	//Rest of the implementation

This way, the Player class does not have to reimplement the gun functionality. It can reuse the functionality by simply composing the Gun instance.

The other relationship is created through generalization. A generalization represents common features of different individual objects. For example, a car is a generalization of different types of cars — it represents all the common properties and behaviors of cars. So, we can say that a BMW or Tesla is a kind of car. Both provide behaviors of a car, but in different ways. This is called “kind of” or “is a” relationship. In object-oriented programming, we refer to it as inheritance. This relationship applies to classes.

Inheritance allows us to put common features of different classes into one generalized class. The generalized class is called a super or base class. Other classes will inherit the features of this class, and are called sub classes or child classes. The base class represents all the features that its child classes may provide, albeit in different ways. This ordering of classes is called class hierarchy.

The advantages of this approach are:

In our computer game example, we want to add different kinds of enemies to keep the gameplay interesting. Each enemy will have its own properties and a different way of attacking the player. A few enemies would be:

EnemyMode of Attack
Werewolfclaws
Dragonfire

To attack these enemies, the Player class has to know about their corresponding classes. This will require modification of the Player and a tight coupling between the classes. If we add more classes in the future, we’ll have to repeatedly modify the Player class. Each modification may introduce bugs and break the code in unexpected ways. Bugs may accumulate to an extent where the software ultimately fails. 

This dependency can be represented graphically:

As far as possible, we should try to minimize dependency between classes. The arrows show the dependency of the Player class on Werewolf and Dragon classes.

Werewolf and Dragon share some properties and behaviors. For example, both will have health and strength. Both will attack the player but in their own ways. Therefore, we should be able to generalize them by creating a class that contains common features of all enemy classes. We’ll call this class Monster

class Monster{
  	private int health = 100 ;
  	private int strength ;
  	public Monster(int strength) {
        	this.strength = strength ;
  	}
  	public void takeDamage(int damage) {
        	health -= damage ;
        	if(health < 0)
              	health = 0 ;
  	}
  	public void attackPlayer() {
        	//Implementation
  	}
}

In Java, we use the keyword extends to represent the inheritance relationship. Here is the implementation of Werewolf and Dragon classes:

class Werewolf extends Monster{
  	public Werewolf(int strength) {
        	super(strength);
  	}
 
  	public void attackPlayer() {
        	//Attack with claws
  	}
}
 
class Dragon extends Monster{
  	int fireDamage ;
  	public Dragon(int strength,int fireDamage) {
        	super(strength);
        	this.fireDamage = fireDamage ;
  	}
  	public void attackPlayer() {
        	//Implementation
  	}
}

The child classes inherit features of the Monster but provide their own implementation of the attackPlayer() method. This is required because they attack the player in different ways. Consequently, they may add their own implementation, in addition to what they inherit from Monster. You may notice that Dragon class has an attribute that supports its mode of attack. However, takeDamage() method implementation will be common for all the classes and doesn’t need to be re-implemented in the child classes.

Now, the only question is how will the Player class interact with these classes? The answer is through the base class Monster. Remember, Monster represents a generalization of all subclasses. Its object can hold a reference to child class objects. 

Monster monster = new Werewolf(20) ;
monster.attackPlayer();

In this case, a Monster object holds a reference to Werewolf. The child type, Werewolf, gets converted to Monster through a process called typecasting. This simply means a conversion of one type to another. In this particular case, the type of cast is called upcasting, as the cast is from child to parent. This cast is applied automatically when a child instance is assigned to a base class reference. This allows us to use the Werewolf object through the Monster reference.

Now, the Player only needs to interact with the Monster class and does not have to deal with its subclasses.

This can be shown in the following figure:

Note how the dependency is reduced to just one connection between the Player and the Monster base class. Now we can add more monsters to the game without modification to the Player class.

The Player class should be able to send messages (invoke methods) on different kinds of monsters without knowing who they are. We’ll update the attack() method in the Player as follows:

public void attack(Monster monster) {
  	monster.takeDamage(20);
}

You might wonder what will happen when the takeDamage() method is invoked on the monster reference. It should invoke the method of the correct object and not the Monster class. This can be achieved through polymorphism.

Polymorphism 

Polymorphism means “many forms.” This is related to a message passing between objects. When an object sends a message to another object, the target object should respond correctly. If the target object is a base class object that internally refers to a child object, the message will automatically go to the child object.

In our case, the message is the method call: takeDamage(). The Werewolf and Dragon classes have re-implemented the takeDamage() method and provided their own implementation. This is called method overriding, which is not to be confused with method overloading. The methods that are overridden are said to be polymorphic.

When this overridden method is invoked on a Monster reference, it will invoke the method of the underlying object and not the Monster reference. It is unknown at compile-time which object’s method will be invoked. This resolution will take place at runtime. This is also called dynamic polymorphism.

In Java, all methods are polymorphic by default and can be overridden. In other languages, you may have to use keywords to indicate the override behavior.  For example, C# and C++ use the virtual keyword. Note that only polymorphic methods can be overridden.

However, in Java, we use the @Override annotation to indicate our intent to override. Note that its usage is optional.

class Werewolf extends Monster{
  	public Werewolf(int strength) {
        	super(strength);
  	}
  	@Override
  	public void attackPlayer() {
        	//Attack with claws
  	}
}

In other cases, you can also implement compile-time polymorphism. The method to be invoked is decided at compile-time, usually through the arguments to the method. This is implemented as function overloading in object-oriented languages.

Polymorphism has several advantages:

Conclusion

Most languages used in modern software development implement the conceptual framework of the object model. Such languages allow us to create large-scale software. They help reduce the complexity through abstraction, hierarchy, and other elements. 

These are natural ways for humans to deal with complexity. Integrating the known elements into languages reduces the overall complexity associated with software development.

The development of an application does not end after the product is shipped. The software just enters a different phase of maintenance — this maintenance simply means a change to the existing software. It includes bug fixes, the addition of new components, deprecation of old or unwanted features, and so forth. Making these changes in the software that is already deployed is difficult. These changes may have to be incorporated during the development phase as well, which presents its own set of challenges.

If the software is written with the conceptual framework of the object model, the software components will be loosely coupled. This simplifies modifications to existing software, as the changes will be localized to a specific component. Its modification will not affect other parts of the software. In the same way, we will be able to add new features by simply adding new classes.

So we’ve seen how object-oriented programming simplifies the overall process of software development. That is why this is the most common programming paradigm used in modern software development. 

However, there are alternative methods to object-oriented programming, such as functional programming, generic programming, and procedural programming. Most modern languages support multiple paradigms that the programmers leverage to write more complicated programs.

Page Last Updated: October 2020

Object Oriented Programming students also learn

Empower your team. Lead the industry.

Get a subscription to a library of online courses and digital learning tools for your organization with Udemy for Business.

Request a demo