Skip to Content
Course content

163: SOLID Principles in Java

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 PaymentStrategy interface 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();
        }
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.