1. Introduction to Factory Pattern
What is Factory Pattern?
The Factory Pattern is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created.
Why Use Factory Pattern?
- Loose coupling between object creation and usage
- Centralized object creation logic
- Easy to extend with new product types
- Promotes Open/Closed Principle
- Simplifies complex object creation
Types of Factory Patterns:
- Simple Factory (Static Factory)
- Factory Method
- Abstract Factory
2. Simple Factory Pattern
// Product interface
interface Notification {
void send(String message);
}
// Concrete Products
class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending Email: " + message);
}
}
class SMSNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}
class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending Push Notification: " + message);
}
}
// Simple Factory
class NotificationFactory {
public static Notification createNotification(String type) {
switch (type.toLowerCase()) {
case "email":
return new EmailNotification();
case "sms":
return new SMSNotification();
case "push":
return new PushNotification();
default:
throw new IllegalArgumentException("Unknown notification type: " + type);
}
}
}
public class SimpleFactoryExample {
public static void main(String[] args) {
System.out.println("=== Simple Factory Pattern ===");
// Create notifications using factory
Notification email = NotificationFactory.createNotification("email");
Notification sms = NotificationFactory.createNotification("sms");
Notification push = NotificationFactory.createNotification("push");
// Use notifications
email.send("Welcome to our service!");
sms.send("Your verification code is 123456");
push.send("You have a new message");
// Demonstrating flexibility
System.out.println("\n=== Dynamic Notification Creation ===");
String[] notificationTypes = {"email", "sms", "push", "email"};
for (String type : notificationTypes) {
try {
Notification notification = NotificationFactory.createNotification(type);
notification.send("Batch message via " + type.toUpperCase());
} catch (IllegalArgumentException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
}
3. Complete Code Examples
Example 1: Factory Method Pattern
// Product interface
interface Document {
void open();
void save();
void close();
String getType();
}
// Concrete Products
class WordDocument implements Document {
@Override
public void open() {
System.out.println("Opening Word document...");
}
@Override
public void save() {
System.out.println("Saving Word document...");
}
@Override
public void close() {
System.out.println("Closing Word document...");
}
@Override
public String getType() {
return "Word Document";
}
}
class PDFDocument implements Document {
@Override
public void open() {
System.out.println("Opening PDF document...");
}
@Override
public void save() {
System.out.println("Saving PDF document...");
}
@Override
public void close() {
System.out.println("Closing PDF document...");
}
@Override
public String getType() {
return "PDF Document";
}
}
class ExcelDocument implements Document {
@Override
public void open() {
System.out.println("Opening Excel document...");
}
@Override
public void save() {
System.out.println("Saving Excel document...");
}
@Override
public void close() {
System.out.println("Closing Excel document...");
}
@Override
public String getType() {
return "Excel Document";
}
}
// Creator abstract class
abstract class DocumentCreator {
// Factory method - to be implemented by subclasses
public abstract Document createDocument();
// Some business logic that uses the factory method
public void processDocument() {
Document doc = createDocument();
System.out.println("Processing " + doc.getType());
doc.open();
doc.save();
doc.close();
System.out.println();
}
}
// Concrete Creators
class WordDocumentCreator extends DocumentCreator {
@Override
public Document createDocument() {
return new WordDocument();
}
}
class PDFDocumentCreator extends DocumentCreator {
@Override
public Document createDocument() {
return new PDFDocument();
}
}
class ExcelDocumentCreator extends DocumentCreator {
@Override
public Document createDocument() {
return new ExcelDocument();
}
}
public class FactoryMethodExample {
public static void main(String[] args) {
System.out.println("=== Factory Method Pattern ===");
// Using different creators
DocumentCreator[] creators = {
new WordDocumentCreator(),
new PDFDocumentCreator(),
new ExcelDocumentCreator()
};
// Process documents using factory method
for (DocumentCreator creator : creators) {
creator.processDocument();
}
// Demonstrate creating documents directly
System.out.println("=== Direct Document Creation ===");
DocumentCreator wordCreator = new WordDocumentCreator();
Document wordDoc = wordCreator.createDocument();
wordDoc.open();
wordDoc.save();
wordDoc.close();
// Factory method with parameters
System.out.println("\n=== Parameterized Factory ===");
Document parameterizedDoc = createDocumentByType("pdf");
if (parameterizedDoc != null) {
parameterizedDoc.open();
parameterizedDoc.save();
parameterizedDoc.close();
}
}
// Utility method demonstrating parameterized factory
public static Document createDocumentByType(String type) {
switch (type.toLowerCase()) {
case "word":
return new WordDocumentCreator().createDocument();
case "pdf":
return new PDFDocumentCreator().createDocument();
case "excel":
return new ExcelDocumentCreator().createDocument();
default:
System.err.println("Unknown document type: " + type);
return null;
}
}
}
Example 2: Abstract Factory Pattern
// Abstract Products
interface Button {
void render();
void onClick();
}
interface Checkbox {
void render();
boolean isChecked();
}
interface TextField {
void render();
void setText(String text);
String getText();
}
// Concrete Products for Windows
class WindowsButton implements Button {
@Override
public void render() {
System.out.println("Rendering Windows-style button");
}
@Override
public void onClick() {
System.out.println("Windows button clicked!");
}
}
class WindowsCheckbox implements Checkbox {
private boolean checked = false;
@Override
public void render() {
System.out.println("Rendering Windows-style checkbox");
}
@Override
public boolean isChecked() {
return checked;
}
public void setChecked(boolean checked) {
this.checked = checked;
System.out.println("Windows checkbox " + (checked ? "checked" : "unchecked"));
}
}
class WindowsTextField implements TextField {
private String text = "";
@Override
public void render() {
System.out.println("Rendering Windows-style text field");
}
@Override
public void setText(String text) {
this.text = text;
System.out.println("Windows text field set to: " + text);
}
@Override
public String getText() {
return text;
}
}
// Concrete Products for Mac
class MacButton implements Button {
@Override
public void render() {
System.out.println("Rendering Mac-style button");
}
@Override
public void onClick() {
System.out.println("Mac button clicked!");
}
}
class MacCheckbox implements Checkbox {
private boolean checked = false;
@Override
public void render() {
System.out.println("Rendering Mac-style checkbox");
}
@Override
public boolean isChecked() {
return checked;
}
public void setChecked(boolean checked) {
this.checked = checked;
System.out.println("Mac checkbox " + (checked ? "checked" : "unchecked"));
}
}
class MacTextField implements TextField {
private String text = "";
@Override
public void render() {
System.out.println("Rendering Mac-style text field");
}
@Override
public void setText(String text) {
this.text = text;
System.out.println("Mac text field set to: " + text);
}
@Override
public String getText() {
return text;
}
}
// Concrete Products for Linux
class LinuxButton implements Button {
@Override
public void render() {
System.out.println("Rendering Linux-style button");
}
@Override
public void onClick() {
System.out.println("Linux button clicked!");
}
}
class LinuxCheckbox implements Checkbox {
private boolean checked = false;
@Override
public void render() {
System.out.println("Rendering Linux-style checkbox");
}
@Override
public boolean isChecked() {
return checked;
}
public void setChecked(boolean checked) {
this.checked = checked;
System.out.println("Linux checkbox " + (checked ? "checked" : "unchecked"));
}
}
class LinuxTextField implements TextField {
private String text = "";
@Override
public void render() {
System.out.println("Rendering Linux-style text field");
}
@Override
public void setText(String text) {
this.text = text;
System.out.println("Linux text field set to: " + text);
}
@Override
public String getText() {
return text;
}
}
// Abstract Factory
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
TextField createTextField();
}
// Concrete Factories
class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
@Override
public TextField createTextField() {
return new WindowsTextField();
}
}
class MacFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacButton();
}
@Override
public Checkbox createCheckbox() {
return new MacCheckbox();
}
@Override
public TextField createTextField() {
return new MacTextField();
}
}
class LinuxFactory implements GUIFactory {
@Override
public Button createButton() {
return new LinuxButton();
}
@Override
public Checkbox createCheckbox() {
return new LinuxCheckbox();
}
@Override
public TextField createTextField() {
return new LinuxTextField();
}
}
// Factory provider
class GUIFactoryProvider {
public static GUIFactory getFactory(String osType) {
switch (osType.toLowerCase()) {
case "windows":
return new WindowsFactory();
case "mac":
return new MacFactory();
case "linux":
return new LinuxFactory();
default:
throw new IllegalArgumentException("Unknown OS type: " + osType);
}
}
}
// Client code
class Application {
private Button button;
private Checkbox checkbox;
private TextField textField;
public Application(GUIFactory factory) {
this.button = factory.createButton();
this.checkbox = factory.createCheckbox();
this.textField = factory.createTextField();
}
public void renderUI() {
System.out.println("\n=== Rendering UI ===");
button.render();
checkbox.render();
textField.render();
}
public void simulateUserInteraction() {
System.out.println("\n=== User Interaction ===");
button.onClick();
// Cast to specific type to use platform-specific methods
if (checkbox instanceof WindowsCheckbox) {
((WindowsCheckbox) checkbox).setChecked(true);
} else if (checkbox instanceof MacCheckbox) {
((MacCheckbox) checkbox).setChecked(true);
} else if (checkbox instanceof LinuxCheckbox) {
((LinuxCheckbox) checkbox).setChecked(true);
}
textField.setText("Hello, World!");
System.out.println("Text field contains: " + textField.getText());
}
}
public class AbstractFactoryExample {
public static void main(String[] args) {
System.out.println("=== Abstract Factory Pattern ===");
// Test with different operating systems
String[] operatingSystems = {"windows", "mac", "linux"};
for (String os : operatingSystems) {
System.out.println("\n" + "=".repeat(50));
System.out.println("Creating application for: " + os.toUpperCase());
System.out.println("=".repeat(50));
try {
GUIFactory factory = GUIFactoryProvider.getFactory(os);
Application app = new Application(factory);
app.renderUI();
app.simulateUserInteraction();
} catch (IllegalArgumentException e) {
System.err.println("Error: " + e.getMessage());
}
}
// Demonstrate factory switching at runtime
System.out.println("\n=== Runtime Factory Switching ===");
demonstrateRuntimeSwitching();
}
public static void demonstrateRuntimeSwitching() {
// Simulate detecting OS at runtime
String detectedOS = System.getProperty("os.name").toLowerCase();
String factoryType;
if (detectedOS.contains("win")) {
factoryType = "windows";
} else if (detectedOS.contains("mac")) {
factoryType = "mac";
} else {
factoryType = "linux";
}
System.out.println("Detected OS: " + detectedOS);
System.out.println("Using factory: " + factoryType);
GUIFactory factory = GUIFactoryProvider.getFactory(factoryType);
Application app = new Application(factory);
app.renderUI();
}
}
Example 3: Real-World Database Connection Factory
import java.sql.*;
import java.util.*;
// Product interface
interface DatabaseConnection {
Connection getConnection() throws SQLException;
void close() throws SQLException;
boolean testConnection() throws SQLException;
String getDatabaseType();
}
// Concrete Products
class MySQLConnection implements DatabaseConnection {
private Connection connection;
private final String url;
private final String username;
private final String password;
public MySQLConnection(String host, int port, String database, String username, String password) {
this.url = String.format("jdbc:mysql://%s:%d/%s", host, port, database);
this.username = username;
this.password = password;
}
@Override
public Connection getConnection() throws SQLException {
if (connection == null || connection.isClosed()) {
connection = DriverManager.getConnection(url, username, password);
}
return connection;
}
@Override
public void close() throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
@Override
public boolean testConnection() throws SQLException {
try (Connection conn = getConnection()) {
return conn.isValid(2); // 2 second timeout
}
}
@Override
public String getDatabaseType() {
return "MySQL";
}
}
class PostgreSQLConnection implements DatabaseConnection {
private Connection connection;
private final String url;
private final String username;
private final String password;
public PostgreSQLConnection(String host, int port, String database, String username, String password) {
this.url = String.format("jdbc:postgresql://%s:%d/%s", host, port, database);
this.username = username;
this.password = password;
}
@Override
public Connection getConnection() throws SQLException {
if (connection == null || connection.isClosed()) {
connection = DriverManager.getConnection(url, username, password);
}
return connection;
}
@Override
public void close() throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
@Override
public boolean testConnection() throws SQLException {
try (Connection conn = getConnection()) {
return conn.isValid(2);
}
}
@Override
public String getDatabaseType() {
return "PostgreSQL";
}
}
class OracleConnection implements DatabaseConnection {
private Connection connection;
private final String url;
private final String username;
private final String password;
public OracleConnection(String host, int port, String service, String username, String password) {
this.url = String.format("jdbc:oracle:thin:@%s:%d:%s", host, port, service);
this.username = username;
this.password = password;
}
@Override
public Connection getConnection() throws SQLException {
if (connection == null || connection.isClosed()) {
connection = DriverManager.getConnection(url, username, password);
}
return connection;
}
@Override
public void close() throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
@Override
public boolean testConnection() throws SQLException {
try (Connection conn = getConnection()) {
return conn.isValid(2);
}
}
@Override
public String getDatabaseType() {
return "Oracle";
}
}
// Connection Configuration
class ConnectionConfig {
private final String host;
private final int port;
private final String database;
private final String username;
private final String password;
private final Map<String, String> properties;
public ConnectionConfig(String host, int port, String database, String username, String password) {
this.host = host;
this.port = port;
this.database = database;
this.username = username;
this.password = password;
this.properties = new HashMap<>();
}
public ConnectionConfig addProperty(String key, String value) {
this.properties.put(key, value);
return this;
}
// Getters
public String getHost() { return host; }
public int getPort() { return port; }
public String getDatabase() { return database; }
public String getUsername() { return username; }
public String getPassword() { return password; }
public Map<String, String> getProperties() { return new HashMap<>(properties); }
}
// Abstract Factory
interface ConnectionFactory {
DatabaseConnection createConnection(ConnectionConfig config);
String getSupportedDatabase();
}
// Concrete Factories
class MySQLConnectionFactory implements ConnectionFactory {
@Override
public DatabaseConnection createConnection(ConnectionConfig config) {
return new MySQLConnection(
config.getHost(),
config.getPort(),
config.getDatabase(),
config.getUsername(),
config.getPassword()
);
}
@Override
public String getSupportedDatabase() {
return "MySQL";
}
}
class PostgreSQLConnectionFactory implements ConnectionFactory {
@Override
public DatabaseConnection createConnection(ConnectionConfig config) {
return new PostgreSQLConnection(
config.getHost(),
config.getPort(),
config.getDatabase(),
config.getUsername(),
config.getPassword()
);
}
@Override
public String getSupportedDatabase() {
return "PostgreSQL";
}
}
class OracleConnectionFactory implements ConnectionFactory {
@Override
public DatabaseConnection createConnection(ConnectionConfig config) {
return new OracleConnection(
config.getHost(),
config.getPort(),
config.getDatabase(), // Used as service name for Oracle
config.getUsername(),
config.getPassword()
);
}
@Override
public String getSupportedDatabase() {
return "Oracle";
}
}
// Factory Provider with Registry
class ConnectionFactoryRegistry {
private static final Map<String, ConnectionFactory> factories = new HashMap<>();
static {
registerFactory("mysql", new MySQLConnectionFactory());
registerFactory("postgresql", new PostgreSQLConnectionFactory());
registerFactory("oracle", new OracleConnectionFactory());
}
public static void registerFactory(String databaseType, ConnectionFactory factory) {
factories.put(databaseType.toLowerCase(), factory);
}
public static ConnectionFactory getFactory(String databaseType) {
ConnectionFactory factory = factories.get(databaseType.toLowerCase());
if (factory == null) {
throw new IllegalArgumentException("Unsupported database type: " + databaseType);
}
return factory;
}
public static Set<String> getSupportedDatabases() {
return new HashSet<>(factories.keySet());
}
}
// Connection Pool using Factory
class ConnectionPool {
private final ConnectionFactory factory;
private final ConnectionConfig config;
private final List<DatabaseConnection> availableConnections;
private final List<DatabaseConnection> usedConnections;
private final int maxPoolSize;
public ConnectionPool(String databaseType, ConnectionConfig config, int maxPoolSize) {
this.factory = ConnectionFactoryRegistry.getFactory(databaseType);
this.config = config;
this.maxPoolSize = maxPoolSize;
this.availableConnections = new ArrayList<>();
this.usedConnections = new ArrayList<>();
initializePool();
}
private void initializePool() {
for (int i = 0; i < Math.min(3, maxPoolSize); i++) { // Start with 3 connections
availableConnections.add(factory.createConnection(config));
}
}
public DatabaseConnection getConnection() throws SQLException {
if (availableConnections.isEmpty()) {
if (usedConnections.size() < maxPoolSize) {
availableConnections.add(factory.createConnection(config));
} else {
throw new SQLException("Connection pool exhausted");
}
}
DatabaseConnection connection = availableConnections.remove(availableConnections.size() - 1);
if (!connection.testConnection()) {
// Create new connection if test fails
connection = factory.createConnection(config);
}
usedConnections.add(connection);
return connection;
}
public void releaseConnection(DatabaseConnection connection) {
usedConnections.remove(connection);
availableConnections.add(connection);
}
public void closeAllConnections() throws SQLException {
for (DatabaseConnection conn : availableConnections) {
conn.close();
}
for (DatabaseConnection conn : usedConnections) {
conn.close();
}
availableConnections.clear();
usedConnections.clear();
}
public int getAvailableCount() {
return availableConnections.size();
}
public int getUsedCount() {
return usedConnections.size();
}
}
public class DatabaseConnectionFactoryExample {
public static void main(String[] args) {
System.out.println("=== Database Connection Factory Pattern ===");
// Display supported databases
System.out.println("Supported databases: " + ConnectionFactoryRegistry.getSupportedDatabases());
// Test different database connections
testDatabaseConnections();
// Test connection pool
testConnectionPool();
// Demonstrate dynamic factory registration
demonstrateDynamicRegistration();
}
public static void testDatabaseConnections() {
System.out.println("\n=== Testing Database Connections ===");
// Configuration for different databases
ConnectionConfig mysqlConfig = new ConnectionConfig("localhost", 3306, "testdb", "user", "password");
ConnectionConfig postgresConfig = new ConnectionConfig("localhost", 5432, "testdb", "user", "password");
ConnectionConfig oracleConfig = new ConnectionConfig("localhost", 1521, "XE", "user", "password");
String[] databases = {"mysql", "postgresql", "oracle"};
ConnectionConfig[] configs = {mysqlConfig, postgresConfig, oracleConfig};
for (int i = 0; i < databases.length; i++) {
String dbType = databases[i];
ConnectionConfig config = configs[i];
System.out.println("\nTesting " + dbType.toUpperCase() + " connection:");
try {
ConnectionFactory factory = ConnectionFactoryRegistry.getFactory(dbType);
DatabaseConnection connection = factory.createConnection(config);
System.out.println("Database type: " + connection.getDatabaseType());
System.out.println("Connection test: " + (connection.testConnection() ? "PASS" : "FAIL"));
// Try to get actual connection (will fail without real database, but demonstrates pattern)
try {
Connection conn = connection.getConnection();
System.out.println("Connection obtained successfully");
connection.close();
} catch (SQLException e) {
System.out.println("Expected connection failure (no real database): " + e.getMessage());
}
} catch (Exception e) {
System.err.println("Error with " + dbType + ": " + e.getMessage());
}
}
}
public static void testConnectionPool() {
System.out.println("\n=== Testing Connection Pool ===");
ConnectionConfig config = new ConnectionConfig("localhost", 3306, "testdb", "user", "password");
try (ConnectionPool pool = new ConnectionPool("mysql", config, 5)) {
System.out.println("Pool created. Available connections: " + pool.getAvailableCount());
// Simulate getting connections
List<DatabaseConnection> connections = new ArrayList<>();
for (int i = 0; i < 3; i++) {
try {
DatabaseConnection conn = pool.getConnection();
connections.add(conn);
System.out.printf("Got connection %d. Available: %d, Used: %d%n",
i + 1, pool.getAvailableCount(), pool.getUsedCount());
} catch (SQLException e) {
System.err.println("Failed to get connection: " + e.getMessage());
}
}
// Release connections
for (DatabaseConnection conn : connections) {
pool.releaseConnection(conn);
System.out.printf("Released connection. Available: %d, Used: %d%n",
pool.getAvailableCount(), pool.getUsedCount());
}
} catch (Exception e) {
System.err.println("Pool error: " + e.getMessage());
}
}
public static void demonstrateDynamicRegistration() {
System.out.println("\n=== Dynamic Factory Registration ===");
// Create a custom database factory
ConnectionFactory sqliteFactory = new ConnectionFactory() {
@Override
public DatabaseConnection createConnection(ConnectionConfig config) {
return new DatabaseConnection() {
@Override
public Connection getConnection() throws SQLException {
throw new SQLException("SQLite not implemented in this example");
}
@Override
public void close() throws SQLException {
// No-op for demo
}
@Override
public boolean testConnection() throws SQLException {
return true; // Always return true for demo
}
@Override
public String getDatabaseType() {
return "SQLite";
}
};
}
@Override
public String getSupportedDatabase() {
return "sqlite";
}
};
// Register the custom factory
ConnectionFactoryRegistry.registerFactory("sqlite", sqliteFactory);
System.out.println("Registered custom factory for: sqlite");
System.out.println("Now supported databases: " + ConnectionFactoryRegistry.getSupportedDatabases());
// Test the custom factory
try {
ConnectionFactory factory = ConnectionFactoryRegistry.getFactory("sqlite");
DatabaseConnection connection = factory.createConnection(
new ConnectionConfig("", 0, "test.db", "", "")
);
System.out.println("Custom connection type: " + connection.getDatabaseType());
System.out.println("Custom connection test: " + (connection.testConnection() ? "PASS" : "FAIL"));
} catch (Exception e) {
System.err.println("Custom factory test failed: " + e.getMessage());
}
}
}
Example 4: Payment Processing System
import java.util.*;
// Payment method interface
interface PaymentMethod {
boolean processPayment(double amount, String currency);
boolean refund(double amount, String currency);
String getPaymentMethodName();
Map<String, String> getPaymentDetails();
}
// Concrete Payment Methods
class CreditCardPayment implements PaymentMethod {
private String cardNumber;
private String cardHolder;
private String expiryDate;
private String cvv;
public CreditCardPayment(String cardNumber, String cardHolder, String expiryDate, String cvv) {
this.cardNumber = cardNumber;
this.cardHolder = cardHolder;
this.expiryDate = expiryDate;
this.cvv = cvv;
}
@Override
public boolean processPayment(double amount, String currency) {
System.out.printf("Processing credit card payment: %.2f %s%n", amount, currency);
System.out.printf("Card: %s, Holder: %s%n", maskCardNumber(cardNumber), cardHolder);
// Simulate payment processing
boolean success = Math.random() > 0.1; // 90% success rate for demo
System.out.println("Payment " + (success ? "approved" : "declined"));
return success;
}
@Override
public boolean refund(double amount, String currency) {
System.out.printf("Processing credit card refund: %.2f %s%n", amount, currency);
System.out.printf("Card: %s%n", maskCardNumber(cardNumber));
// Simulate refund processing
boolean success = Math.random() > 0.05; // 95% success rate for demo
System.out.println("Refund " + (success ? "processed" : "failed"));
return success;
}
@Override
public String getPaymentMethodName() {
return "Credit Card";
}
@Override
public Map<String, String> getPaymentDetails() {
Map<String, String> details = new HashMap<>();
details.put("cardNumber", maskCardNumber(cardNumber));
details.put("cardHolder", cardHolder);
details.put("expiryDate", expiryDate);
details.put("type", "VISA"); // Simplified
return details;
}
private String maskCardNumber(String cardNumber) {
if (cardNumber.length() <= 4) return cardNumber;
return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
}
}
class PayPalPayment implements PaymentMethod {
private String email;
private String transactionId;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public boolean processPayment(double amount, String currency) {
System.out.printf("Processing PayPal payment: %.2f %s%n", amount, currency);
System.out.printf("Email: %s%n", email);
// Simulate PayPal processing
boolean success = Math.random() > 0.05; // 95% success rate for demo
if (success) {
this.transactionId = "PP" + System.currentTimeMillis();
System.out.println("Payment approved. Transaction ID: " + transactionId);
} else {
System.out.println("Payment declined");
}
return success;
}
@Override
public boolean refund(double amount, String currency) {
System.out.printf("Processing PayPal refund: %.2f %s%n", amount, currency);
System.out.printf("Transaction ID: %s%n", transactionId);
// Simulate refund processing
boolean success = Math.random() > 0.02; // 98% success rate for demo
System.out.println("Refund " + (success ? "processed" : "failed"));
return success;
}
@Override
public String getPaymentMethodName() {
return "PayPal";
}
@Override
public Map<String, String> getPaymentDetails() {
Map<String, String> details = new HashMap<>();
details.put("email", email);
details.put("transactionId", transactionId != null ? transactionId : "N/A");
return details;
}
}
class BankTransferPayment implements PaymentMethod {
private String accountNumber;
private String routingNumber;
private String accountHolder;
public BankTransferPayment(String accountNumber, String routingNumber, String accountHolder) {
this.accountNumber = accountNumber;
this.routingNumber = routingNumber;
this.accountHolder = accountHolder;
}
@Override
public boolean processPayment(double amount, String currency) {
System.out.printf("Processing bank transfer: %.2f %s%n", amount, currency);
System.out.printf("Account: %s, Holder: %s%n", maskAccountNumber(accountNumber), accountHolder);
// Bank transfers are always "successful" immediately in this simulation
System.out.println("Transfer initiated successfully");
return true;
}
@Override
public boolean refund(double amount, String currency) {
System.out.printf("Processing bank transfer refund: %.2f %s%n", amount, currency);
System.out.println("Bank transfer refunds require manual processing");
return false; // Bank transfers typically don't support instant refunds
}
@Override
public String getPaymentMethodName() {
return "Bank Transfer";
}
@Override
public Map<String, String> getPaymentDetails() {
Map<String, String> details = new HashMap<>();
details.put("accountNumber", maskAccountNumber(accountNumber));
details.put("routingNumber", routingNumber);
details.put("accountHolder", accountHolder);
return details;
}
private String maskAccountNumber(String accountNumber) {
if (accountNumber.length() <= 4) return accountNumber;
return "***" + accountNumber.substring(accountNumber.length() - 4);
}
}
class CryptocurrencyPayment implements PaymentMethod {
private String walletAddress;
private String cryptocurrency;
public CryptocurrencyPayment(String walletAddress, String cryptocurrency) {
this.walletAddress = walletAddress;
this.cryptocurrency = cryptocurrency;
}
@Override
public boolean processPayment(double amount, String currency) {
System.out.printf("Processing %s payment: %.2f %s%n", cryptocurrency, amount, currency);
System.out.printf("Wallet: %s%n", maskWalletAddress(walletAddress));
// Simulate crypto transaction
boolean success = Math.random() > 0.2; // 80% success rate for demo
if (success) {
System.out.println("Transaction confirmed on blockchain");
} else {
System.out.println("Transaction failed or pending");
}
return success;
}
@Override
public boolean refund(double amount, String currency) {
System.out.printf("Processing %s refund: %.2f %s%n", cryptocurrency, amount, currency);
System.out.println("Cryptocurrency refunds require new transaction");
// In crypto, refunds are just new transactions
return processPayment(amount, currency);
}
@Override
public String getPaymentMethodName() {
return cryptocurrency + " (Crypto)";
}
@Override
public Map<String, String> getPaymentDetails() {
Map<String, String> details = new HashMap<>();
details.put("walletAddress", maskWalletAddress(walletAddress));
details.put("cryptocurrency", cryptocurrency);
return details;
}
private String maskWalletAddress(String walletAddress) {
if (walletAddress.length() <= 8) return walletAddress;
return walletAddress.substring(0, 4) + "..." + walletAddress.substring(walletAddress.length() - 4);
}
}
// Payment Factory
class PaymentFactory {
public static PaymentMethod createPaymentMethod(String type, Map<String, String> parameters) {
switch (type.toLowerCase()) {
case "creditcard":
return new CreditCardPayment(
parameters.get("cardNumber"),
parameters.get("cardHolder"),
parameters.get("expiryDate"),
parameters.get("cvv")
);
case "paypal":
return new PayPalPayment(parameters.get("email"));
case "banktransfer":
return new BankTransferPayment(
parameters.get("accountNumber"),
parameters.get("routingNumber"),
parameters.get("accountHolder")
);
case "crypto":
return new CryptocurrencyPayment(
parameters.get("walletAddress"),
parameters.get("cryptocurrency")
);
default:
throw new IllegalArgumentException("Unsupported payment method: " + type);
}
}
}
// Payment Processor
class PaymentProcessor {
private PaymentMethod paymentMethod;
public void setPaymentMethod(PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
}
public boolean processPayment(double amount, String currency) {
if (paymentMethod == null) {
throw new IllegalStateException("Payment method not set");
}
System.out.println("\n" + "=".repeat(50));
System.out.println("PROCESSING PAYMENT");
System.out.println("=".repeat(50));
boolean success = paymentMethod.processPayment(amount, currency);
if (success) {
System.out.println("✅ Payment completed successfully");
} else {
System.out.println("❌ Payment failed");
}
return success;
}
public boolean processRefund(double amount, String currency) {
if (paymentMethod == null) {
throw new IllegalStateException("Payment method not set");
}
System.out.println("\n" + "=".repeat(50));
System.out.println("PROCESSING REFUND");
System.out.println("=".repeat(50));
boolean success = paymentMethod.refund(amount, currency);
if (success) {
System.out.println("✅ Refund completed successfully");
} else {
System.out.println("❌ Refund failed or requires manual processing");
}
return success;
}
public void displayPaymentDetails() {
if (paymentMethod != null) {
System.out.println("\nPayment Method: " + paymentMethod.getPaymentMethodName());
Map<String, String> details = paymentMethod.getPaymentDetails();
details.forEach((key, value) -> System.out.printf(" %s: %s%n", key, value));
}
}
}
public class PaymentProcessingExample {
public static void main(String[] args) {
System.out.println("=== Payment Processing Factory Pattern ===");
PaymentProcessor processor = new PaymentProcessor();
// Test different payment methods
testCreditCardPayment(processor);
testPayPalPayment(processor);
testBankTransferPayment(processor);
testCryptocurrencyPayment(processor);
// Demonstrate dynamic payment method switching
demonstrateDynamicSwitching(processor);
}
public static void testCreditCardPayment(PaymentProcessor processor) {
System.out.println("\n" + "=".repeat(60));
System.out.println("TESTING CREDIT CARD PAYMENT");
System.out.println("=".repeat(60));
Map<String, String> params = new HashMap<>();
params.put("cardNumber", "4111111111111111");
params.put("cardHolder", "John Doe");
params.put("expiryDate", "12/25");
params.put("cvv", "123");
PaymentMethod creditCard = PaymentFactory.createPaymentMethod("creditcard", params);
processor.setPaymentMethod(creditCard);
processor.displayPaymentDetails();
processor.processPayment(99.99, "USD");
processor.processRefund(25.00, "USD");
}
public static void testPayPalPayment(PaymentProcessor processor) {
System.out.println("\n" + "=".repeat(60));
System.out.println("TESTING PAYPAL PAYMENT");
System.out.println("=".repeat(60));
Map<String, String> params = new HashMap<>();
params.put("email", "[email protected]");
PaymentMethod paypal = PaymentFactory.createPaymentMethod("paypal", params);
processor.setPaymentMethod(paypal);
processor.displayPaymentDetails();
processor.processPayment(49.99, "EUR");
processor.processRefund(49.99, "EUR");
}
public static void testBankTransferPayment(PaymentProcessor processor) {
System.out.println("\n" + "=".repeat(60));
System.out.println("TESTING BANK TRANSFER PAYMENT");
System.out.println("=".repeat(60));
Map<String, String> params = new HashMap<>();
params.put("accountNumber", "1234567890");
params.put("routingNumber", "021000021");
params.put("accountHolder", "John Doe");
PaymentMethod bankTransfer = PaymentFactory.createPaymentMethod("banktransfer", params);
processor.setPaymentMethod(bankTransfer);
processor.displayPaymentDetails();
processor.processPayment(199.99, "USD");
processor.processRefund(199.99, "USD");
}
public static void testCryptocurrencyPayment(PaymentProcessor processor) {
System.out.println("\n" + "=".repeat(60));
System.out.println("TESTING CRYPTOCURRENCY PAYMENT");
System.out.println("=".repeat(60));
Map<String, String> params = new HashMap<>();
params.put("walletAddress", "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa");
params.put("cryptocurrency", "Bitcoin");
PaymentMethod crypto = PaymentFactory.createPaymentMethod("crypto", params);
processor.setPaymentMethod(crypto);
processor.displayPaymentDetails();
processor.processPayment(0.005, "BTC");
processor.processRefund(0.001, "BTC");
}
public static void demonstrateDynamicSwitching(PaymentProcessor processor) {
System.out.println("\n" + "=".repeat(60));
System.out.println("DYNAMIC PAYMENT METHOD SWITCHING");
System.out.println("=".repeat(60));
// Simulate user selecting different payment methods
String[] paymentTypes = {"creditcard", "paypal", "crypto"};
double[] amounts = {75.50, 120.00, 0.0025};
String[] currencies = {"USD", "EUR", "BTC"};
for (int i = 0; i < paymentTypes.length; i++) {
System.out.printf("\n--- Transaction %d: %s ---%n", i + 1, paymentTypes[i].toUpperCase());
Map<String, String> params = new HashMap<>();
switch (paymentTypes[i]) {
case "creditcard":
params.put("cardNumber", "5555555555554444");
params.put("cardHolder", "Jane Smith");
params.put("expiryDate", "06/26");
params.put("cvv", "456");
break;
case "paypal":
params.put("email", "[email protected]");
break;
case "crypto":
params.put("walletAddress", "0x742d35Cc6634C0532925a3b8D");
params.put("cryptocurrency", "Ethereum");
break;
}
try {
PaymentMethod method = PaymentFactory.createPaymentMethod(paymentTypes[i], params);
processor.setPaymentMethod(method);
processor.displayPaymentDetails();
processor.processPayment(amounts[i], currencies[i]);
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
}
}
}
}
Example 5: Advanced Factory with Configuration and Caching
import java.util.*;
import java.util.concurrent.*;
// Product interface
interface Cache {
void put(String key, Object value);
Object get(String key);
void remove(String key);
void clear();
int size();
String getCacheType();
Map<String, Object> getStats();
}
// Concrete Products
class MemoryCache implements Cache {
private final Map<String, Object> storage = new ConcurrentHashMap<>();
private final Map<String, Long> accessTimes = new ConcurrentHashMap<>();
private final int maxSize;
private int hitCount = 0;
private int missCount = 0;
public MemoryCache(int maxSize) {
this.maxSize = maxSize;
}
@Override
public void put(String key, Object value) {
if (storage.size() >= maxSize) {
// Simple LRU eviction
evictLeastRecentlyUsed();
}
storage.put(key, value);
accessTimes.put(key, System.currentTimeMillis());
}
@Override
public Object get(String key) {
Object value = storage.get(key);
if (value != null) {
accessTimes.put(key, System.currentTimeMillis());
hitCount++;
} else {
missCount++;
}
return value;
}
@Override
public void remove(String key) {
storage.remove(key);
accessTimes.remove(key);
}
@Override
public void clear() {
storage.clear();
accessTimes.clear();
hitCount = 0;
missCount = 0;
}
@Override
public int size() {
return storage.size();
}
@Override
public String getCacheType() {
return "Memory Cache";
}
@Override
public Map<String, Object> getStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("size", size());
stats.put("maxSize", maxSize);
stats.put("hitCount", hitCount);
stats.put("missCount", missCount);
stats.put("hitRate", hitCount + missCount > 0 ?
(double) hitCount / (hitCount + missCount) : 0.0);
return stats;
}
private void evictLeastRecentlyUsed() {
String lruKey = null;
long oldestTime = Long.MAX_VALUE;
for (Map.Entry<String, Long> entry : accessTimes.entrySet()) {
if (entry.getValue() < oldestTime) {
oldestTime = entry.getValue();
lruKey = entry.getKey();
}
}
if (lruKey != null) {
remove(lruKey);
}
}
}
class DiskCache implements Cache {
private final Map<String, Object> storage = new ConcurrentHashMap<>();
private final String cacheDir;
private int hitCount = 0;
private int missCount = 0;
public DiskCache(String cacheDir) {
this.cacheDir = cacheDir;
// In real implementation, this would interact with actual disk storage
}
@Override
public void put(String key, Object value) {
storage.put(key, value);
// Simulate disk write
System.out.println("Writing to disk: " + key);
}
@Override
public Object get(String key) {
Object value = storage.get(key);
if (value != null) {
hitCount++;
} else {
missCount++;
}
return value;
}
@Override
public void remove(String key) {
storage.remove(key);
// Simulate disk removal
System.out.println("Removing from disk: " + key);
}
@Override
public void clear() {
storage.clear();
hitCount = 0;
missCount = 0;
System.out.println("Cleared disk cache");
}
@Override
public int size() {
return storage.size();
}
@Override
public String getCacheType() {
return "Disk Cache";
}
@Override
public Map<String, Object> getStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("size", size());
stats.put("cacheDir", cacheDir);
stats.put("hitCount", hitCount);
stats.put("missCount", missCount);
stats.put("hitRate", hitCount + missCount > 0 ?
(double) hitCount / (hitCount + missCount) : 0.0);
return stats;
}
}
class RedisCache implements Cache {
private final Map<String, Object> storage = new ConcurrentHashMap<>();
private final String redisUrl;
private final int redisPort;
private int hitCount = 0;
private int missCount = 0;
public RedisCache(String redisUrl, int redisPort) {
this.redisUrl = redisUrl;
this.redisPort = redisPort;
// In real implementation, this would connect to Redis
}
@Override
public void put(String key, Object value) {
storage.put(key, value);
// Simulate Redis SET operation
System.out.println("Redis SET: " + key);
}
@Override
public Object get(String key) {
Object value = storage.get(key);
if (value != null) {
hitCount++;
} else {
missCount++;
}
return value;
}
@Override
public void remove(String key) {
storage.remove(key);
// Simulate Redis DEL operation
System.out.println("Redis DEL: " + key);
}
@Override
public void clear() {
storage.clear();
hitCount = 0;
missCount = 0;
System.out.println("Redis FLUSHDB");
}
@Override
public int size() {
return storage.size();
}
@Override
public String getCacheType() {
return "Redis Cache";
}
@Override
public Map<String, Object> getStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("size", size());
stats.put("redisUrl", redisUrl);
stats.put("redisPort", redisPort);
stats.put("hitCount", hitCount);
stats.put("missCount", missCount);
stats.put("hitRate", hitCount + missCount > 0 ?
(double) hitCount / (hitCount + missCount) : 0.0);
return stats;
}
}
// Cache Configuration
class CacheConfig {
private final String cacheType;
private final Map<String, Object> properties;
public CacheConfig(String cacheType) {
this.cacheType = cacheType;
this.properties = new HashMap<>();
}
public CacheConfig setProperty(String key, Object value) {
properties.put(key, value);
return this;
}
public String getCacheType() { return cacheType; }
public Map<String, Object> getProperties() { return new HashMap<>(properties); }
public <T> T getProperty(String key, T defaultValue) {
return properties.containsKey(key) ? (T) properties.get(key) : defaultValue;
}
}
// Abstract Factory
interface CacheFactory {
Cache createCache(CacheConfig config);
boolean supportsType(String cacheType);
}
// Concrete Factories
class MemoryCacheFactory implements CacheFactory {
@Override
public Cache createCache(CacheConfig config) {
int maxSize = config.getProperty("maxSize", 1000);
return new MemoryCache(maxSize);
}
@Override
public boolean supportsType(String cacheType) {
return "memory".equalsIgnoreCase(cacheType);
}
}
class DiskCacheFactory implements CacheFactory {
@Override
public Cache createCache(CacheConfig config) {
String cacheDir = config.getProperty("cacheDir", "/tmp/cache");
return new DiskCache(cacheDir);
}
@Override
public boolean supportsType(String cacheType) {
return "disk".equalsIgnoreCase(cacheType);
}
}
class RedisCacheFactory implements CacheFactory {
@Override
public Cache createCache(CacheConfig config) {
String redisUrl = config.getProperty("redisUrl", "localhost");
int redisPort = config.getProperty("redisPort", 6379);
return new RedisCache(redisUrl, redisPort);
}
@Override
public boolean supportsType(String cacheType) {
return "redis".equalsIgnoreCase(cacheType);
}
}
// Factory Manager with Registration
class CacheFactoryManager {
private static final Map<String, CacheFactory> factories = new HashMap<>();
private static final Map<String, Cache> cacheInstances = new ConcurrentHashMap<>();
static {
// Register default factories
registerFactory("memory", new MemoryCacheFactory());
registerFactory("disk", new DiskCacheFactory());
registerFactory("redis", new RedisCacheFactory());
}
public static void registerFactory(String cacheType, CacheFactory factory) {
factories.put(cacheType.toLowerCase(), factory);
}
public static Cache createCache(CacheConfig config) {
String cacheType = config.getCacheType().toLowerCase();
CacheFactory factory = factories.get(cacheType);
if (factory == null) {
throw new IllegalArgumentException("Unsupported cache type: " + cacheType);
}
return factory.createCache(config);
}
public static Cache getOrCreateCache(String cacheName, CacheConfig config) {
return cacheInstances.computeIfAbsent(cacheName, k -> createCache(config));
}
public static Set<String> getSupportedCacheTypes() {
return new HashSet<>(factories.keySet());
}
public static void clearCacheInstances() {
cacheInstances.values().forEach(Cache::clear);
cacheInstances.clear();
}
}
// Cache Manager
class CacheManager {
private final Map<String, Cache> caches = new ConcurrentHashMap<>();
public Cache createCache(String name, CacheConfig config) {
Cache cache = CacheFactoryManager.createCache(config);
caches.put(name, cache);
System.out.println("Created cache: " + name + " (" + cache.getCacheType() + ")");
return cache;
}
public Cache getCache(String name) {
return caches.get(name);
}
public void removeCache(String name) {
Cache cache = caches.remove(name);
if (cache != null) {
cache.clear();
}
}
public Set<String> getCacheNames() {
return new HashSet<>(caches.keySet());
}
public void displayStats() {
System.out.println("\n=== Cache Statistics ===");
caches.forEach((name, cache) -> {
System.out.println("\nCache: " + name + " (" + cache.getCacheType() + ")");
Map<String, Object> stats = cache.getStats();
stats.forEach((key, value) -> System.out.printf(" %s: %s%n", key, value));
});
}
}
public class AdvancedCacheFactoryExample {
public static void main(String[] args) {
System.out.println("=== Advanced Cache Factory Pattern ===");
// Display supported cache types
System.out.println("Supported cache types: " + CacheFactoryManager.getSupportedCacheTypes());
CacheManager cacheManager = new CacheManager();
// Create different types of caches
createVariousCaches(cacheManager);
// Test cache operations
testCacheOperations(cacheManager);
// Demonstrate cache statistics
cacheManager.displayStats();
// Test performance
testCachePerformance();
// Clean up
CacheFactoryManager.clearCacheInstances();
}
public static void createVariousCaches(CacheManager cacheManager) {
System.out.println("\n=== Creating Various Caches ===");
// Memory cache with small size
CacheConfig memoryConfig = new CacheConfig("memory")
.setProperty("maxSize", 100);
cacheManager.createCache("smallMemoryCache", memoryConfig);
// Memory cache with large size
CacheConfig largeMemoryConfig = new CacheConfig("memory")
.setProperty("maxSize", 10000);
cacheManager.createCache("largeMemoryCache", largeMemoryConfig);
// Disk cache
CacheConfig diskConfig = new CacheConfig("disk")
.setProperty("cacheDir", "/var/cache/myapp");
cacheManager.createCache("diskCache", diskConfig);
// Redis cache
CacheConfig redisConfig = new CacheConfig("redis")
.setProperty("redisUrl", "redis.example.com")
.setProperty("redisPort", 6379);
cacheManager.createCache("redisCache", redisConfig);
System.out.println("Created caches: " + cacheManager.getCacheNames());
}
public static void testCacheOperations(CacheManager cacheManager) {
System.out.println("\n=== Testing Cache Operations ===");
Cache memoryCache = cacheManager.getCache("smallMemoryCache");
if (memoryCache != null) {
// Basic operations
memoryCache.put("key1", "value1");
memoryCache.put("key2", 12345);
memoryCache.put("key3", Arrays.asList("a", "b", "c"));
System.out.println("Cache size: " + memoryCache.size());
System.out.println("Get key1: " + memoryCache.get("key1"));
System.out.println("Get key2: " + memoryCache.get("key2"));
System.out.println("Get non-existent key: " + memoryCache.get("key999"));
// Test eviction by adding more items
for (int i = 0; i < 150; i++) {
memoryCache.put("testKey" + i, "testValue" + i);
}
System.out.println("Cache size after adding 150 items: " + memoryCache.size());
// Test multiple caches
Cache largeMemoryCache = cacheManager.getCache("largeMemoryCache");
if (largeMemoryCache != null) {
largeMemoryCache.put("largeKey", "This is in large cache");
System.out.println("Large cache get: " + largeMemoryCache.get("largeKey"));
}
}
}
public static void testCachePerformance() {
System.out.println("\n=== Cache Performance Test ===");
CacheConfig config = new CacheConfig("memory")
.setProperty("maxSize", 5000);
Cache cache = CacheFactoryManager.createCache(config);
int operations = 1000;
long startTime, endTime;
// Write performance
startTime = System.currentTimeMillis();
for (int i = 0; i < operations; i++) {
cache.put("perfKey" + i, "perfValue" + i);
}
endTime = System.currentTimeMillis();
System.out.printf("Write %d operations: %d ms%n", operations, (endTime - startTime));
// Read performance
startTime = System.currentTimeMillis();
for (int i = 0; i < operations; i++) {
cache.get("perfKey" + i);
}
endTime = System.currentTimeMillis();
System.out.printf("Read %d operations: %d ms%n", operations, (endTime - startTime));
// Mixed operations
startTime = System.currentTimeMillis();
for (int i = 0; i < operations; i++) {
if (i % 2 == 0) {
cache.put("mixedKey" + i, "mixedValue" + i);
} else {
cache.get("mixedKey" + (i - 1));
}
}
endTime = System.currentTimeMillis();
System.out.printf("Mixed %d operations: %d ms%n", operations, (endTime - startTime));
cache.clear();
}
}
9. Conclusion
Key Takeaways:
- Simple Factory: Static method that creates objects based on parameters
- Factory Method: Subclasses decide which class to instantiate
- Abstract Factory: Families of related objects without specifying concrete classes
When to Use Factory Pattern:
- ✅ Object creation logic is complex
- ✅ Need to decouple object creation from usage
- ✅ System should be independent of how objects are created
- ✅ Want to provide a library of objects that can be extended
- ✅ Need to create families of related objects
Benefits:
- Loose coupling between client and concrete classes
- Single Responsibility Principle - creation logic in one place
- Open/Closed Principle - easy to introduce new types
- Code organization and maintainability
- Configuration flexibility
Common Use Cases:
- Database connections and connection pools
- Logger frameworks with different appenders
- UI toolkits with different look-and-feels
- Payment processors with different payment methods
- Cache systems with different storage backends
Best Practices:
- Use meaningful names for factory methods
- Consider using configuration files for factory setup
- Implement proper error handling for creation failures
- Use dependency injection with factories for better testability
- Consider caching for expensive object creation
Comparison:
| Pattern | Use Case | Complexity | Flexibility |
|---|---|---|---|
| Simple Factory | Simple object creation | Low | Limited |
| Factory Method | Subclass-specific creation | Medium | High |
| Abstract Factory | Families of related objects | High | Very High |
Final Thoughts:
The Factory Pattern is one of the most widely used creational patterns in Java. It provides:
- Clean separation of object creation and business logic
- Enhanced testability through dependency injection
- Improved maintainability through centralized creation logic
- Greater flexibility for future extensions
Master the Factory Pattern to write more flexible, maintainable, and testable Java applications!