You are here
Home > java >

OOPs Design Principles

OOPs Design PrinciplesDesign Principles are nothing but a set of thoughts that initiate the basis of any good product. No doubt that they continuously help teams to make decisions in delivering a good quality product. Organizations, individuals and product teams always get benefited from writing and following their design principles. If we talk about OOPs Design Principles, they deal with the principles to design a good quality software where Object Oriented Programming Language is being used for development.

In this article, we are going to discuss the most commonly used OOPs Design Principles that are used by Industry Designers & Developers in their assignments.

DRY (Don’t repeat yourself)

One of the most important object-oriented design principles is DRY, as the name suggests DRY (don’t repeat yourself) means don’t write duplicate code, instead use Abstraction to abstract common things in one place. If you are using JDK 8 or later versions, you can implement the method in interfaces as well. If you have the same block of code in more than two places, consider making it a separate method. Even if you use a hard-coded value more than once, make them public final constant.

The main advantage of implementing this OOPs design principle is in maintenance. Therefore, It’s important not to abuse it, duplication is not for code, but for functionality.

When it comes to OOP, it suggests us to utilize abstract classes, interfaces, and public constants as much as possible. Whenever there’s a functionality common across classes, one better option is to abstract them away into a common parent class. Another option is to use interfaces to couple their functionality. For example, below code demonstrates the concept.

public interface Animal {

      /**we could implement eats() as a default method : starting from jdk 8 onward*/

       public default void eats() {

           System.out.println(“Eating food…”);

       }

public void speaks();

}

public class Cat implements Animal {

       public void speaks() {

           System.out.println(“Meow! *purrs*”);

       }

}

public class Dog implements Animal {

       public void speaks() {

           System.out.println(“Woof! *wags tail*”);

      }

}

Both a Cat and a Dog need to eat food, but they speak differently. Since eating food is a common functionality for them, we can abstract it into an interface (as in example above) or a parent class, such as Animal and then have them extend/implement as per our need.

Include Default Methods in Interfaces (JDK 8)

Java Community has introduced default methods in interfaces in JDK 8. Don’t forget to use it while designing the application when required.

Default methods enable you to add new functionality to the interfaces and ensure backward compatibility for existing classes which implement that interface. It provides the flexibility to allow interface to define implementation which will use as the default in a situation where a concrete class fails to provide an implementation for that method. If each added method in an interface is defined with the implementation, then no implementing class is affected. An implementing class can override the default implementation provided by the interface.

Modifying one interface in a JDK framework breaks all classes that extend the interface, which means that adding any new method could break millions of lines of code. Therefore, default methods have introduced as a mechanism to extend interfaces in a backward-compatible way. Hence, if our implementation permits to use default methods, it is advisable to use it judiciously.

Favor Composition over Inheritance

Always favor composition over inheritance, whenever it is possible. Some of us may argue this, but I found that Composition is a lot more flexible than Inheritance. Composition allows changing the behavior of a class at run-time by setting property during run-time, and by using Interfaces to compose a class, we use polymorphism, which provides flexibility to replace with better implementation at any time. Even ‘Effective Java’ also suggests favoring composition over inheritance.

Difference between Composition and Inheritance

Now let’s understand the difference between Inheritance and Composition in a little bit more detail. We will go point by point and try to understand each point in as much detail as possible without boring you 🙂

Static vs Dynamic

The first difference between Inheritance and Composition comes from a flexibility point of view. When we use Inheritance, we have to define which class you are extending in code. It cannot be changed at runtime, but with Composition you just define a Type which you want to use, which can hold it’s different implementation. In this sense, Composition is much more flexible than Inheritance.

Limited code reuse with Inheritance

As aforementioned, with Inheritance you can only extend one class, which means you code can only reuse just one class, not more than one. If you want to leverage functionalities from multiple classes, you must use Composition. For example, if your code needs authentication functionality, you can use an Authenticator, for authorization you can use an Authorizer etc.  But with Inheritance you just stuck with only class, why? Because Java doesn’t support multiple Inheritance. This difference between Inheritance vs Composition actually highlights a severe limitation of later reusability.

Unit Testing

This is in my opinion, the most important difference between Inheritance and Composition in OOP probably is the deciding factor whether to use Composition or Inheritance. When you design classes using Composition, they are easier to test because you can supply a mock implementation of the classes you are using. But when you design your class using Inheritance, you must need a parent class in order to test it’s child class. There is no way you can provide a mock implementation of the parent class.

