OOPs and Clean Code Part 1: Principles of Object Oriented Programming

Posted by

I see a lot of people thinking of programming as just another job, involving writing code to fix a problem. Even though it is those things, to me it’s more of an engaging craft, an art. There is code that works and gets the job done, and there is code that works and looks beautiful too. As with any art, it takes a lot of patience and experience to master it.

There’s a reason why books such as Clean Code exist and are so popular. Clean code is not just beautiful to look at, but it’s easy to maintain and extend as well – both of which add value to the business, and save money and time. Extending your working hours every week to fix an issue caused by bad code is not a sustainable practice. Instead, write clean code that doesn’t take a weekend to debug.

This article series is an attempt to give detailed inputs on what is good code, based on Geektrust’s coding help docs as well. The first article on OOPs covers the principles of OOPs. In the next one, I cover how to write good code using OOPs.

  • Introduction
    • What is object oriented programming?
    • Principles of object oriented programming
  • Writing clean code with OOPs (Do’s and Don’ts)
    • Object Oriented Modelling
    • Avoiding duplication of code
    • Class should do one thing
    • Designing maintainable and extendable code
    • Managing dependency
    • Composition VS Inheritance

What is object oriented programming?

Among the numerous programming paradigms available, Object Oriented Programming (OOP) is by far the most common. In OOP, the primary manipulation unit is an Object, which is an instance of a Class. An object can have various properties or “attributes” and can “behave” in a certain fashion.

The behaviours of objects are manipulated using methods, which are very much similar to functions in Functional Programming. Also, objects are mutable, as the values of the attributes can be changed using methods. 

There are certain principles of object oriented programming that one must follow when writing code in an OOP language or designing a system using  OOP. These principles are:

  • Inheritance
  • Encapsulation
  • Abstraction
  • Polymorphism

We’ll look at these principles before going into a few best practices in OOP in the form of Dos and Don’ts.

Principles of object oriented programming

Inheritance

Not all classes and objects are unique in terms of their properties and methods. More often than not, you can easily derive a few properties and methods as the common denominators from a few classes. These common properties and methods could be separated out and placed in a common class. All other classes that need these properties and methods can then “extend” these from that common class. In object oriented programming, this common class is called the base class or the parent class, and the classes that extend this base class are known as derived classes, children classes, or subclasses. Let’s look at an example.

Suppose you have an organization and want to write a program to manage all its employees. But as with all other organizations, you have employees from different departments and they all have different functions. But all of them have a few common identifiers.

Figure 1: Employee Class

Figure 1 above shows the definition of a class called Employee. This class has three properties that can identify an employee in your organization, and these properties will be common across employees from all departments. So you can extract these properties into the common Employee class.

Next, each type of employee can have a class specific to their departments. For example, you can have an Admin class that extends the Employee class. You can then add admin-specific methods or behaviours to that Admin class. Figure 2 below shows an example of that Admin class.

Figure 2: Admin class extending Employee Class

As you can see, the Admin class “extends” the Employee class. This will automatically add all properties and methods from the Employee class to the Admin class. So if you want to add a name to an Admin object, you can call the setName() method on an Admin object, like this:

Admin admin = new Admin();
admin.setName(“Hulk”);

Because Admin is a subclass of Employee, the setName() method in the Employee class is available to objects of the Admin class as well. This principle is called Inheritance.

Encapsulation

As we have already seen, objects have properties and methods. When a class is manipulating the properties of an object of another class, it should not have direct access to the properties. If a property has to be changed, it has to be through the use of its own methods. Rich domain models provide such methods on the data they encapsulate also known as behaviour for the class. In order to achieve this, properties in a class are kept private to that class and methods are made public. This not only makes the class more secure, but also allows you to perform any other operation required along with the value change of that property. This principle of wrapping all private variables inside the class and using public methods as behaviour on them is called Encapsulation. Let’s look at an example.

Figure 3: Example of broken Encapsulation
Figure 3: Example of broken Encapsulation

You can see two classes in figure 3 above: One is a Salary class and another is an Employee class. The Employee class has a Salary object as  its property. The Employee class has the properties – name, id and salary. The Salary class has one property – amount.

 The encapsulation is broken in these classes in 2 ways.

  1. In Employee class the value of the property name can be modified directly by any other class as the name variable is public.eg:
    this.employee.name = name;
  2. In Employee class and Salary class, the public getter and setter exposes their properties. Anybody can set the salary of an employee to zero or any amount. The employee id also can reset.

    eg:
    this.employee.getSalary().setAmount(0);
    this.employee.setSalary(new Salary(0));

    this.employee.setID(“anyId”);

