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
159: Dependency Injection Principles
I want to show you something that usually happens in a codebase over time. We start with a design that seems perfectly logical because it's simple, and then we hit a wall where that simplicity becomes a liability. Let's look at a notification system I'm sketching out.
The Hard-Coded Trap
Imagine we have a UserRegistrationService. Its job is to save a user to the database and then send a welcome email. In my first pass, I'd probably write it like this:
public class UserRegistrationService {
private EmailService emailService = new EmailService();
public void registerUser(String email) {
System.out.println("Saving user to DB...");
emailService.sendWelcomeEmail(email);
}
}
public class EmailService {
public void sendWelcomeEmail(String email) {
System.out.println("Sending email to " + email);
}
}
On the surface, this works. I run it, the email sends, and I'm happy. But here's where the problem starts: my boss comes to me and says, "We need to support SMS notifications for users who don't have email addresses," or "We need to use a Mock service for our unit tests so we don't actually spam real customers every time the build runs."
If I want to change EmailService to SmsService, I have to open UserRegistrationService.java and manually change the code. That feels wrong. The registration service should care that a notification is sent, not how it's sent.
Introducing an Abstraction
Okay, so my first instinct is to create an interface. That way, I'm not tied to a specific class. Let's try that.
public interface MessageService {
void sendMessage(String destination);
}
public class EmailService implements MessageService {
public void sendMessage(String destination) {
System.out.println("Sending email to " + destination);
}
}
public class SmsService implements MessageService {
public void sendMessage(String destination) {
System.out.println("Sending SMS to " + destination);
}
}
Now I'll update my registration service to use the interface:
public class UserRegistrationService {
private MessageService messageService = new EmailService(); // Still hard-coded!
public void registerUser(String contact) {
messageService.sendMessage(contact);
}
}
Wait. I just realized I didn't actually solve the problem. Even though I'm using an interface, I'm still calling new EmailService() inside the constructor. If I want to switch to SMS, I still have to modify the UserRegistrationService class. I've added an abstraction, but I'm still controlling the creation of the dependency inside the class that uses it.
Handing Over the Keys
This is the "aha!" moment. The problem isn't the interface; it's the new keyword. As long as UserRegistrationService is responsible for creating its own tools, it's stuck with whatever tool I hard-coded.
So, let's try something different. What if I just stop creating the service inside the class entirely? What if I tell the class: "You need a MessageService to function, but I'm not going to tell you which one. Someone else will just hand it to you when you're created."
public class UserRegistrationService {
private final MessageService messageService;
// I'm "injecting" the dependency via the constructor
public UserRegistrationService(MessageService messageService) {
this.messageService = messageService;
}
public void registerUser(String contact) {
messageService.sendMessage(contact);
}
}
Now, look at how this changes the way I actually run the program. The decision of which service to use has moved up the chain to the main entry point of the application:
public class Main {
public static void main(String[] args) {
// I can choose Email...
MessageService email = new EmailService();
UserRegistrationService regEmail = new UserRegistrationService(email);
regEmail.registerUser("test@example.com");
// ...or I can choose SMS without touching the UserRegistrationService code!
MessageService sms = new SmsService();
UserRegistrationService regSms = new UserRegistrationService(sms);
regSms.registerUser("555-0123");
}
}
This is Dependency Injection. It sounds like a fancy academic term, but all we did was move the new keyword. Instead of the class reaching out to grab a dependency, we "inject" the dependency into it. The UserRegistrationService is now completely decoupled from the specific implementation of the message service. It's cleaner, it's easier to test, and I don't have to rewrite my core logic every time the business decides to change vendors.
📋 Practical Task
Refactoring the Payment Gateway for Multi-Provider Support
You have been handed a legacy OrderProcessor class that is currently hard-coded to use a PayPalPaymentProvider. Your goal is to refactor this code using Dependency Injection so that the OrderProcessor can work with any payment provider (like Stripe or Square) without needing to be modified.
Requirements:
- Create a
PaymentProviderinterface with a methodprocessPayment(double amount). - Implement two classes,
PayPalProviderandStripeProvider, that implement this interface. - Refactor the
OrderProcessorclass to remove thenew PayPalProvider()call from its internals and instead receive thePaymentProviderthrough its constructor. - In your
mainmethod, demonstrate that you can create anOrderProcessorthat uses PayPal and another that uses Stripe.
There are no comments for now.