The Fundamentals of OOP
Concepts and Examples.
Object-Oriented Programming is a concept that is usually taught in one of the initial lessons or courses in computer science. This is because it lies the foundation for how upcoming languages will work (fundamentally). As for myself, I was taught this approach and its fundamentals right before diving into Ruby.
The best way to describe OOP in simple terms is that it’s all about creating objects that contain both data and methods. It refers to a type of software design in which the programmer defines the data type of a data structure and also the types of functions (or operations) that can be applied to the data structure. There are four principles of object-oriented programming: encapsulation, inheritance, polymorphism and abstraction. We’ll take a deeper dive into each of these fundamentals in this blog.
Let’s say we have a program, which contains several interlinked objects ( according to the rules defined in the program). Encapsulation can be achieved when each object keeps its state private, inside of a class. This means that other objects do not have direct access to this state. Instead, they are only able to call a list of public functions, which are called methods. The object manages its own state through these methods and no other class can touch it unless explicitly allowed. So, if you want to communicate with the object, you should use the methods that are provided.
Let’s give a more real-life example. Imagine we’re building a game where there are people and there are coins (that they have to earn). Both of these communicate with each other. If we want to apply encapsulation, we encapsulate all the “coin” login into a Coin class, like so:
Here, you can play a coin but you cannot directly change the color or country the coin comes from. The “state” of the coin are the private variables: color and country. It also has a private method earn(). It can call it whenever it wants, the other classes can’t tell the coin when to be earned. However, what they can do is defined in the public methods play() and toss(). Each of these functions modifies the internal state somehow and may invoke the earn() method. This is how the binding between the private state and public methods are made.
Takeaway: encapsution = private state, public methods
It is completely fair to think of abstraction as a natural extension of encapsulation. In OOP, programs are often extremely large and separate objects communicate with each other a lot. Thus, maintaining a large codebase can be difficult. Enter abstraction — a concept aiming to ease this problem.
Integrating abstraction into your program means that each object should only expose a high-level mechanism for using it. This mechanism should hide internal implementation details. It should only reveal operations relevant for the other objects to see.
Let’s take a more real-life approach to understand abstraction a bit more clearly. Think of a coffee maker. It is responsible for a lot of action and makes weird noises under the hood. However, as a consumer all you have to do is put in the coffee and press a button. Ideally, this mechanism should be easy to use and should rarely change over time. You can think of this process as a small set of public methods which any other class can call without “knowing” in particular, how they work — they just need it to work, similar to how we just need our coffee to be ready in the morning!
Another good example of abstraction can be in how we use our phone. If you think about it, all we have on our phone as a user are a few buttons and inputs to use like a home, volume button and a charging input. But what happens under the hood when we utilize these? Well, we don’t have to know. The implementation details are hidden because we only need to know a short set of actions.
Up until now, we’ve seen how encapsulation and abstraction can help in both developing and maintaining a big codebase. But there are some other common problems that need to be tackled in object-oriented programming.
Let’s look at objects. Objects are structured very similar to one another (often). They share common logic within a program but are not entirely the same. Inheritance helps to reuse this common logic that can become repetitive if we start building them out in each object. In order to reuse common logic and extract the unique logic into a separate class, we use inheritance. This basically means that we create a child class by deriving from another parent class, forming a hierarchy. The child class is able to reuse all the fields and methods of the parent class and can also implement its own.
Let’s take an example of a school program with a teacher and student objects. If the program needs to manage something like public and private teachers, but also students we can implement a class hierarchy. This way each class adds only what is necessary for it while reusing common logic with its parent classes. So if we have a main Teacher class, it can serve as a parent to build the Private teacher and Public teacher classes. The same goes for students. There can be a main Person class that student and teacher classes derive from.
takeaway: abstraction = the process of picking out(abstracting) common features of objects and procedures
The direct meaning of polymorphism literally means ‘many shapes’ in Greek. Now that we know how inheritance works and how it can be useful, it can come with a slight problem in some cases. Let’s say we have a parent class and a few child classes which inherit from the parent. Sometimes we’d want to use a collection, like a list, which contains a mix of all these classes. Or we have a method implemented for the parent class and would like to use it for the children too. This is where we use polymorphism.
Polymorphism provides a way to use a class like it is the parent so that there is no confusion with mixing types, but each child class keeps its own methods as they are. This happens by defining a parent class interface to be reused — outlining a bunch of common methods. Then, each child class implements its own version of these methods. So anytime a collection (such as a list) or method expects an instance of the parent (where the common methods are outlines), the language takes care of evaluating the right implementation of the common method.
Let’s try to visualize this to understand a bit better. Say we have an initial (parent) Figure class that includes common interface logic for calculating the surface area and perimeter of different shapes. We also have triangle, circle and rectangle child classes that inherit this logic. Using polymorphism, we can create a list of mixed triangles, circles and rectangles treating them like the same type of object. Furthermore, if the list attempts to calculate the surface area for an element, the correct method is found from the parent class for the pertaining shape. So if we need to calculate the SA of the triangle, we will use the method for triangle surface area calculation (same with the circle and rectangle). This will allow you to write your main logic only once, in the parent class and you don’t have to define it there separate times for the same functionality.
takeaway: polymorphism = ability to process objects differently depending on their data type or class
And that’s it! Those are the four main concepts. I hope the examples added a bit of ease in understanding these ‘complex’ analogies. Putting a bit of real life context always helps in understanding text-heavy concepts (at least for me).
Thank you for reading!