Table of Contents
SOLID Design Principles in Java Application Development
SOLID refers to five design principles in object-oriented programming, designed to reduce code rot and improve the value, function, and maintainability of software. The SOLID principles help the user develop less coupled code. If code is tightly coupled, a group of classes are dependent on one another.
Here, we give an overview of each SOLID principle along with an example of each.
What Are SOLID Design Principles?
1: What is the default value of a boolean in Java?
2: What is the TreeSet Interface?
3: Which loop construct in Java best suits when the number of iterations is known?
4: What is the size of a byte variable?
5: Which of the following is thread safe?
Ever wondered how much you really know? It's time to put your brain to the test!
SOLID principles are object-oriented design concepts that, in conjunction with an extensive test suite, help you avoid code rot.
SOLID design is an acronym for the following five principles:
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
These principles provide a valuable standard for guiding developers away from such “code rot,” and instead help them move towards building applications that provide lasting value for customers and sanity for future developers working on your project.
Grab the opportunity to learn Java with Entri! Click Here
SOLID Design Principles
1. Single Responsibility Principle (SRP)
The Single Responsibility Principle (SRP) says that there should not be more than one reason for a class to change. This means that every class in your code should have only one job to do.
Everything in the class should be related to that single purpose. It does not mean that your classes should only contain one method or property.
There can be a lot of members as long as they relate to the single responsibility. It may be that when the one reason to change occurs, multiple members of the class may need modification. It may also be that multiple classes will require updates.
For example:
class Employee {
public Pay calculatePay() {...}
public void save() {...}
public String describeEmployee() {...}
}
Here we have to pay calculation logic with database logic and reporting logic all mixed up within one class. If you have multiple responsibilities combined into one class, it might be difficult to change one part without breaking others.
The easiest way to fix this is to split the class into three different classes, with each having only one responsibility: database access, calculating pay, and reporting, all separated.
Learn Coding in your Language! Enroll Here!
2. Open-Closed Principle (OCP)
The Open-Closed Principle (OCP) states that classes should be open for extension but closed for modification. “Open to extension” means that you should design your classes so that new functionality can be added as new requirements are generated. “Closed for modification” means that once you have developed a class you should never modify it, except to correct bugs.
These two parts of the principle appear to be contradictory. However, if you correctly structure classes and their dependencies, you can add functionality without editing existing source code.
Applying OCP to your projects reduces the need to change source code once it has been written, tested, and debugged. This reduces the risk of introducing new bugs to existing code, leading to more robust software.
Open-Closed Principle Example
Another side effect of the use of interfaces for dependencies is reduced coupling (i.e. when changing code in class A forces related changes in class B), and increased flexibility.
void checkOut(Receipt receipt) {
Money total = Money.zero;
for (item : items) {
total += item.getPrice();
receipt.addItem(item);
}
Payment p = acceptCash(total);
receipt.addPayment(p);
}
So how do we add credit card support? You could add an “if” statement like this, but then that would be violation of OCP.
Payment p;
if (credit)
p = acceptCredit(total);
else
p = acceptCash(total);
receipt.addPayment(p);
Here is a better solution:
public interface PaymentMethod {void acceptPayment(Money total);}
void checkOut(Receipt receipt, PaymentMethod pm) {
Money total = Money.zero;
for (item : items) {
total += item.getPrice();
receipt.addItem(item);
}
Payment p = pm.acceptPayment(total);
receipt.addPayment(p);
}
Keep in mind – OCP helps only if the changes that are going to come are predictable, so you should apply it only if a similar change has already happened. So, first do the simplest thing and then see what changes are requested so you can more accurately predict the future changes.
3. Liskov Substitution Principles (LSP)
The Liskov Substitution Principle (LSP) applies to inheritance hierarchies, specifying that you should design your classes so that client dependencies can be substituted with subclasses without the client knowing about the change.
All subclasses must, therefore, operate in the same manner as their base classes. The specific functionality of the subclass may be different but must match to the expected behavior of the base class. The subclass must not only implement the methods and properties of the base class, but also conform to its implied behavior.
In general, if a subtype of the supertype does something that the client of the supertype does not expect, then this is in violation of LSP. Imagine a derived class throwing an exception that the superclass does not throw, or if a derived class has some unexpected side effects. Basically, derived classes should never do less than their base class.
A typical example that violates LSP is a Square class that derives from a Rectangle class. The Square class always assumes that the width is equal with the height. If a Square object is used in a context where a Rectangle is expected, unexpected behavior may occur because the dimensions of a Square cannot (or rather should not) be modified independently.
Learn to code from industry experts! Enroll here
Liskov Substitution Principle Example
This problem cannot be easily fixed: if we can modify the setter methods in the Square class so that they preserve the Square invariant (i.e., keep the dimensions equal), then these methods will weaken (violate) the post-conditions for the Rectangle setters, which state that dimensions can be modified independently.
public class Rectangle {
private double height;
private double width;
public double area();
public void setHeight(double height);
public void setWidth(double width);
}
What you see above violates LSP.
public class Square extends Rectangle {
public void setHeight(double height) {
super.setHeight(height);
super.setWidth(height);
}
public void setWidth(double width) {
setHeight(width);
}
}
Violations of LSP cause undefined behavior, which can lead to weeks of wasted time trying to find out where the bug is.
4. Interface Segregation Principle (ISP)
The Interface Segregation Principle (ISP) states that clients should not be forced to depend upon interface members they do not use. When we have non-cohesive interfaces, the ISP guides us to create multiple, smaller and cohesive interfaces.
When you apply ISP, classes and their dependencies communicate using tightly-focused interfaces, minimizing dependencies on unused members and reducing coupling accordingly. Smaller interfaces are easier to implement, improving flexibility and the possibility of reuse. As fewer classes share these interfaces, the number of changes that are required in response to an interface modification is lowered, which increases robustness.
Interface Segregation Principle Example
Picture an ATM, which has a screen where we wish to display different messages. If you want to add a message on the ATM that appears only for withdrawal functionality, how would you solve it?
Perhaps you would add a method to the Messenger interface and be done with it. But this causes you to recompile all the users of this interface and almost all the system needs to be changed, which is in direct violation of OCP.
What happened here was that changing the withdrawal functionality caused changes to other totally unrelated functionalities as well, which is something we now know we don’t want. How did this happen?
Actually, there is backwards dependency at play, where each functionality that uses this Messengers interface depends on methods it does not need but are needed by other functionalities. Here is what we want to avoid:
public interface Messenger {
askForCard();
tellInvalidCard();
askForPin();
tellInvalidPin();
tellCardWasSiezed();
askForAccount();
tellNotEnoughMoneyInAccount();
tellAmountDeposited();
tellBalance();
}
Instead, split the Messenger interface up so that different ATM functionality depend on separate Messengers.
public interface LoginMessenger {
askForCard();
tellInvalidCard();
askForPin();
tellInvalidPin();
}
public interface WithdrawalMessenger {
tellNotEnoughMoneyInAccount();
askForFeeConfirmation();
}
publc class EnglishMessenger implements LoginMessenger, WithdrawalMessenger {
...
}
5. Dependency Inversion Principle (DIP)
The Dependency Inversion Principle (DIP) states that high-level modules should not depend upon low-level modules , they should depend on abstractions.
Also, abstractions should not depend upon details, details should depend upon abstractions. The idea is that we isolate our class behind a boundary formed by the abstractions it depends on. If all the details behind those abstractions change, then our class is still safe. This helps keep coupling low and makes our design easier to change. DIP also allows us to test things in isolation.
Grab the opportunity to learn Java with Entri! Click Here
Dependency Inversion Principle Example
Example: A program depends on Reader and Writer interfaces that are abstractions, and Keyboard and Printer are details that depend on those abstractions by implementing those interfaces. Here CharCopier is oblivious to the low-level details of Reader and Writer implementations and thus you can pass in any Device that implements the Reader and Writer interface and CharCopier would still work correctly.
public interface Reader { char getchar(); }
public interface Writer { void putchar(char c)}
class CharCopier {
void copy(Reader reader, Writer writer) {
int c;
while ((c = reader.getchar()) != EOF) {
writer.putchar();
}
}
}
public Keyboard implements Reader {...}
public Printer implements Writer {…}
Final Thoughts
SOLID principles are valuable tools in your toolbox, ones you should keep them in back of your mind when designing your next feature or application.