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
147: The Factory Pattern
I've seen this exact pattern in dozens of junior-level pull requests: a service class that knows way too much about how its dependencies are created. It usually starts small, but it quickly turns into a maintenance nightmare. Let me show you what I mean.
public class NotificationService {
public void sendNotification(String type, String message) {
Notification notification;
if (type.equals("EMAIL")) {
notification = new EmailNotification("smtp.provider.com", 587);
} else if (type.equals("SMS")) {
notification = new SmsNotification("api.sms-gateway.net", "secret-key");
} else if (type.equals("PUSH")) {
notification = new PushNotification("fcm.google.com");
} else {
throw new IllegalArgumentException("Unknown notification type");
}
notification.send(message);
}
}
The Fragility of Hard-Coded Instantiation
At first glance, this code "works." But look at the NotificationService. Its primary job should be sending notifications, yet it's currently burdened with the knowledge of how to construct every single type of notification. It knows the SMTP server for emails, the API key for SMS, and the endpoint for push notifications.
Here is where it breaks: imagine your boss tells you we need to add WhatsApp notifications. You now have to go into this service class and add another else if block. Then, you realize you need this same logic in a AlertManager class and a UserWelcomeService class. Suddenly, you're copying and pasting that switch statement across your entire codebase. If the SMS API key changes, you're hunting through five different files to update a string. This is a violation of the Single Responsibility Principle, and it makes your code incredibly brittle.
Abstracting Creation with a Factory
The fix is to move the "decision-making" logic about which object to create into its own dedicated class. We call this the Factory Pattern. Instead of the service asking "Which class should I instantiate?", it simply asks the factory for a Notification object and doesn't care how it's made.
First, we ensure all notifications follow a common interface:
public interface Notification {
void send(String message);
}
Now, we build the Factory. I usually make the factory method static for simplicity unless the factory itself needs to be injected with dependencies:
public class NotificationFactory {
public static Notification createNotification(String type) {
return switch (type.toUpperCase()) {
case "EMAIL" -> new EmailNotification("smtp.provider.com", 587);
case "SMS" -> new SmsNotification("api.sms-gateway.net", "secret-key");
case "PUSH" -> new PushNotification("fcm.google.com");
default -> throw new IllegalArgumentException("Unknown type: " + type);
};
}
}
Now, look how clean the NotificationService becomes:
public class NotificationService {
public void sendNotification(String type, String message) {
// The service no longer knows about SMTP servers or API keys
Notification notification = NotificationFactory.createNotification(type);
notification.send(message);
}
}
When to Actually Use This
I'll be honest with you: don't over-engineer this. If you have exactly one implementation of an interface and that will never change, a factory is just extra boilerplate. You're adding a new class and a new layer of indirection for no reason.
Use a Factory when the object creation logic is complex, or when you have multiple implementations of an interface and the specific one you need is determined at runtime (like from a config file or a user input). It keeps your business logic pure and your instantiation logic centralized. If you add a new notification type tomorrow, you change exactly one line in the factory, and the rest of your system just works.
📋 Practical Task
Exercise: Implementing a Document Parser Factory
You are building a data import tool that needs to handle different file formats: CSV, JSON, and XML. Currently, the code is using a messy if/else chain inside the main processing loop.
Your Task:
- Create a
DocumentParserinterface with a methodString parse(String content). - Create three concrete implementation classes:
CsvParser,JsonParser, andXmlParser. Each should return a string indicating how it parsed the data (e.g., "Parsing CSV content..."). - Create a
ParserFactoryclass with a methodgetParser(String format)that returns the appropriateDocumentParserbased on the input string. - In a
Mainclass, demonstrate the factory by requesting a "JSON" parser and a "CSV" parser and calling theparsemethod on each.
There are no comments for now.