The problem we have here is, as the caller, you should not be making decisions for the object based on its state that results in you modifying the state of that object. The logic you are implementing should be the called object’s responsibility as a behavior. For you to make decisions outside the object, violates its encapsulation.

eg:
if (this.employee.getSalary() != null){
this.employee.getSalary().setAmount(1000);
}

This null check could be avoided, if the behaviour of modifying salary belongs to the Salary class.

Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.
— Alec Sharp

You should endeavor to tell objects what you want them to do; do not ask them questions about their state, Instead make a decision, and then tell them what to do. This promotes the encapsulation of state, since only behavior is exposed and not state.

Figure 4: Example of Encapsulation
Figure 4: Example of Encapsulation

In the above figure 4: the state of the object is not exposed either through public variables or getters. The valid behaviours of the class provide a mechanism to modify the state of the object.

There’s no meaning when you just read or write out directly or set it to whatever you want. You have got to add meaning. The meaning would be, “This person got a raise.” That means something, or “We made a mistake. We had to correct the salary.”

Tell, Don’t Ask principle comes handy when designing classes based on their responsibilities. This principle helps you remember that you should design your components by focusing on their behavior and by hiding their internal working adhering to encapsulation.

Abstraction

In some of your applications, you will encounter functionalities that a few classes have in common. But at the same time, even though the functionality is the same, the implementation is not. I’ll take the tried and tested example of rendering different shapes. The shapes square, circle, triangle, rectangle, and others can all have the behaviour of rendering themselves. Even though the functionality of rendering is common, the way rendering happens is not the same. In other words, you can’t render a circle and a square in the same way. So when you are designing such a system, you want to make sure all shape subclasses (circle, square, etc.) have the render method, but are implemented in their own way. For this, you could use a base class named Shape, which is abstract in nature. This means, the base class just tells you what methods you need to support, but will not dictate how you need to implement that method.

Figure 5: Example of Abstraction

In figure 4 above, we see the Shape class declared using the abstract keyword. This means we don’t have to define the abstract method render(), we just have to declare the method. The two subclasses Circle and Square have “overridden” the render() method to define their own ways of rendering their shapes.

When you are extending an abstract class, you need to implement all the abstract methods in that class. This way, you are making sure that all shapes have a render method. This principle is called Abstraction.

Polymorphism

Polymorphism, as the name suggests, is a principle that allows us to declare multiple methods with the same name but different implementations. There are two types here – static polymorphism and dynamic polymorphism. 

Dynamic polymorphism is when the program decides which method to call in runtime. This is because the compiler will not be able to make the distinction at compile time, as the method signatures will be the same. The method overriding example that we saw in the principle of abstraction (figure 4) is dynamic polymorphism. 

Static polymorphism is when the distinction of which method to call is made during compile time. This happens when there are two methods in a class with the same name but have different signatures.

Figure 6: Example of method overloading

In figure 5 above, you can see there are two log() methods in the Logger class, but they don’t have the same signature. One method only takes a string as an argument, and the other takes a string and a long as parameters. If the signature is not different, you can’t have two methods with the same name in the same class. In this case, the compiler can easily identify which method to call based on the parameters passed.

Part 2: Writing Clean Code with OOPs >>


How to assess your clean code skills?

Geektrust’s coding challenges are not the average coding tests. They are challenges designed to test real-world coding skills – how you can write clean, readable, maintainable code – the kind you’d write while working at a company. No unnecessary DS or algorithm tests, no hard deadlines, no competition or leaderboards. Take your time, craft your solution and write your best code ever in your own IDE — one challenge is all you need to solve.

When you submit code, we evaluate it and give you detailed feedback on what you did well, what you can improve and how to go about it. Some of the top tech companies in the world like ThoughtWorks and Intuit believe in clean code to hire talented developers. When you’re looking for a job, you can showcase your coding skill to 100+ companies hiring on Geektrust. Get started with a challenge to see all this for yourself!

3 comments

  1. I found Encapsulation explanation a bit complicated.
    That’s the only reason I prefer videos to understand concepts.

    1. Haha thank you for the feedback. We got some other feedback as well about encapsulation, and have been editing this section to make it simple for everyone based on the feedback. Anyway, we’re conducting some live sessions on clean code. One happens to be tomorrow (Saturday, 10th July) at 10.30 AM, conducted by Dhanush (CTO of Geektrust). If you’d like to attend, watch out for our emails (if you’re a registered user) or check the website on the event page.

Leave a Reply

Your email address will not be published.