Final classes

This difference between them also highlights the other limitation of Inheritance. Composition allows code reuse even from final classes, which is not possible using Inheritance because you cannot extend final class in Java, which is necessary for Inheritance to reuse code.

Encapsulation

The last difference between Composition and Inheritance in Java in this list comes from Encapsulation and robustness point of view. Though both Inheritance and Composition allow code reuse, Inheritance breaks encapsulation because in case of Inheritance, subclass is dependent upon super class behavior. If parent classes change its behavior, then child class will also get affected. If classes are not properly documented and child class has not used the super class in a way it should be used, any change in super class can break functionality in the subclass.

The Composition provides a better way to reuse code and same time protect the class you are reusing from any of its client, but Inheritance doesn’t offer that guarantee. However, sometimes Inheritance becomes necessary, mainly when you are creating class from the same family.

Programming for Interface not for Implementation

Always program for the interface and not for implementation; this will lead to flexible code that can work with any new implementation of the interface. But hold on for a min and go through below lines!

An interface might be a language keyword and even an interface might also be a design principle. Don’t confuse both! There are two rules to think of:

  • Use interfaces (the language keyword) if you have multiple concrete implementations.
  • Use interfaces (the design principle) to decouple your own system from external system. It refers to loose coupling between modules or systems.

Whenever you build a larger piece of software, you will have dependencies to other systems. For example, to a relational database or to a REST service. All of those dependencies represent tools for your software to fulfil its goals. Such a goal might be to manage contacts in an address book app or to process orders in an online shop. But whatever the goal of your software is, your goal as a software developer should be to model your problem domain nice and clean.

To achieve such a clean domain layer in your application, you need to create a separation between your domain and external systems. So instead of programming to a relational database, you program to a “data store interface”. Instead of programming to a REST service, you program to a “client interface”. This will make your application independent of all dependencies and focused on its own domain. If you use MongoDB instead of a relational database in the future, there will not be a problem. All your code is developed to an interface and doesn’t even know that there is any database behind it.

Minimize Coupling

Coupling between modules/components is their degree of mutual interdependence; lower coupling is better. In other words, coupling is the probability that code unit “B” will “break” after an unknown change to code unit “A”.

Coupling refers to the degree of direct knowledge that one element has of another. In other words, how often do changes in class A force related changes in class B.

Eliminate, minimize, and reduce complexity of necessary relationships. By hiding implementation details, we can reduce coupling. Let’s understand two types of coupling:

What is Tight Coupling?

In general, Tight coupling means the two classes often change together. In other words, if A knows more than it should about the way in which B was implemented, then A and B are tightly coupled. For example, if you want to change the skin, you would also have to change the design of your body as well because the two are joined together, they are tightly coupled. The best example of tight coupling is RMI (Remote Method Invocation).

 

public class Subject {

      Topic t = new Topic();

       public void startReading() {

             t.understand();

       }

}

public class Topic {

     public void understand() {

          System.out.println("Tight coupling concept");

     }

}

In the above program the Subject class dependents on Topic class. Therefore, Subject class is tightly coupled with Topic class. It means any change in the Topic class requires the Subject class to change. For example, if Topic class understand() method change to gotIt() method, then you have to change the startReading() method will call gotIt() method instead of calling understand() method.

What is Loose Coupling ?

In simple words, loose coupling means they are mostly independent. If the only knowledge that class A has about class B, is what class B has exposed through its interface, then class A and class B are said to be loosely coupled. In order to overcome from the problems of tight coupling between objects, spring framework uses dependency injection mechanism with the help of a POJO/POJI model. Needless to say, through dependency injection its possible to achieve loose coupling. For example, if you change your shirt, then you are not forced to change your body – when you can do that, then you have loose coupling. When you can’t do that, then you have tight coupling. The examples of Loose coupling are Interface, JMS.

public interface Topic {

      void understand();

}

public class Topic1 implements Topic {

      public void understand() {

          System.out.println("Got it");

      }
}

public class Topic2 implements Topic {

       public void understand() {

           System.out.println("understood");
       }
}


public class Subject {

       public static void main(String[] args) {

            Topic t = new Topic1();
            t.understand();

       }
}

In the above example, Topic1 and Topic2 objects are loosely coupled. It means Topic is an interface and we can inject any of the implemented classes at run time and we can provide service to the end user.

