Introduction to Object-Oriented Programming (OOP)
Learn the basics of OOP.
We'll cover the following
Introduction
In this lesson, we’ll introduce the object-oriented programming (OOP) paradigm.
So far, we’ve discussed procedural/structured programming, which portrays its modularity in terms of functions and structures, which group together various variables under a single category. OOP is a paradigm that emphasizes the organization of code around objects and encapsulates both data and the operations that can be performed on that data. It provides a better data abstraction than procedural and structured programming. It introduces the concept of data hiding, which makes the code more modular and easier to maintain, allowing reusability and scalability.
OOP concepts
In OOP, classes, objects, abstraction, encapsulation, and object relations (such as composition, association, and aggregation) enhance code organization and design. While inheritance and polymorphism are important aspects, we won’t cover them in this course. Our focus will be on the foundational aspects of OOP.
Classes
In the realm of object-oriented programming (OOP), two essential concepts are abstract data types (ADTs) and cohesion. ADTs provide a conceptual framework for defining classes and objects, while cohesion refers to the idea of keeping related parts of a codebase together.
ADTs in OOP serve as blueprints for creating classes. For example, consider the concept of a “car” as a class. The attributes of the Car
class can include properties such as color, model, and horsepower, which are all adjectives that describe the car. The behaviors or actions of a car, such as turning the engine on/off, controlling lights, accelerating, and braking, can be implemented as member functions within the Car
class.
We achieve high cohesion by organizing attributes and behaviors within a class based on their logical relationship. This means that related attributes and functions are kept together, enhancing code organization and readability. For instance, all the attributes related to the car’s appearance can be grouped together, while the functions related to controlling the car’s movements can be grouped separately. This approach ensures that the class remains focused and self-contained.
By utilizing ADTs and striving for high cohesion, OOP promotes a structured and modular approach to software development. Classes act as ADTs, providing an abstract representation of objects, their attributes, and behaviors. Cohesion aids in maintaining the integrity of classes by grouping related attributes and functions together, resulting in more organized and maintainable code.
So, in summary:
Nouns
Classes Adjectives
Member attributes Behaviors
Member functions Proper Nouns
Actual instances of corresponding classes
Structure vs. class
The high-level block diagram below compares the relationship between functionality and attributes in structured programming and OOP.
In software development, structures and classes serve as containers for organizing data. However, there are key differences in how functionality is added to these containers.
In structures (introduced in C language), we define the attributes that represent the data we want to store. However, if we want to add functionality or operations that manipulate this data, we need to write separate functions outside the scope of the structure. These functions exist independently and can be called on structured data from the main()
function or other parts of the code. They have no inherent association with the structure itself and require passing the structure’s data as parameters.
There are some issues with this approach:
Data becomes scattered and needs to be passed to various functions wherever it is required. Let’s consider a scenario where we have thousands of functions, and the data is spread out and passed through multiple functions. In such cases, if the data undergoes an intrinsic change, like the addition of new information, it becomes crucial to meticulously review all the corresponding functions where this data is received to ensure that the change is properly handled. This can lead to a challenging situation.
Additionally, when passing data to functions, we need to ensure that we pass the parameters with the appropriate data either by value or by reference, depending on whether the function needs to modify that data or not.
Classes in object-oriented programming offer a more integrated approach compared to structures in structured programming. In classes, functionality is associated directly with attribute members and defined within the class’s scope. This means that functions or methods that operate on the data are encapsulated within the class definition.
One of the advantages of this approach is that these functions have direct access to the object’s attributes without the need to explicitly pass them as parameters. This is made possible through the use of a pointer called the
this
pointer, which refers to the object itself. As a result, the object’s data can be seamlessly manipulated with pure abstraction.
Moreover, in object-oriented programming, classes can be designed to represent distinct entities or concepts, each with its own attributes and behaviors. By defining separate classes, relationships can be established between them, such as inheritance, composition, or association. Inheritance allows for specialized classes (derived classes) based on a more general class (base class), inheriting its attributes and behaviors. Composition enables the construction of complex objects by combining multiple classes as components. Association allows classes to interact with each other, often through method calls or data exchange.
Furthermore, object-oriented programming emphasizes the importance of low coupling, which refers to the degree of dependency between classes. Low coupling ensures that classes are loosely interconnected, reducing dependencies and promoting modularity. This approach enables better maintainability, flexibility, and reusability of code.
Object-oriented programming offers a cohesive approach by encapsulating data and functionality within classes. This ensures tight binding between the object’s behavior and its data, promoting intuitive and modular code organization. With defined relationships and low coupling, object-oriented programming enables code reuse, improves system design, and provides a powerful mechanism for building maintainable and flexible software systems, in contrast to the scattered data and complexity in passing parameters found in structured programming.
Remember: In C++,
struct
andclass
are equivalent. This means you can structure both data and functions inside astruct
definition, just like withclass
. However, the key distinction lies in the default access specifiers: in aclass
, everything isprivate
by default unless explicitly marked aspublic
orprotected
, whereas in astruct
, everything is public by default. When access specifiers are specified in astruct
, the scope is changed accordingly. This flexibility allows you to choose the appropriate keyword based on your specific needs while designing your code.
Objects
An object is an instance of a class. What does that mean? An object is to a class what a structure variable is to a structure. Objects encapsulate data and the operations or functions that can be performed on that data. Imagine making a Eye
class. The human body has two eyes, but the properties and function of both remain the same. Hence, we define an Eye
class and not Eyes
(plural). Instead, we make two separate objects of the same class, naming them left
and right
as shown in the below diagram.
The objects created from a class are referred to as instances. Each instance appears to have its own set of functions, like blink()
, setColor()
, and setVision()
, as well as attributes. In the upcoming lesson, we’ll explore how this behavior is achieved through the use of the this
pointer.
Abstraction
In OOP, abstraction aims to simplify complex systems by modeling them at a higher, more abstract level. It entails removing extraneous features and choosing and displaying an object’s or system’s essential components to the user. We can build models using an abstraction that accurately represents real-world items or relevant traits and behaviors of certain concepts.
In C++, classes are built upon this concept. Classes are used to make user-defined ADTs. Technically, all of our data types fall under the ADT category ( int
, string
, float
, and so on) because we are unaware of what implementation goes behind each data type, but we know how it works on an abstract level. For example, we made the Eye
class in the previous diagram that defines the attributes and behavior of a human eye. For a user to work with this user-defined ADT, they do not have to understand the internal workings of this class. By using the functions of the class, they can perform various operations, such as blink()
, setColor()
, and setVision()
without needing to go through the entire process of rewriting/understanding each function.
This abstraction property promotes code flexibility using these ADTs as we can enable the users to extend the code more modularly. Let’s see how. Let’s say that user 1 makes a class called Eye
, user 2 makes a class called Heart
, user 3 makes a class called Brain
, and so on, until all the human organs and limbs are covered. Here’s a brief list:
Now, a programmer wants to make a Human
class that has all the functionalities of a human being. In structured programming, the programmer needs to write all the struct objects and must know the functionalities associated with each struct object.
But here’s what we can do while using classes:
When we include the objects of the associated classes (such as
Heart
,Nose
,Brain
,Leg
,Eye
, andEar
) within theHuman
class, we achieve a higher level of abstraction. This abstraction allows us to seamlessly integrate the functionalities associated with each type of object without needing to understand the internal details of those functions.Similarly, any attribute object can be easily accessed through this abstraction. For instance, if we want to access the
legs
functionality ofH
, we can use expressions likeH.legs[0].walk()
to invoke thewalk()
function of theLeg
object associated with a specific leg ofH
.
This abstraction simplifies the usage of functionalities and attributes within the Human
class. It allows us to work with the Human
object and its associated objects as a cohesive unit without needing to delve into the intricate implementation details of each function. This promotes code readability, maintainability, and modularity, enabling us to design and interact with complex systems in a more intuitive and efficient manner.
A sample abstract implementation can be seen here:
class Heart {// Heart class implementationpublic:void pump() {// Implementation of heart pumping functionality}};class Nose {// Nose class implementationpublic:void smell() {// Implementation of smell functionality}};class Brain {// Brain class implementationpublic:void think() {// Implementation of thinking functionality}};// ... Implementation of Leg, Eye, and Ear classesclass Human {private:Heart heart;Nose nose;Brain brain;Leg legs[2];Eye eyes[2];Ear ears[2];public:void doHumanThings() {heart.pump(); // Accessing heart functionalitynose.smell(); // Accessing nose functionalitybrain.think(); // Accessing brain functionality// Accessing leg, eye, and ear functionalities through respective objectslegs[0].walk();eyes[1].see();ears[0].hear();}};
The provided code demonstrates the concept of abstraction through the use of classes. Each class, such as Heart
, Nose
, Brain
, Leg
, Eye
, and Ear
, encapsulates specific functionalities within their respective implementations. The Human
class serves as a higher-level abstraction that aggregates these individual objects. By invoking the member functions of the associated objects within the Human
class, such as heart.pump()
, nose.smell()
, and brain.think()
, we can access the corresponding functionalities without being aware of the internal details of their implementations. This abstraction allows us to work with the object of the Human
class as a unified entity, utilizing the functionalities of its constituent objects in a simplified and intuitive manner.
Have you noticed that we have used the labels public
and private
? These are access specifiers, which define the visibility and accessibility of class members. We’ll explore their advantages and their usage as part of the second most important element of the object-oriented paradigm.
Encapsulation
Encapsulation is a fundamental principle in OOP. It operates on the concept of hiding attributes and functions within a class. Why is this important? As we discussed in the timer example in the previous section, we don’t want an external command to modify the values of a running timer, and we want to protect the data integrity and ensure that in case of a very complex code, we don’t accidentally tamper with member variables by assigning them the wrong values. This concept can also be termed as data hiding. It eventually reduces code complexity and makes it easier to maintain.
In C++, classes allow us to restrict access to certain members. Classes enable us to manage the access of its members, dividing them into three categories: private, protected, and public (we’ll look at private and public members in more detail). What we’ve mostly seen in classes or structures are called public members. These attributes can be accessed and modified by the object of a class or other functions throughout the code.
Private members, on the other hand, are only accessible within the class itself. This means that, except for the class functions, these members can’t be accessed. This forms a proper hierarchy within the code, making it more organized. These members are used to encapsulate data and only provide implementation details of the class, improving data integrity and protecting sensitive information. Here’s an example of the Timer
class:
The above illustration makes our class members private and restricts user access. We’ve set the member attributes of the Timer
class to private
, ensuring that the user can’t directly access these members. Instead, we’ve defined some public
functions within the class; these act like a bridge to access and modify these private
members. However, it is important to note that this access through functions is limited to certain functionalities. Although we can access private
members through them, we don’t have complete freedom to do whatever we want with these private attributes. We can only start the timer (startTimer()
), reset the timer (reset()
), and display the current value of the timer (displayTime()
). This way, we ensure controlled access to the private
members, allowing only the desired behavior of a class.
In the next lesson, we’ll explore more about classes and their use cases in coding.