Java is one of the most widely-used object-oriented programming languages today, known for its robustness, cross-platform compatibility, and user-friendly syntax. Java developers are responsible for creating a wide range of software products that fuel our digital world, from web applications to mobile apps, desktop software, and enterprise solutions and again a question arises how many oops concepts in Java are there? However, any good developer must write code that is not only functional but also modular, easy to test, debug, and maintain. This is where the principles of object-oriented design come into play. By following these regulations, developers may ensure that their code is clean, efficient, and simple to use, both for themselves and for those who may need to work with it in the future.
Now continuing with the article, we’ll discuss some of the best object-oriented design principles that Java programmers should learn in 2023 to take their skills to the next level and write code that is maintainable, scalable, and efficient.
7 OOP Design Principles For Java Programmers
1. DRY – Don’t Repeat Yourself
DRY is an acronym for Don’t Repeat Yourself. As the name suggests this principle focuses on reducing the duplication of the same code throughout the program. If you have the same block of code, performing the same tasks in multiple parts of the program, then it means that you are not following the DRY principle. The DRY principle can be implemented by refactoring the code such that it removes duplication and redundancy by creating a single reuseable code in the form of abstraction, or a function.
Example: 1.1. Before DRY
Java
// Java program to illustrate the repeated code import java.io.*; public class GFG { public static void main(String[] args) { // cerate 4x4 matrix with values int [][] matrix = { { 1 , 2 , 3 , 4 }, { 5 , 6 , 7 , 8 }, { 9 , 10 , 11 , 12 }, { 13 , 14 , 15 , 16 } }; // print matrix System.out.println( "Matrix1: " ); for ( int i = 0 ; i < 4 ; i++) { for ( int j = 0 ; j < 4 ; j++) { System.out.print(matrix[i][j] + " " ); } System.out.println(); } // create 5x5 matrix with values int [][] matrix2 = { { 1 , 2 , 3 , 4 , 5 }, { 6 , 7 , 8 , 9 , 10 }, { 11 , 12 , 13 , 14 , 15 }, { 16 , 17 , 18 , 19 , 20 }, { 21 , 22 , 23 , 24 , 25 } }; // print matrix System.out.println( "Matrix2: " ); for ( int i = 0 ; i < 5 ; i++) { for ( int j = 0 ; j < 5 ; j++) { System.out.print(matrix2[i][j] + " " ); } System.out.println(); } } } |
Matrix1: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Matrix2: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
In Example 1.1, the code used for printing the matrix of different lengths. But the code block is repeated twice, once for printing the matrix of length 4×4 and the other for the matrix of length 5×5, which does not follow the DRY guidelines.
1.2. After DRY
Java
// Java program to illustrate the usage of DRY principle to // avoid code repetition import java.io.*; public class GFG { // print matrix method private void printMatrix( int [][] matrix) { int n = matrix.length; int m = matrix[ 0 ].length; for ( int i = 0 ; i < n; i++) { for ( int j = 0 ; j < m; j++) { System.out.print(matrix[i][j] + " " ); } System.out.println(); } } public static void main(String[] args) { GFG obj = new GFG(); // cerate 4x4 matrix with values int [][] matrix = { { 1 , 2 , 3 , 4 }, { 5 , 6 , 7 , 8 }, { 9 , 10 , 11 , 12 }, { 13 , 14 , 15 , 16 } }; // print matrix System.out.println( "Matrix1: " ); obj.printMatrix(matrix); // create 5x5 matrix with values int [][] matrix2 = { { 1 , 2 , 3 , 4 , 5 }, { 6 , 7 , 8 , 9 , 10 }, { 11 , 12 , 13 , 14 , 15 }, { 16 , 17 , 18 , 19 , 20 }, { 21 , 22 , 23 , 24 , 25 } }; // print matrix System.out.println( "Matrix2: " ); obj.printMatrix(matrix2); } } |
Matrix1: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Matrix2: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
The repetition from Example 1.1 is resolved in Example 1.2 where a method `printMartix()` is created to print the matrix of any dimensions and then the repetitive block is placed inside of it. Now, this method can then be called as many times as required to print the matrix of any possible dimensions, with just one line of code.
Must Read – DRY (Don’t Repeat Yourself) Principle in Java with Examples
2. OCP – Open-Closed Principle
OCP is an acronym for Open Closed Principle, it is also considered as a basic principle of OOPs. According to this principle, all the entities, like classes, methods, etc, should be open for extensions but closed for modifications. This means that you have to keep your code open for extending the behavior, but it should not allow modification of the existing source code. The principle emphasizes the statement by Robert C. Martin, “If already tried and tested code is not touched, it won’t break”.
Example: 2.1. Before OCP
Java
// Java program to illustrate the OCP principle import java.io.*; // calculator class to calculate basic artimetic operations class Calculator { public double calculate( double a, double b, char operator) { switch (operator) { case '+' : return a + b; case '-' : return a - b; } return 0.0 ; } } public class GFG { public static void main(String args[]) { Calculator obj = new Calculator(); System.out.println(obj.calculate( 10 , 20 , '+' )); System.out.println(obj.calculate( 10 , 20 , '-' )); } } |
30.0 -10.0
In example 2.1, the `Calculator` class is used used to perform some arithmetic operations on the provided numbers. The operation is specified by the char `operator`. Currently, the Class only supports 2 operations i.e. addition and subtraction. If a new operation is to be added, it would need to modify the existing implementation of the ‘Calculator’ class, which does not follow the `Open Closed Principle (OCP)`.
2.2. After OCP
Java
// Java program to illustrate the OCP principle import java.io.*; // creating artithmetic interface rather than concrete class interface Arithmetic { double perform( double a, double b); } // using arithmetic interface to implement addition class Addition implements Arithmetic { public double perform( double a, double b) { return a + b; } } // using arithmetic interface to implement substraction class Substraction implements Arithmetic { public double perform( double a, double b) { return a - b; } } class Calculator { public double calculate(Arithmetic arithmetic, double a, double b) { return arithmetic.perform(a, b); } } public class GFG { public static void main(String[] args) { Calculator obj = new Calculator(); System.out.println( obj.calculate( new Addition(), 10 , 20 )); System.out.println( obj.calculate( new Substraction(), 10 , 20 )); } } |
30.0 -10.0
The need for modification of a class in Example 2.1 is removed in Example 2.2. The interface `Arithmetic` is declared which declares a `perform()` method. Two classes Addition and Subtraction are also defined which implement the Arithmetic interface and provide their own implementation of `perform()` the method.
In the same way, other classes can be created for Multiplication or any other operation, by implementing the `Arithmetic` interface and providing their own version of the `perform()` method. The approach follows Open Closed Principle (OCP) as we can extend the functionality of the code by modifying the original code.
Must Read – Open Closed Principle in Java with Examples
3. SRP – Single Responsibility Principle
SRP is an acronym for Single Responsibility Principle. This principle suggests that a `Class` should have only one reason to change. This means that a `Class` should only implement one functionality and only change when there is a need to change the functionality. If a class has too many responsibilities, it becomes difficult to manage in the long run. With SRP the classes become more modular and focused, leading to a more maintainable and flexible code.
Example – 3.1. Before SRP
Java
// User class with all the methods assiociated with user class User { String name; String email; public User(String name, String email) { this .name = name; this .email = email; } public void showUser() { System.out.println( "Name: " + this .name); System.out.println( "Email: " + this .email); } public void sendEmail(String message) { System.out.println( "Email sent to " + this .email + " with message: " + message); } public void saveToFile() { System.out.println( "Saving user to file..." ); } } // driver code public class GFG { public static void main(String[] args) { User user1 = new User( "John Doe" , "john@gfg.com" ); user1.showUser(); user1.sendEmail( "Hello John" ); user1.saveToFile(); } } |
Name: John Doe Email: john@gfg.com Email sent to john@gfg.com with message: Hello John Saving user to file...
In Example 3.1, the User class is used to perform multiple responsibilities, including displaying user information with the `showUser()` method, sending email to the user with `sendEmail()`, and saving user data to a file with the `saveFile()` method. This clearly violates the Single Responsibility Principle (SRP), as a class should have only one reason to change. If there are multiple responsibilities a change in one of the functionalities (like a change in file storage method) can potentially break existing behaviors, necessitating another round of testing to avoid unexpected behavior in production.
3.2. After SRP
Java
// user class with only user details methods class User { String name; String email; public User(String name, String email) { this .name = name; this .email = email; } public void showUser() { System.out.println( "Name: " + this .name); System.out.println( "Email: " + this .email); } } // email service class to send email class EmailService { public void sendEmail(User user, String message) { System.out.println( "Email sent to " + user.email + " with message: " + message); } } // file service class to save to file class FileService { public void saveToFile(User user) { System.out.println( "Saving user to file..." ); } } // driver code public class GFG { public static void main(String[] args) { User user = new User( "John Doe" , "john@gfg.com" ); user.showUser(); EmailService emailService = new EmailService(); emailService.sendEmail(user, "Hello John" ); FileService fileService = new FileService(); fileService.saveToFile(user); } } |
Name: John Doe Email: john@gfg.com Email sent to john@gfg.com with message: Hello John Saving user to file...
In Example 3.2, all the services are moved into their individual classes. The `User` class is used to display the user data, the `FileService` class is used save the user to the file, and `EmailService` is used to send Emails to the user. This was one class is only responsible for managing a single functionality. By doing this, we have ensured that each class has only one reason to change and is adhering to the Single Responsibility Principle (SRP) guidelines.
Must Read – Single Responsibility Principle in Java with Examples
4. ISP – Interface Segregation Principle
ISP is an acronym for Interface Segregation Principle. The principle states that clients should not forcefully implement an Interface if it does not use that. This means that the class should not implement an interface if the methods declared by the interface are not used by the class. Similar to SRP, this principle states that one should focus on creating multiple client interfaces responsible for a particular task, rather than having a one-fat interface.
Example – 4.1. Before ISP
Java
interface Animal{ public void breath(); public void fly(); public void swim(); } class Fish implements Animal{ public void swim(){ System.out.println( "Fish swims" ); } public void fly(){ // Fish cannot fly } } |
In Example 4.1, the `Animal` interface has two methods: `fly()`, and `swim()`. The `Fish` class implements all two methods, but the fly() method does not make sense for a fish since they do not fly. Hence, this code breaks the Interface Segregation Principle (ISP) because the Animal interface is too generic and has methods that are not relevant to all animals. This can lead to unnecessary complexity and confusion in the implementation of classes that implement the interface
4.2. After ISP
Java
interface Animal { // only method common in all animal implementations public void breath(); } // rather than Animal interface, we use waterAnimal and // AirAnimal interface interface WaterAnimal { public void swim(); } interface AirAnimal { public void fly(); } class Fish implements WaterAnimal { public void swim() { System.out.println( "Fish swims" ); } } |
In Example 4.2, the Animal interface is broken down into more specific interfaces, WaterAnimal and AirAnimal. The Fish class now implements only the WaterAnimal interfaces, similarly AirAnimal interface can be used for the animals that fly, for example, Birds. This way, each interface only contains methods that are relevant to the types of animals that implement them. In this way the code follows the Interface Segregation Principle, making the code more modular and hence manageable.
5. LSP – Liskov Substitution Principle
LSP is an acronym for Liskov Substitution Principle. According to the principle, Derived classes must be substitutable for their base classes. This indicates that superclass objects in the program should be interchangeable by instances of their subclasses without compromising the program’s correctness. It guarantees that any child class of a parent class can be used in place of their parent without causing any unexpected behavior.
Example – 5.1. Before LSP
Java
// parent rectangle class class Rectangle { int length; int width; void setLength( int l) { length = l; } void setWidth( int w) { width = w; } int area() { return length * width; } } // square class inherited from rectangle class Square extends Rectangle { void setLength( int l) { length = l; width = l; } void setWidth( int w) { length = w; width = w; } } // driver code public class GFG { public static void main(String[] args) { Rectangle obj = new Square(); obj.setLength( 5 ); obj.setWidth( 10 ); System.out.println(obj.area()); } } |
100
In Example 5.1, `Square` is a base class that inherits from the parent `Rectangle` class. In the `Rectangle` class, the `setLength()` and `setWidth()` methods set the length and width of the rectangle respectively. However, in the Square class, these methods set both the length and width to the same value, which tells that a Square object cannot be substituted for a Rectangle object in all cases. Hence the above code violates LSP, this can be verified with the code in GFG class that uses the `Square` object as a Rectangle object which leads to unexpected behavior.
5.2. After LSP
Java
// instead of inheriting from rectangle class, we will use // shape inteface to implement them interface Shape{ void setLength( int l); void setWidth( int w); int area(); } class Rectangle implements Shape{ int length; int width; public void setLength( int l) { length = l; } public void setWidth( int w) { width = w; } public int area() { return length * width; } } public class GFG { public static void main(String[] args) { Shape obj = new Rectangle(); obj.setLength( 5 ); obj.setWidth( 10 ); System.out.println(obj.area()); } } |
50
In Example 5.2, the Square class is removed, and a Shape interface is introduced with methods for getting the width, height, and area. The Rectangle class (Child) can now implement the Shape interface (Base). This example now follows the Liskov Substitution Principle (LSP) as it guarantees that the child class, Rectangle of a parent, and Shape can be used in place of their parent without causing any unexpected behavior.
6. DIP – Dependency Inversion Principle
DIP is an acronym for Dependency Inversion Principle. The principle states that high-level classes should not depend on low-level classes, instead, both should depend on Abstractions. In other words, the modules/classes should depend on Abstractions, (interfaces and abstract classes) rather than concrete implementation. By introducing an abstract layer, DIP aims in reducing the coupling between the classes and hence make the application more easier to test and maintain.
Example – 6.1 Before DIP
Java
class Computer { public void boot() { System.out.println( "Booting the computer..." ); } } class User { public void startComputer() { Computer computer = new Computer(); computer.boot(); } } public class GFG { public static void main(String[] args) { User user = new User(); user.startComputer(); } } |
Booting the computer...
In Example 6.1, the `User` class depends on the `Computer` class, which violates the Dependency Inversion Principle (DIP). If there was some change in the Computer class the User class had to be changed as well. This makes it difficult to maintain and manage the code.
6.2 After DIP
Java
interface IComputer { void boot(); } class Computer implements IComputer { public void boot() { System.out.println( "Booting the computer..." ); } } class User { public void startComputer(IComputer computer) { computer.boot(); } } public class GFG { public static void main(String[] args) { User user = new User(); Computer computer = new Computer(); user.startComputer(computer); } } |
Booting the computer...
In Example 6.2, an interface `IComputer` is created, and now, the `User` class depends on the `IComputer` interface instead of the `Computer` class directly. This change allows us to make changes to the Computer class that is an implementation of `IComputer`, as per requirement, without affecting the User class. Hence, the Dependency Inversion Principle allows us to decouple the code and make it more maintainable.
7. COI – Composition Over Inheritance
COI is an acronym for Composition Over Inheritance. As the name implies, this principle emphasizes using Composition instead of Inheritance to achieve code reusability. Inheritance allows a subclass to inherit its superclass’s properties and behavior, but this approach can lead to a rigid class hierarchy that is difficult to modify and maintain. In contrast, Composition enables greater flexibility and modularity in class design by constructing objects from other objects and combining their behaviors. Additionally, the fact that Java doesn’t support multiple inheritances can be another reason to favor Composition over Inheritance.
Example – 7.1 Before COI
Java
/*package whatever //do not write package name here */ class Musician { public void play() { System.out.println( "play" ); } } class Singer extends Musician { public void sing() { System.out.println( "sing" ); } } class Drummer extends Musician { public void drum() { System.out.println( "drum" ); } } |
In Example 7.1, a base class Musician is defined which contains the `play()` method that is common to all musicians. This class is then extended by the `Singer` and `Drummer` classes, which add more methods specific to their functionalities. While this implementation may seem reasonable at first, it can create problems if there is a need to create a new type of Musician who can both sing and play drums. This design can lead to a rigid class hierarchy that becomes difficult to maintain as more functionality is added to the subclasses. This is because inheritance represents an is-a relationship and may not always be the best approach for code reuse.
7.2 After COI
Java
class Singer { public void sing() { System.out.println( "sing" ); } } class Drummer { public void drum() { System.out.println( "drum" ); } } class SingerDrummer { Singer singer = new Singer(); Drummer drummer = new Drummer(); public void play() { singer.sing(); drummer.drum(); } } |
In Example 7.2, the COI principle was applied to implement the same logic using Composition instead of Inheritance. In this approach, the classes for `Singer` and `Drummer` are separated from the Musician class, and their behavior is combined in the `SingerDrummer` class using composition. This provides greater flexibility in class design and modularity by combining objects instead of inheriting their behavior. By using composition, code reusability can be achieved without creating a rigid class hierarchy.
Conclusion
Utilizing Object-Oriented Design Principles when writing code can greatly benefit Java Software Developers. These principles enable the creation of flexible and maintainable code, with low coupling and high cohesion. While applying these principles may require more effort upfront, they can ultimately save time and effort by reducing the number of bugs, improving code readability, and facilitating code reuse.
In this article, we’ve discussed 7 principles: DRY, OCP, SRP, ISP, LSP, DSP, and COI, which provide a framework for writing effective and scalable code. While there may be times when these principles conflict with one another, understanding how to balance them can lead to better overall design choices. It’s also worth noting that these principles are not exhaustive, and there may be additional principles to consider.
Useful Links:
FAQs on OOP Design Principles For Java Programmers
Q1: Are OOP design principles unique to Java programming?
Answer:
No, OOP design principles are not unique to Java programming. They apply to any object-oriented programming language and can be useful for improving the quality of software in any programming language.
Q2: Do all Java programmers need to know OOP design principles?
Answer:
OOP design principles are not mandatory for writing Java code, but they can help developers write better more efficient code. Therefore, it is recommended that all Java programmers learn and understand these principles.
Q3: Can OOP design principles conflict with each other?
Answer:
Yes, it is possible for OOP design principles to conflict with each other. For example, the SRP and DRY principles may seem to conflict in some cases. However, understanding how to balance these principles can lead to better overall design choices.