In general, Tight Coupling is bad in but most of the time, because it reduces flexibility and re-usability of code, it makes changes much more difficult, it impedes test ability etc. Loose coupling is a better choice because it will help you when your application needs to change or grow. Moreover, if you design with loosely coupled architecture, only a few parts of the application will have an impact when requirements change.

Maximize Cohesion

The Cohesion of a single module/component is the degree to which its responsibilities form a meaningful unit; higher cohesion is better. We should group the related functionalities as to share a single responsibility (e.g. in a class).

In general, Cohesion is most closely associated with making sure that a class is designed with a single, well-focused purpose. The more focused a class is, the cohesiveness of that class is more. The advantages of high cohesion is that such classes are much easier to maintain (and less frequently changed) than classes with low cohesion. Another benefit of high cohesion is that classes with a well-focused purpose tend to be more reusable than other classes.

Suppose we have a class that multiply two numbers, but the same class creates a pop-up window displaying the result. This is the example of low cohesive class because the window and the multiplication operation don’t have much in common. To make it high cohesive, we would have to create a class Display and a class Multiply. The Display will call Multiply’s method to get the result and display it. Therefore, this could be an example to develop a high cohesive solution.


public class Multiply {

       int a = 5;
       int b = 5;

      public int mul(int a, int b) {

          this.a = a;
          this.b = b;
          return a * b;
     }
}

public class Display {

    public static void main(String[] args) {

         Multiply m = new Multiply();

         System.out.println(m.mul(5, 5));
    }
}

KISS (Keep It Simple, Stupid)

The Keep it Simple, Stupid (KISS) principle states that most systems work the best if they are kept simpler rather than made complex. Therefore, we should consider the simplicity as a key goal in the design, and avoid the unnecessary complication.

Some other variations on the phrase ‘KISS” include: “Keep it simple, silly”, “keep it short and simple”, “keep it simple and straightforward”, “keep it small and simple”, “keep it simple, soldier”, “keep it simple, sailor”, or “keep it sweet and simple”.

The Keep it Simple, Stupid (KISS) principle is a reminder to keep your code simple and readable for humans. If your method handles multiple use-cases, split them into smaller methods. If it performs multiple functionalities, make multiple methods instead.

Furthermore, if a single method handles multiple functionalities, it will become long and bulky. A long method will be very hard to maintain for programmers. Consequently, bugs will be harder to find, and we might find ourselves violating other design principles as well. If a method does two things, you can’t call it to do just one of them, so you’ll obviously make another method.

Also, you should keep your code simple to be easily understood by other developers. For example, if a simple for loop does the job efficiently, you should not use a stream API unnecessarily.

It’s almost possible that the design could be tweaked to make it more readable. We should always keep in mind that the other programmer could feel it readable and understandable who will work on it in the future for the first time. Moreover, if you are having trouble as the one who designed it while it’s all still fresh in your mind, think about how the other programmer who works on it will perform for the first time.

Delegation Principles

Don’t do all stuff by yourself, delegate it to the respective classes. A classical example of the delegation design principle is equals() and hashCode() method in Java. In order to compare two objects for equality, we ask the class itself to make comparison instead of the Client class doing that check.

The key benefit of this design principle is no duplication of code and pretty easy to modify behavior. Event delegation is another example of this principle, where an event is delegated to handlers for handling.

Encapsulate What Changes

Only one thing is constant in the software field, and that is “Change,” So encapsulate the code you expect or suspect to be changed in the future. The benefit of this OOP Design principle is that It’s easy to test and maintain proper encapsulated code.

YAGNI (you aren’t gonna need it)

YAGNI stands for “you aren’t gonna need it”: don’t implement something until it is necessary. Always implement things when you actually need them, never when you just foresee that you need them. It leads to code bloat; the software becomes larger and more complicated.

Summary

All these OOPs design principles help you write flexible and better code by aspiring high cohesion and low coupling. The theory is the first step, but what is most important is to develop the ability to find out when to apply these OOPs design principles. In addition, find out whether we are violating any design principle and compromising the flexibility of code. Based on these findings try to improve your design with the help of implementing these principles. Once you get hold of it, the next step should be to learn Design Patterns in Java, which uses these basic design patterns to solve common problems of application development and software engineering. Additionally, if we find any update on the topics in this article, we will also update our article accordingly.

If you are a book reader, we have listed out some of the good books on Design Patterns. You may find them in the article ‘Java Design Patterns Book‘. Also, if you have an interest in learning ‘How to Design a Better Software?’, kindly visit our article on ‘How to Design a Better Software?‘. 

close

Leave a Reply

Top