7 OOP Design Principles Java Programmers Should Learn in 2023 - GeeksforGeeks
Mai 23, às 05:14
·
16 min de leitura
·
0 leituras
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
import
java.io.*;
public
class
GFG {
public
static
void
main(String[] args)
{
int
[][] matrix = { {
1
,
2
,
3
,
4
},
{
5
,
6
,
7
,
8
},
{
9
,
10
,
11
,
12
},
{
13
,
14
,
15
,
16
} };
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();
}
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
} };
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();
}
}
}
Output
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
import
java.io.*;
public
class
GFG {
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();
int
[][] matrix = { {
1
,
2
,
3
,
4
},
{
5
,
6
,
7
,
8
},
{
9
,
10
,
11
,
12
},
{
13
,
14
,
15
,
16
} };
System.out.println(
"Matrix1: "
);
obj.printMatrix(matrix);
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
} };
System.out.println(
"Matrix2: "
);
obj.printMatrix(matrix2);
}
}
Output
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
import
java.io.*;
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
,
'-'
));
}
}
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
import
java.io.*;
interface
Arithmetic {
double
perform(
double
a,
double
b);
}
class
Addition
implements
Arithmetic {
public
double
perform(
double
a,
double
b)
{
return
a + b;
}
}
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
));
}
}
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
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..."
);
}
}
public
class
GFG {
public
static
void
main(String[] args)
{
User user1 =
new
User(
"John Doe"
,
"[email protected]"
);
user1.showUser();
user1.sendEmail(
"Hello John"
);
user1.saveToFile();
}
}
Output
Name: John Doe Email: [email protected] Email sent to [email protected] 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
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);
}
}
class
EmailService {
public
void
sendEmail(User user, String message)
{
System.out.println(
"Email sent to "
+ user.email
+
" with message: "
+ message);
}
}
class
FileService {
public
void
saveToFile(User user)
{
System.out.println(
"Saving user to file..."
);
}
}
public
class
GFG {
public
static
void
main(String[] args)
{
User user =
new
User(
"John Doe"
,
"[email protected]"
);
user.showUser();
EmailService emailService =
new
EmailService();
emailService.sendEmail(user,
"Hello John"
);
FileService fileService =
new
FileService();
fileService.saveToFile(user);
}
}
Output
Name: John Doe Email: [email protected] Email sent to [email protected] 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(){
}
}
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 {
public
void
breath();
}
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
class
Rectangle {
int
length;
int
width;
void
setLength(
int
l) {
length = l;
}
void
setWidth(
int
w) {
width = w;
}
int
area() {
return
length * width;
}
}
class
Square
extends
Rectangle {
void
setLength(
int
l) {
length = l;
width = l;
}
void
setWidth(
int
w) {
length = w;
width = w;
}
}
public
class
GFG {
public
static
void
main(String[] args) {
Rectangle obj =
new
Square();
obj.setLength(
5
);
obj.setWidth(
10
);
System.out.println(obj.area());
}
}
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
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());
}
}
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();
}
}
Output
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);
}
}
Output
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
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.
Continue lendo
Showmetech
Hoje, às 15:20
DEV
Hoje, às 15:13
TabNews
Hoje, às 14:37
Hacker News
Hoje, às 14:25
TabNews
Hoje, às 13:58
TabNews
Hoje, às 13:43
Showmetech
Hoje, às 13:13
Hacker News
Hoje, às 13:11
Showmetech
Hoje, às 11:00
DEV
Hoje, às 10:21