Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
163: SOLID Principles in Java
I once worked with a junior dev who had built what he called the "Universal Order Manager." It was a single Java class, about 2,000 lines long, that handled everything: validating the order, calculating taxes, charging the credit card, sending the confirmation email, and updating the warehouse inventory. For a while, it worked great. Then, the business decided to add PayPal and Bitcoin as payment options. Because the payment logic was hard-coded right in the middle of the order flow using a giant if-else block, adding PayPal meant touching the same lines of code that handled credit cards. He accidentally deleted a semicolon or shifted a bracket, and suddenly, nobody could pay with a Visa card anymore. He spent an entire weekend hunting for a bug that he created while trying to add a feature that had nothing to do with Visa.
That's the nightmare we avoid by using the SOLID principles. They aren't strict laws carved in stone, but rather a set of heuristics to keep your code from becoming a house of cards. When you follow them, you stop fearing the "ripple effect"—where a change in one class breaks five others in a completely different part of the system.
Decoupling Responsibility and Openness
The first two letters, S and O, are where most of your daily wins happen. The Single Responsibility Principle (SRP) tells us that a class should have one, and only one, reason to change. In the case of our unlucky developer, the Order Manager had five reasons to change: tax laws, payment gateways, email templates, inventory logic, and order validation. By splitting those into TaxCalculator, PaymentProcessor, and NotificationService, you isolate the risk. If the email server changes, you only touch the notification class; the payment logic remains untouched and safe.
Then there's the Open/Closed Principle (OCP): software entities should be open for extension, but closed for modification. This is where Java interfaces shine. Instead of writing a processPayment() method with a switch statement for every payment type, you create a PaymentMethod interface. Now, when the business wants to add "Apple Pay," you don't modify your existing, tested code. You simply create a new class ApplePay implements PaymentMethod. You've extended the system's behavior without risking the stability of the existing logic.
// The OCP way: No need to change this class when adding new payment types
public class OrderService {
public void completeOrder(Order order, PaymentMethod method) {
method.process(order.getTotal());
// ... other logic
}
}
Substituting Logic and Segregating Interfaces
The Liskov Substitution Principle (LSP) is often the most confusing, but it's really just about honesty in your inheritance. It says that if Square extends Rectangle, you should be able to use a Square anywhere you use a Rectangle without the program crashing or behaving weirdly. If your subclass throws an UnsupportedOperationException for a method it inherited, you've violated LSP. You're lying to the compiler about what the object can actually do. If a subclass can't fulfill the contract of the parent, it shouldn't be a subclass.
Closely related is the Interface Segregation Principle (ISP). I've seen "Fat Interfaces" that have 20 different methods, forcing every implementing class to write // do nothing in ten of them. Don't do this. It's better to have four small, specific interfaces than one giant one. If you have a Worker interface with work() and eat(), a RobotWorker class is forced to implement eat(), which makes no sense. Split them into Workable and Feedable.
Inverting the Dependency Chain
Finally, we have the Dependency Inversion Principle (DIP). This is the "secret sauce" that makes unit testing actually possible. The core idea is that high-level modules should not depend on low-level modules; both should depend on abstractions.
If your OrderService explicitly creates a new SqlDatabase() inside its constructor, it's tightly coupled. You can't test the service without a live SQL database running. Instead, you should depend on a Database interface and "inject" the implementation via the constructor. I usually call this "flipping the script." The service no longer cares how the data is saved, only that it is saved. This allows you to swap a real database for a mock object during testing, which is the only way to keep your CI/CD pipeline fast.
// Dependency Inversion in action
public class OrderService {
private final Repository repo;
// We depend on the interface (abstraction), not the concrete SqlRepository
public OrderService(Repository repo) {
this.repo = repo;
}
}📋 Practical Task
Refactoring the Monolithic Payment Processor
You have been handed a legacy class called PaymentManager. It is currently a "God Object" that violates several SOLID principles. It handles the logic for three different payment types (CreditCard, PayPal, Crypto) using a series of if/else statements, and it also handles the logging of transactions directly to a file.
Your Goal: Refactor this code into a SOLID-compliant structure.
Requirements:
- Apply SRP: Move the logging logic out of the payment processing class and into its own dedicated service.
- Apply OCP: Create a
PaymentStrategyinterface so that new payment methods can be added without modifying the main processing logic. - Apply DIP: Ensure the main service depends on the interface rather than concrete implementations.
// STARTING CODE (Refactor this!)
public class PaymentManager {
public void process(String type, double amount) {
if (type.equals("CreditCard")) {
System.out.println("Processing credit card payment of $" + amount);
} else if (type.equals("PayPal")) {
System.out.println("Processing PayPal payment of $" + amount);
} else if (type.equals("Crypto")) {
System.out.println("Processing Crypto payment of $" + amount);
} else {
throw new IllegalArgumentException("Unknown payment type");
}
// Hard-coded logging logic (Violates SRP)
try {
FileWriter writer = new FileWriter("log.txt", true);
writer.write("Processed " + type + " payment of " + amount + "\n");
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}There are no comments for now.