Object-Oriented Design Principles

Object-Oriented Design Principles

Programming is fun until you have to incorporate a new requirement that changes the whole design of your code. Everyone writes code but it’s too difficult to write a code that your colleagues can understand. It’s difficult to write a code that sucks less and doesn’t succumb to changes. However, there are a few design principles that can be learned, practiced, and absorbed so that it will make us suck less.

Design principles are basic guidelines that help us to avoid bad object-oriented design. Nobody is going to point a gun towards you and make you adhere to it strictly. It’s not a rule of thumb but it will definitely help you. We are now going to see three principles.

  1. Encapsulate what varies.
  2. Favor composition over inheritance.
  3. Program to interface.

Encapsulate what varies.

The principle is so simple. Identify the aspects of the code that varies and isolate it with what stays the same. You gotta ask yourself

“Does this code change every time I get the new requirement?”

If the answer is yes then you now know what to do. Encapsulate. Let’s see an example.

public void connectToDatabase(String connectionType) {
    DBConnection connection = null;
    if ("mysql".equals(connectionType)) {
        connection = new MySqlConnection();
    } else if("mongodb".equals(connectionType)){
        connection = new MongoConnection();
    }else if("postgres".equals(connectionType)){
        connection = new PostgresConnection();
    }
    connection.createConnection();
}

I bet every one of us has seen code like this and who knows we may still have such code running in production. So, let’s say we need to add an Oracle database in our code. How are we going to add it? Another else if condition, isn’t it? But let’s say there are a bunch of other important codes in the same file and while adding a new Oracle database you mistakenly touched other production codes in the same file which introduced many newer bugs. Had it been encapsulated, then there would have been a single place to change. By encapsulating it, our design will be much more flexible.

The part that will change with every new requirement is if-else conditions. So, we can create a factory class that will be solely responsible for creating database connections. I will not show how to create a factory class but I will show how the code looks once we encapsulated all the logics inside the factory class.

public void connectToDatabase(String connectionType) {
    DBConnectionFactory dbConnectionFactory = new DBConnectionFactory();
    DBConnection connection = dbConnectionFactory.getConnection(connectionType);
    connection.createConnection();
}

With this approach, let’s say if you want to add an H2 database connection then you have to change only in DBConnectionFactory class.

Favor composition over inheritance

In object-oriented programming (OOP), inheritance is IS-A relationship (A car is a vehicle) whereas composition is HAS-A relationship (A car has an engine).

Inheritance provides a great way to reuse the code but it becomes less effective once the hierarchy grows on. Classes and objects created through inheritance are tightly coupled) because if something is changed in the base class then it will affect its subclasses. But it’s not true for composition. The classes and objects created through composition are loosely coupled). There is another great benefit of using Composition over Inheritance. Inheritance is not easily testable because to test derived class you also need superclass but the composition is as we can use mock objects.

Program to interface

The term interface here doesn’t merely mean the keyword we have in Java programming language. It is a kind of contract that has a bunch of rules that needs to be followed by concrete classes that implement it. It is often called as program to the supertype. If we program to an interface rather than the concrete implementations then it provides the flexibility to exploit polymorphism.

The interface provides rules and doesn’t care whoever implements it and how the implementation looks like. Let’s take a real-world simple example.

interface FileUpload {
    void upload(Object o);
}

We want to upload the file in our application. There may be many different ways to do so. We can use Cloudinary, AWS S3 bucket, upload care, etc. So the implementations of FileUpload would look like AmazonS3FileUpload, CloudinaryFileUpload, UploadCareFileUpload etcetera.

Let’s program to interface.

FileUpload fileUpload = new CloudinaryFileUpload();
// code related to fileupload

Great, our file uploading feature is working correctly and after one year, we want to switch our file uploading from Cloudinary to AmazonS3. We just need to change the implementation like:

FileUpload fileUpload = new AmazonS3FileUpload();

It will not break other codes because we are relying on the interface and the methods provided by it. We can only use methods defined within the FileUpload interface. Had it been concrete implementation with a bunch of extra methods, then it would have created a havoc.

//program to concrete implementation
CloudinaryFileUpload fileUpload = new CloudinaryFileUpload();
//code related to fileUpload

So, it’s always a good idea to program to an interface. Besides, the flexibility it provides, it is also easy to test. We can easily mock FileUpload interface wherever necessary.


Thanks a lot for reading my article. If you have any suggestions, then they are welcomed.