๐ฏ Introduction
Design patterns are proven solutions to commonly occurring problems in software design. They represent best practices evolved over time and provide a shared vocabulary for developers. This comprehensive guide explores the most essential design patterns in Java, demonstrating practical implementations with real-world examples.
We’ll cover the three main categories of design patterns from the Gang of Four: Creational, Structural, and Behavioral patterns, showing how to implement them effectively in modern Java applications.
๐ญ Creational Patterns
Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.
๐ง Singleton Pattern
The Singleton pattern ensures a class has only one instance and provides global access to that instance.
graph TD
A[Client] --> B[Singleton.getInstance()]
B --> C{Instance exists?}
C -->|No| D[Create new instance]
C -->|Yes| E[Return existing instance]
D --> F[Store in static field]
F --> G[Return instance]
E --> G
style A fill:#ff6b6b
style G fill:#4ecdc4
style C fill:#feca57
๐ ๏ธ Thread-Safe Singleton Implementation
1. Enum Singleton (Recommended):
1public enum DatabaseManager {
2 INSTANCE;
3
4 private Connection connection;
5 private final Properties config;
6
7 DatabaseManager() {
8 this.config = loadConfiguration();
9 initializeConnection();
10 }
11
12 public Connection getConnection() {
13 if (connection == null || !isConnectionValid()) {
14 synchronized (this) {
15 if (connection == null || !isConnectionValid()) {
16 initializeConnection();
17 }
18 }
19 }
20 return connection;
21 }
22
23 private void initializeConnection() {
24 try {
25 String url = config.getProperty("database.url");
26 String username = config.getProperty("database.username");
27 String password = config.getProperty("database.password");
28
29 this.connection = DriverManager.getConnection(url, username, password);
30 System.out.println("Database connection initialized");
31
32 } catch (SQLException e) {
33 throw new RuntimeException("Failed to initialize database connection", e);
34 }
35 }
36
37 private boolean isConnectionValid() {
38 try {
39 return connection != null && !connection.isClosed() && connection.isValid(5);
40 } catch (SQLException e) {
41 return false;
42 }
43 }
44
45 private Properties loadConfiguration() {
46 Properties props = new Properties();
47 try (InputStream input = getClass().getClassLoader().getResourceAsStream("database.properties")) {
48 if (input != null) {
49 props.load(input);
50 } else {
51 // Default configuration
52 props.setProperty("database.url", "jdbc:h2:mem:testdb");
53 props.setProperty("database.username", "sa");
54 props.setProperty("database.password", "");
55 }
56 } catch (IOException e) {
57 throw new RuntimeException("Failed to load database configuration", e);
58 }
59 return props;
60 }
61
62 public void executeQuery(String sql) {
63 try (PreparedStatement stmt = getConnection().prepareStatement(sql)) {
64 ResultSet rs = stmt.executeQuery();
65 // Process results
66 while (rs.next()) {
67 System.out.println("Query result: " + rs.getString(1));
68 }
69 } catch (SQLException e) {
70 System.err.println("Query execution failed: " + e.getMessage());
71 }
72 }
73
74 public void closeConnection() {
75 if (connection != null) {
76 try {
77 connection.close();
78 System.out.println("Database connection closed");
79 } catch (SQLException e) {
80 System.err.println("Error closing connection: " + e.getMessage());
81 }
82 }
83 }
84}
85
86// Usage
87public class DatabaseExample {
88 public static void main(String[] args) {
89 // Get singleton instance
90 DatabaseManager dbManager = DatabaseManager.INSTANCE;
91
92 // Use the database manager
93 dbManager.executeQuery("SELECT 1");
94
95 // Same instance everywhere
96 DatabaseManager anotherReference = DatabaseManager.INSTANCE;
97 System.out.println("Same instance: " + (dbManager == anotherReference)); // true
98
99 // Cleanup
100 dbManager.closeConnection();
101 }
102}
2. Double-Checked Locking Singleton:
1public class ConfigurationManager {
2 private static volatile ConfigurationManager instance;
3 private final Map<String, String> properties;
4 private final long lastModified;
5
6 private ConfigurationManager() {
7 this.properties = new ConcurrentHashMap<>();
8 this.lastModified = System.currentTimeMillis();
9 loadConfiguration();
10 }
11
12 public static ConfigurationManager getInstance() {
13 if (instance == null) {
14 synchronized (ConfigurationManager.class) {
15 if (instance == null) {
16 instance = new ConfigurationManager();
17 }
18 }
19 }
20 return instance;
21 }
22
23 private void loadConfiguration() {
24 // Simulate loading configuration from file
25 properties.put("app.name", "Design Patterns Demo");
26 properties.put("app.version", "1.0.0");
27 properties.put("app.environment", "production");
28 properties.put("database.pool.size", "20");
29 properties.put("cache.ttl.seconds", "3600");
30
31 System.out.println("Configuration loaded with " + properties.size() + " properties");
32 }
33
34 public String getProperty(String key) {
35 return properties.get(key);
36 }
37
38 public String getProperty(String key, String defaultValue) {
39 return properties.getOrDefault(key, defaultValue);
40 }
41
42 public void setProperty(String key, String value) {
43 properties.put(key, value);
44 System.out.println("Property updated: " + key + " = " + value);
45 }
46
47 public Map<String, String> getAllProperties() {
48 return new HashMap<>(properties);
49 }
50
51 public void reloadConfiguration() {
52 synchronized (this) {
53 properties.clear();
54 loadConfiguration();
55 System.out.println("Configuration reloaded");
56 }
57 }
58}
โ Pros and Cons
โ Advantages:
- Controlled access to sole instance
- Reduced namespace pollution
- Lazy initialization possible
- Thread-safe with proper implementation
โ Disadvantages:
- Hidden dependencies (hard to test)
- Violates Single Responsibility Principle
- Difficult to subclass
- Global state can cause issues
๐ฏ Use Cases:
- Configuration managers
- Database connection pools
- Logging systems
- Cache managers
๐ญ Factory Method Pattern
The Factory Method pattern creates objects without specifying the exact class to create.
graph TD
A[Client] --> B[NotificationFactory]
B --> C{Notification Type?}
C -->|EMAIL| D[EmailNotification]
C -->|SMS| E[SmsNotification]
C -->|PUSH| F[PushNotification]
D --> G[send()]
E --> G
F --> G
style A fill:#ff6b6b
style B fill:#4ecdc4
style G fill:#feca57
๐ ๏ธ Factory Method Implementation
1// Notification interface
2public interface Notification {
3 void send(String recipient, String message);
4 String getType();
5 boolean isDelivered();
6}
7
8// Concrete notification implementations
9public class EmailNotification implements Notification {
10 private boolean delivered = false;
11
12 @Override
13 public void send(String recipient, String message) {
14 System.out.println("Sending EMAIL to " + recipient);
15 System.out.println("Subject: Notification");
16 System.out.println("Message: " + message);
17
18 // Simulate email sending
19 try {
20 Thread.sleep(100);
21 delivered = Math.random() > 0.1; // 90% success rate
22
23 if (delivered) {
24 System.out.println("Email delivered successfully");
25 } else {
26 System.out.println("Email delivery failed");
27 }
28 } catch (InterruptedException e) {
29 Thread.currentThread().interrupt();
30 }
31 }
32
33 @Override
34 public String getType() {
35 return "EMAIL";
36 }
37
38 @Override
39 public boolean isDelivered() {
40 return delivered;
41 }
42}
43
44public class SmsNotification implements Notification {
45 private boolean delivered = false;
46
47 @Override
48 public void send(String recipient, String message) {
49 System.out.println("Sending SMS to " + recipient);
50 System.out.println("Message: " + message);
51
52 // Simulate SMS sending
53 try {
54 Thread.sleep(50);
55 delivered = Math.random() > 0.05; // 95% success rate
56
57 if (delivered) {
58 System.out.println("SMS delivered successfully");
59 } else {
60 System.out.println("SMS delivery failed");
61 }
62 } catch (InterruptedException e) {
63 Thread.currentThread().interrupt();
64 }
65 }
66
67 @Override
68 public String getType() {
69 return "SMS";
70 }
71
72 @Override
73 public boolean isDelivered() {
74 return delivered;
75 }
76}
77
78public class PushNotification implements Notification {
79 private boolean delivered = false;
80
81 @Override
82 public void send(String recipient, String message) {
83 System.out.println("Sending PUSH notification to " + recipient);
84 System.out.println("Message: " + message);
85
86 // Simulate push notification
87 try {
88 Thread.sleep(20);
89 delivered = Math.random() > 0.02; // 98% success rate
90
91 if (delivered) {
92 System.out.println("Push notification delivered successfully");
93 } else {
94 System.out.println("Push notification delivery failed");
95 }
96 } catch (InterruptedException e) {
97 Thread.currentThread().interrupt();
98 }
99 }
100
101 @Override
102 public String getType() {
103 return "PUSH";
104 }
105
106 @Override
107 public boolean isDelivered() {
108 return delivered;
109 }
110}
111
112// Factory class
113public class NotificationFactory {
114
115 public enum NotificationType {
116 EMAIL, SMS, PUSH
117 }
118
119 // Factory method
120 public static Notification createNotification(NotificationType type) {
121 switch (type) {
122 case EMAIL:
123 return new EmailNotification();
124 case SMS:
125 return new SmsNotification();
126 case PUSH:
127 return new PushNotification();
128 default:
129 throw new IllegalArgumentException("Unknown notification type: " + type);
130 }
131 }
132
133 // Factory method with validation
134 public static Notification createNotification(String type) {
135 try {
136 NotificationType notificationType = NotificationType.valueOf(type.toUpperCase());
137 return createNotification(notificationType);
138 } catch (IllegalArgumentException e) {
139 throw new IllegalArgumentException("Invalid notification type: " + type +
140 ". Supported types: " + Arrays.toString(NotificationType.values()));
141 }
142 }
143
144 // Factory method with multiple notifications
145 public static List<Notification> createNotifications(NotificationType... types) {
146 List<Notification> notifications = new ArrayList<>();
147
148 for (NotificationType type : types) {
149 notifications.add(createNotification(type));
150 }
151
152 return notifications;
153 }
154}
155
156// Notification service using factory
157public class NotificationService {
158 private final List<Notification> sentNotifications = new ArrayList<>();
159
160 public void sendNotification(NotificationType type, String recipient, String message) {
161 Notification notification = NotificationFactory.createNotification(type);
162 notification.send(recipient, message);
163 sentNotifications.add(notification);
164 }
165
166 public void sendMultiChannelNotification(String recipient, String message,
167 NotificationType... channels) {
168 List<Notification> notifications = NotificationFactory.createNotifications(channels);
169
170 for (Notification notification : notifications) {
171 notification.send(recipient, message);
172 sentNotifications.add(notification);
173 }
174 }
175
176 public void printDeliveryReport() {
177 System.out.println("\n=== Delivery Report ===");
178 Map<String, Long> deliveryStats = sentNotifications.stream()
179 .collect(Collectors.groupingBy(
180 Notification::getType,
181 Collectors.counting()
182 ));
183
184 Map<String, Long> successStats = sentNotifications.stream()
185 .filter(Notification::isDelivered)
186 .collect(Collectors.groupingBy(
187 Notification::getType,
188 Collectors.counting()
189 ));
190
191 deliveryStats.forEach((type, total) -> {
192 long successful = successStats.getOrDefault(type, 0L);
193 double successRate = (double) successful / total * 100;
194 System.out.printf("%s: %d sent, %d delivered (%.1f%% success rate)%n",
195 type, total, successful, successRate);
196 });
197 }
198}
199
200// Usage example
201public class FactoryPatternExample {
202 public static void main(String[] args) {
203 NotificationService service = new NotificationService();
204
205 // Send individual notifications
206 service.sendNotification(NotificationType.EMAIL, "user@example.com", "Welcome to our service!");
207 service.sendNotification(NotificationType.SMS, "+1234567890", "Your verification code is 12345");
208 service.sendNotification(NotificationType.PUSH, "user_device_id", "You have a new message");
209
210 // Send multi-channel notification
211 service.sendMultiChannelNotification(
212 "premium_user@example.com",
213 "Your premium subscription expires soon",
214 NotificationType.EMAIL,
215 NotificationType.PUSH
216 );
217
218 // Print delivery statistics
219 service.printDeliveryReport();
220 }
221}
๐จ Builder Pattern
The Builder pattern constructs complex objects step by step, allowing different representations using the same construction process.
graph TD
A[Client] --> B[UserBuilder]
B --> C[setName()]
C --> D[setEmail()]
D --> E[setAge()]
E --> F[addRole()]
F --> G[build()]
G --> H[User Object]
style A fill:#ff6b6b
style B fill:#4ecdc4
style H fill:#feca57
๐ ๏ธ Builder Pattern Implementation
1// Complex User class
2public class User {
3 // Required parameters
4 private final String username;
5 private final String email;
6
7 // Optional parameters
8 private final String firstName;
9 private final String lastName;
10 private final int age;
11 private final String phone;
12 private final String address;
13 private final List<String> roles;
14 private final boolean isActive;
15 private final LocalDateTime createdAt;
16 private final Map<String, String> preferences;
17
18 // Private constructor - only Builder can create instances
19 private User(UserBuilder builder) {
20 this.username = builder.username;
21 this.email = builder.email;
22 this.firstName = builder.firstName;
23 this.lastName = builder.lastName;
24 this.age = builder.age;
25 this.phone = builder.phone;
26 this.address = builder.address;
27 this.roles = Collections.unmodifiableList(new ArrayList<>(builder.roles));
28 this.isActive = builder.isActive;
29 this.createdAt = builder.createdAt;
30 this.preferences = Collections.unmodifiableMap(new HashMap<>(builder.preferences));
31 }
32
33 // Getters
34 public String getUsername() { return username; }
35 public String getEmail() { return email; }
36 public String getFirstName() { return firstName; }
37 public String getLastName() { return lastName; }
38 public int getAge() { return age; }
39 public String getPhone() { return phone; }
40 public String getAddress() { return address; }
41 public List<String> getRoles() { return roles; }
42 public boolean isActive() { return isActive; }
43 public LocalDateTime getCreatedAt() { return createdAt; }
44 public Map<String, String> getPreferences() { return preferences; }
45
46 public String getFullName() {
47 if (firstName != null && lastName != null) {
48 return firstName + " " + lastName;
49 } else if (firstName != null) {
50 return firstName;
51 } else if (lastName != null) {
52 return lastName;
53 } else {
54 return username;
55 }
56 }
57
58 public boolean hasRole(String role) {
59 return roles.contains(role);
60 }
61
62 @Override
63 public String toString() {
64 return String.format("User{username='%s', email='%s', fullName='%s', roles=%s, active=%s}",
65 username, email, getFullName(), roles, isActive);
66 }
67
68 // Static method to create builder
69 public static UserBuilder builder(String username, String email) {
70 return new UserBuilder(username, email);
71 }
72
73 // Builder class
74 public static class UserBuilder {
75 // Required parameters
76 private final String username;
77 private final String email;
78
79 // Optional parameters with default values
80 private String firstName;
81 private String lastName;
82 private int age = 0;
83 private String phone;
84 private String address;
85 private List<String> roles = new ArrayList<>();
86 private boolean isActive = true;
87 private LocalDateTime createdAt = LocalDateTime.now();
88 private Map<String, String> preferences = new HashMap<>();
89
90 // Constructor with required parameters
91 public UserBuilder(String username, String email) {
92 if (username == null || username.trim().isEmpty()) {
93 throw new IllegalArgumentException("Username cannot be null or empty");
94 }
95 if (email == null || !isValidEmail(email)) {
96 throw new IllegalArgumentException("Invalid email address");
97 }
98
99 this.username = username.trim();
100 this.email = email.toLowerCase().trim();
101 }
102
103 public UserBuilder firstName(String firstName) {
104 this.firstName = firstName;
105 return this;
106 }
107
108 public UserBuilder lastName(String lastName) {
109 this.lastName = lastName;
110 return this;
111 }
112
113 public UserBuilder age(int age) {
114 if (age < 0 || age > 120) {
115 throw new IllegalArgumentException("Age must be between 0 and 120");
116 }
117 this.age = age;
118 return this;
119 }
120
121 public UserBuilder phone(String phone) {
122 this.phone = phone;
123 return this;
124 }
125
126 public UserBuilder address(String address) {
127 this.address = address;
128 return this;
129 }
130
131 public UserBuilder addRole(String role) {
132 if (role != null && !role.trim().isEmpty()) {
133 this.roles.add(role.trim().toUpperCase());
134 }
135 return this;
136 }
137
138 public UserBuilder roles(String... roles) {
139 for (String role : roles) {
140 addRole(role);
141 }
142 return this;
143 }
144
145 public UserBuilder roles(List<String> roles) {
146 for (String role : roles) {
147 addRole(role);
148 }
149 return this;
150 }
151
152 public UserBuilder active(boolean isActive) {
153 this.isActive = isActive;
154 return this;
155 }
156
157 public UserBuilder createdAt(LocalDateTime createdAt) {
158 this.createdAt = createdAt != null ? createdAt : LocalDateTime.now();
159 return this;
160 }
161
162 public UserBuilder preference(String key, String value) {
163 if (key != null && value != null) {
164 this.preferences.put(key, value);
165 }
166 return this;
167 }
168
169 public UserBuilder preferences(Map<String, String> preferences) {
170 if (preferences != null) {
171 this.preferences.putAll(preferences);
172 }
173 return this;
174 }
175
176 // Build method with validation
177 public User build() {
178 validateBuild();
179 return new User(this);
180 }
181
182 private void validateBuild() {
183 // Additional validation before building
184 if (age > 0 && age < 13 && roles.contains("ADMIN")) {
185 throw new IllegalStateException("Users under 13 cannot have admin role");
186 }
187
188 // Ensure at least one role is assigned
189 if (roles.isEmpty()) {
190 roles.add("USER");
191 }
192 }
193
194 private boolean isValidEmail(String email) {
195 return email != null && email.matches("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$");
196 }
197 }
198}
199
200// Usage examples
201public class BuilderPatternExample {
202 public static void main(String[] args) {
203 // Simple user
204 User basicUser = User.builder("johndoe", "john@example.com")
205 .firstName("John")
206 .lastName("Doe")
207 .build();
208
209 System.out.println("Basic user: " + basicUser);
210
211 // Complex user with all options
212 User adminUser = User.builder("admin", "admin@company.com")
213 .firstName("System")
214 .lastName("Administrator")
215 .age(35)
216 .phone("+1-555-0123")
217 .address("123 Admin Street, Server City, Cloud State 12345")
218 .roles("ADMIN", "USER", "MODERATOR")
219 .preference("theme", "dark")
220 .preference("notifications", "enabled")
221 .preference("language", "en-US")
222 .build();
223
224 System.out.println("Admin user: " + adminUser);
225 System.out.println("Admin roles: " + adminUser.getRoles());
226 System.out.println("Admin preferences: " + adminUser.getPreferences());
227
228 // User with custom creation time
229 User historicalUser = User.builder("historical", "historical@example.com")
230 .firstName("Historical")
231 .lastName("User")
232 .createdAt(LocalDateTime.of(2020, 1, 1, 0, 0))
233 .active(false)
234 .roles(Arrays.asList("LEGACY_USER", "ARCHIVED"))
235 .build();
236
237 System.out.println("Historical user: " + historicalUser);
238 System.out.println("Created at: " + historicalUser.getCreatedAt());
239
240 // Demonstrate method chaining flexibility
241 User flexibleUser = User.builder("flexible", "flexible@example.com")
242 .addRole("USER")
243 .age(28)
244 .addRole("PREMIUM")
245 .firstName("Flexible")
246 .preference("tier", "premium")
247 .addRole("BETA_TESTER")
248 .build();
249
250 System.out.println("Flexible user: " + flexibleUser);
251
252 // Building users in a factory-like method
253 createSampleUsers();
254 }
255
256 private static void createSampleUsers() {
257 System.out.println("\n=== Sample Users ===");
258
259 List<User> users = Arrays.asList(
260 User.builder("user1", "user1@example.com")
261 .firstName("Alice")
262 .lastName("Smith")
263 .age(29)
264 .roles("USER", "PREMIUM")
265 .build(),
266
267 User.builder("user2", "user2@example.com")
268 .firstName("Bob")
269 .lastName("Johnson")
270 .age(34)
271 .addRole("USER")
272 .preference("newsletter", "weekly")
273 .build(),
274
275 User.builder("user3", "user3@example.com")
276 .firstName("Charlie")
277 .age(42)
278 .roles("ADMIN", "USER")
279 .phone("+1-555-9999")
280 .build()
281 );
282
283 users.forEach(System.out::println);
284
285 // Demonstrate role checking
286 System.out.println("\nAdmin users:");
287 users.stream()
288 .filter(user -> user.hasRole("ADMIN"))
289 .forEach(user -> System.out.println(" " + user.getFullName()));
290 }
291}
โ Pros and Cons
โ Advantages:
- Flexible object construction
- Immutable objects possible
- Clear, readable code
- Step-by-step construction
โ Disadvantages:
- More verbose code
- Additional classes needed
- Memory overhead
- Complex for simple objects
๐ฏ Use Cases:
- Complex configuration objects
- Immutable data transfer objects
- SQL query builders
- Test data builders
๐๏ธ Structural Patterns
Structural patterns deal with object composition, creating relationships between objects to form larger structures.
๐ญ Decorator Pattern
The Decorator pattern attaches additional responsibilities to objects dynamically, providing a flexible alternative to subclassing.
graph TD
A[Client] --> B[Coffee Interface]
B --> C[SimpleCoffee]
B --> D[CoffeeDecorator]
D --> E[MilkDecorator]
D --> F[SugarDecorator]
D --> G[WhipDecorator]
E --> H[wraps Coffee]
F --> H
G --> H
style A fill:#ff6b6b
style B fill:#4ecdc4
style H fill:#feca57
๐ ๏ธ Decorator Pattern Implementation
1// Base coffee interface
2public interface Coffee {
3 String getDescription();
4 double getCost();
5 List<String> getIngredients();
6}
7
8// Base coffee implementation
9public class SimpleCoffee implements Coffee {
10 @Override
11 public String getDescription() {
12 return "Simple Coffee";
13 }
14
15 @Override
16 public double getCost() {
17 return 2.00;
18 }
19
20 @Override
21 public List<String> getIngredients() {
22 return Arrays.asList("Coffee Beans", "Water");
23 }
24}
25
26// Base decorator
27public abstract class CoffeeDecorator implements Coffee {
28 protected final Coffee coffee;
29
30 public CoffeeDecorator(Coffee coffee) {
31 this.coffee = coffee;
32 }
33
34 @Override
35 public String getDescription() {
36 return coffee.getDescription();
37 }
38
39 @Override
40 public double getCost() {
41 return coffee.getCost();
42 }
43
44 @Override
45 public List<String> getIngredients() {
46 return new ArrayList<>(coffee.getIngredients());
47 }
48}
49
50// Concrete decorators
51public class MilkDecorator extends CoffeeDecorator {
52 public MilkDecorator(Coffee coffee) {
53 super(coffee);
54 }
55
56 @Override
57 public String getDescription() {
58 return coffee.getDescription() + ", with Milk";
59 }
60
61 @Override
62 public double getCost() {
63 return coffee.getCost() + 0.50;
64 }
65
66 @Override
67 public List<String> getIngredients() {
68 List<String> ingredients = super.getIngredients();
69 ingredients.add("Milk");
70 return ingredients;
71 }
72}
73
74public class SugarDecorator extends CoffeeDecorator {
75 private final int cubes;
76
77 public SugarDecorator(Coffee coffee, int cubes) {
78 super(coffee);
79 this.cubes = cubes;
80 }
81
82 public SugarDecorator(Coffee coffee) {
83 this(coffee, 1);
84 }
85
86 @Override
87 public String getDescription() {
88 return coffee.getDescription() + ", with " + cubes + " sugar cube(s)";
89 }
90
91 @Override
92 public double getCost() {
93 return coffee.getCost() + (0.25 * cubes);
94 }
95
96 @Override
97 public List<String> getIngredients() {
98 List<String> ingredients = super.getIngredients();
99 ingredients.add("Sugar (" + cubes + " cube" + (cubes > 1 ? "s" : "") + ")");
100 return ingredients;
101 }
102}
103
104public class WhipDecorator extends CoffeeDecorator {
105 public WhipDecorator(Coffee coffee) {
106 super(coffee);
107 }
108
109 @Override
110 public String getDescription() {
111 return coffee.getDescription() + ", with Whipped Cream";
112 }
113
114 @Override
115 public double getCost() {
116 return coffee.getCost() + 0.75;
117 }
118
119 @Override
120 public List<String> getIngredients() {
121 List<String> ingredients = super.getIngredients();
122 ingredients.add("Whipped Cream");
123 return ingredients;
124 }
125}
126
127public class VanillaDecorator extends CoffeeDecorator {
128 public VanillaDecorator(Coffee coffee) {
129 super(coffee);
130 }
131
132 @Override
133 public String getDescription() {
134 return coffee.getDescription() + ", with Vanilla Syrup";
135 }
136
137 @Override
138 public double getCost() {
139 return coffee.getCost() + 0.60;
140 }
141
142 @Override
143 public List<String> getIngredients() {
144 List<String> ingredients = super.getIngredients();
145 ingredients.add("Vanilla Syrup");
146 return ingredients;
147 }
148}
149
150// Coffee shop service
151public class CoffeeShop {
152
153 public void printOrder(Coffee coffee) {
154 System.out.println("Order Details:");
155 System.out.println("Description: " + coffee.getDescription());
156 System.out.println("Cost: $" + String.format("%.2f", coffee.getCost()));
157 System.out.println("Ingredients: " + String.join(", ", coffee.getIngredients()));
158 System.out.println("-".repeat(50));
159 }
160
161 // Pre-configured coffee combinations
162 public Coffee createLatte() {
163 return new MilkDecorator(
164 new VanillaDecorator(
165 new SimpleCoffee()
166 )
167 );
168 }
169
170 public Coffee createMochaWithWhip() {
171 return new WhipDecorator(
172 new VanillaDecorator(
173 new MilkDecorator(
174 new SimpleCoffee()
175 )
176 )
177 );
178 }
179
180 public Coffee createSweetCoffee() {
181 return new SugarDecorator(
182 new MilkDecorator(
183 new SimpleCoffee()
184 ),
185 3 // 3 sugar cubes
186 );
187 }
188}
189
190// Usage example
191public class DecoratorPatternExample {
192 public static void main(String[] args) {
193 CoffeeShop shop = new CoffeeShop();
194
195 // Basic coffee
196 Coffee simpleCoffee = new SimpleCoffee();
197 shop.printOrder(simpleCoffee);
198
199 // Coffee with milk
200 Coffee coffeeWithMilk = new MilkDecorator(simpleCoffee);
201 shop.printOrder(coffeeWithMilk);
202
203 // Complex decorated coffee
204 Coffee fancyCoffee = new WhipDecorator(
205 new VanillaDecorator(
206 new SugarDecorator(
207 new MilkDecorator(
208 new SimpleCoffee()
209 ), 2
210 )
211 )
212 );
213 shop.printOrder(fancyCoffee);
214
215 // Pre-configured coffees
216 System.out.println("=== Pre-configured Coffees ===");
217
218 Coffee latte = shop.createLatte();
219 shop.printOrder(latte);
220
221 Coffee mocha = shop.createMochaWithWhip();
222 shop.printOrder(mocha);
223
224 Coffee sweetCoffee = shop.createSweetCoffee();
225 shop.printOrder(sweetCoffee);
226
227 // Demonstrate decorator flexibility
228 demonstrateFlexibility();
229 }
230
231 private static void demonstrateFlexibility() {
232 System.out.println("=== Decorator Flexibility ===");
233
234 // Start with simple coffee
235 Coffee coffee = new SimpleCoffee();
236 System.out.println("1. " + coffee.getDescription() + " - $" + String.format("%.2f", coffee.getCost()));
237
238 // Add decorators one by one
239 coffee = new MilkDecorator(coffee);
240 System.out.println("2. " + coffee.getDescription() + " - $" + String.format("%.2f", coffee.getCost()));
241
242 coffee = new SugarDecorator(coffee, 2);
243 System.out.println("3. " + coffee.getDescription() + " - $" + String.format("%.2f", coffee.getCost()));
244
245 coffee = new VanillaDecorator(coffee);
246 System.out.println("4. " + coffee.getDescription() + " - $" + String.format("%.2f", coffee.getCost()));
247
248 coffee = new WhipDecorator(coffee);
249 System.out.println("5. " + coffee.getDescription() + " - $" + String.format("%.2f", coffee.getCost()));
250
251 System.out.println("Final ingredients: " + String.join(", ", coffee.getIngredients()));
252 }
253}
๐ Adapter Pattern
The Adapter pattern allows incompatible interfaces to work together by acting as a bridge between them.
๐ ๏ธ Adapter Pattern Implementation
1// Target interface (what we want)
2public interface MediaPlayer {
3 void play(String audioType, String fileName);
4}
5
6// Adaptee interfaces (what we have)
7public interface AdvancedMediaPlayer {
8 void playVlc(String fileName);
9 void playMp4(String fileName);
10 void playMov(String fileName);
11}
12
13// Concrete adaptee implementations
14public class VlcPlayer implements AdvancedMediaPlayer {
15 @Override
16 public void playVlc(String fileName) {
17 System.out.println("Playing vlc file: " + fileName);
18 }
19
20 @Override
21 public void playMp4(String fileName) {
22 // Do nothing - not supported
23 }
24
25 @Override
26 public void playMov(String fileName) {
27 // Do nothing - not supported
28 }
29}
30
31public class Mp4Player implements AdvancedMediaPlayer {
32 @Override
33 public void playVlc(String fileName) {
34 // Do nothing - not supported
35 }
36
37 @Override
38 public void playMp4(String fileName) {
39 System.out.println("Playing mp4 file: " + fileName);
40 }
41
42 @Override
43 public void playMov(String fileName) {
44 // Do nothing - not supported
45 }
46}
47
48// Adapter class
49public class MediaAdapter implements MediaPlayer {
50 private final AdvancedMediaPlayer advancedPlayer;
51
52 public MediaAdapter(String audioType) {
53 switch (audioType.toLowerCase()) {
54 case "vlc":
55 advancedPlayer = new VlcPlayer();
56 break;
57 case "mp4":
58 advancedPlayer = new Mp4Player();
59 break;
60 default:
61 throw new IllegalArgumentException("Unsupported audio type: " + audioType);
62 }
63 }
64
65 @Override
66 public void play(String audioType, String fileName) {
67 switch (audioType.toLowerCase()) {
68 case "vlc":
69 advancedPlayer.playVlc(fileName);
70 break;
71 case "mp4":
72 advancedPlayer.playMp4(fileName);
73 break;
74 default:
75 System.out.println("Invalid media. " + audioType + " format not supported");
76 }
77 }
78}
79
80// Context class that uses the adapter
81public class AudioPlayer implements MediaPlayer {
82 private MediaAdapter mediaAdapter;
83
84 @Override
85 public void play(String audioType, String fileName) {
86 // Built-in support for mp3
87 if (audioType.equalsIgnoreCase("mp3")) {
88 System.out.println("Playing mp3 file: " + fileName);
89 }
90 // Use adapter for other formats
91 else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
92 try {
93 mediaAdapter = new MediaAdapter(audioType);
94 mediaAdapter.play(audioType, fileName);
95 } catch (IllegalArgumentException e) {
96 System.out.println("Error: " + e.getMessage());
97 }
98 }
99 else {
100 System.out.println("Invalid media. " + audioType + " format not supported");
101 }
102 }
103}
๐ฏ Behavioral Patterns
Behavioral patterns focus on communication between objects and the assignment of responsibilities between objects.
๐๏ธ Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all dependents are notified.
๐ ๏ธ Observer Pattern Implementation
1// Observer interface
2public interface Observer {
3 void update(String message);
4 String getObserverId();
5}
6
7// Subject interface
8public interface Subject {
9 void attach(Observer observer);
10 void detach(Observer observer);
11 void notifyObservers(String message);
12}
13
14// Concrete subject
15public class NewsAgency implements Subject {
16 private final List<Observer> observers = new ArrayList<>();
17 private String latestNews;
18
19 @Override
20 public void attach(Observer observer) {
21 observers.add(observer);
22 System.out.println("Observer " + observer.getObserverId() + " attached");
23 }
24
25 @Override
26 public void detach(Observer observer) {
27 observers.remove(observer);
28 System.out.println("Observer " + observer.getObserverId() + " detached");
29 }
30
31 @Override
32 public void notifyObservers(String message) {
33 System.out.println("Notifying " + observers.size() + " observers about: " + message);
34 for (Observer observer : observers) {
35 observer.update(message);
36 }
37 }
38
39 public void publishNews(String news) {
40 this.latestNews = news;
41 notifyObservers(news);
42 }
43
44 public String getLatestNews() {
45 return latestNews;
46 }
47}
48
49// Concrete observers
50public class NewsChannel implements Observer {
51 private final String channelId;
52 private String lastNews;
53
54 public NewsChannel(String channelId) {
55 this.channelId = channelId;
56 }
57
58 @Override
59 public void update(String message) {
60 this.lastNews = message;
61 System.out.println("[" + channelId + "] Broadcasting: " + message);
62 }
63
64 @Override
65 public String getObserverId() {
66 return channelId;
67 }
68
69 public String getLastNews() {
70 return lastNews;
71 }
72}
73
74public class MobileApp implements Observer {
75 private final String appId;
76 private final List<String> notifications = new ArrayList<>();
77
78 public MobileApp(String appId) {
79 this.appId = appId;
80 }
81
82 @Override
83 public void update(String message) {
84 notifications.add(message);
85 System.out.println("[" + appId + "] Push notification: " + message);
86 }
87
88 @Override
89 public String getObserverId() {
90 return appId;
91 }
92
93 public List<String> getNotifications() {
94 return new ArrayList<>(notifications);
95 }
96}
97
98// Usage example
99public class ObserverPatternExample {
100 public static void main(String[] args) {
101 // Create subject
102 NewsAgency newsAgency = new NewsAgency();
103
104 // Create observers
105 NewsChannel cnn = new NewsChannel("CNN");
106 NewsChannel bbc = new NewsChannel("BBC");
107 MobileApp newsApp = new MobileApp("NewsApp");
108 MobileApp alertApp = new MobileApp("AlertApp");
109
110 // Attach observers
111 newsAgency.attach(cnn);
112 newsAgency.attach(bbc);
113 newsAgency.attach(newsApp);
114
115 // Publish news
116 newsAgency.publishNews("Breaking: New design patterns guide published!");
117 System.out.println();
118
119 // Add another observer
120 newsAgency.attach(alertApp);
121 newsAgency.publishNews("Technology: Java 21 features announced");
122 System.out.println();
123
124 // Remove an observer
125 newsAgency.detach(bbc);
126 newsAgency.publishNews("Sports: World Cup results are in!");
127 System.out.println();
128
129 // Display observer states
130 System.out.println("=== Observer States ===");
131 System.out.println("CNN last news: " + cnn.getLastNews());
132 System.out.println("BBC last news: " + bbc.getLastNews());
133 System.out.println("NewsApp notifications: " + newsApp.getNotifications().size());
134 System.out.println("AlertApp notifications: " + alertApp.getNotifications().size());
135 }
136}
๐๏ธ Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime.
๐ ๏ธ Strategy Pattern Implementation
1// Strategy interface
2public interface PaymentStrategy {
3 boolean pay(double amount);
4 String getPaymentMethod();
5}
6
7// Concrete strategies
8public class CreditCardPayment implements PaymentStrategy {
9 private final String cardNumber;
10 private final String expiryDate;
11 private final String cvv;
12
13 public CreditCardPayment(String cardNumber, String expiryDate, String cvv) {
14 this.cardNumber = cardNumber;
15 this.expiryDate = expiryDate;
16 this.cvv = cvv;
17 }
18
19 @Override
20 public boolean pay(double amount) {
21 System.out.println("Processing credit card payment of $" + amount);
22 System.out.println("Card: ****-****-****-" + cardNumber.substring(cardNumber.length() - 4));
23
24 // Simulate payment processing
25 try {
26 Thread.sleep(1000);
27 boolean success = Math.random() > 0.1; // 90% success rate
28
29 if (success) {
30 System.out.println("Credit card payment successful!");
31 return true;
32 } else {
33 System.out.println("Credit card payment declined!");
34 return false;
35 }
36 } catch (InterruptedException e) {
37 Thread.currentThread().interrupt();
38 return false;
39 }
40 }
41
42 @Override
43 public String getPaymentMethod() {
44 return "Credit Card";
45 }
46}
47
48public class PayPalPayment implements PaymentStrategy {
49 private final String email;
50 private final String password;
51
52 public PayPalPayment(String email, String password) {
53 this.email = email;
54 this.password = password;
55 }
56
57 @Override
58 public boolean pay(double amount) {
59 System.out.println("Processing PayPal payment of $" + amount);
60 System.out.println("PayPal account: " + email);
61
62 // Simulate payment processing
63 try {
64 Thread.sleep(800);
65 boolean success = Math.random() > 0.05; // 95% success rate
66
67 if (success) {
68 System.out.println("PayPal payment successful!");
69 return true;
70 } else {
71 System.out.println("PayPal payment failed!");
72 return false;
73 }
74 } catch (InterruptedException e) {
75 Thread.currentThread().interrupt();
76 return false;
77 }
78 }
79
80 @Override
81 public String getPaymentMethod() {
82 return "PayPal";
83 }
84}
85
86public class BankTransferPayment implements PaymentStrategy {
87 private final String accountNumber;
88 private final String routingNumber;
89
90 public BankTransferPayment(String accountNumber, String routingNumber) {
91 this.accountNumber = accountNumber;
92 this.routingNumber = routingNumber;
93 }
94
95 @Override
96 public boolean pay(double amount) {
97 System.out.println("Processing bank transfer payment of $" + amount);
98 System.out.println("Account: ****" + accountNumber.substring(accountNumber.length() - 4));
99
100 // Simulate payment processing
101 try {
102 Thread.sleep(1500); // Bank transfers take longer
103 boolean success = Math.random() > 0.02; // 98% success rate
104
105 if (success) {
106 System.out.println("Bank transfer payment successful!");
107 return true;
108 } else {
109 System.out.println("Bank transfer payment failed!");
110 return false;
111 }
112 } catch (InterruptedException e) {
113 Thread.currentThread().interrupt();
114 return false;
115 }
116 }
117
118 @Override
119 public String getPaymentMethod() {
120 return "Bank Transfer";
121 }
122}
123
124// Context class
125public class ShoppingCart {
126 private final List<Item> items = new ArrayList<>();
127 private PaymentStrategy paymentStrategy;
128
129 public void addItem(Item item) {
130 items.add(item);
131 System.out.println("Added to cart: " + item.getName() + " - $" + item.getPrice());
132 }
133
134 public void removeItem(Item item) {
135 items.remove(item);
136 System.out.println("Removed from cart: " + item.getName());
137 }
138
139 public double getTotalAmount() {
140 return items.stream().mapToDouble(Item::getPrice).sum();
141 }
142
143 public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
144 this.paymentStrategy = paymentStrategy;
145 System.out.println("Payment method set to: " + paymentStrategy.getPaymentMethod());
146 }
147
148 public boolean checkout() {
149 if (items.isEmpty()) {
150 System.out.println("Cart is empty!");
151 return false;
152 }
153
154 if (paymentStrategy == null) {
155 System.out.println("Please select a payment method!");
156 return false;
157 }
158
159 double total = getTotalAmount();
160 System.out.println("\n=== Checkout ===");
161 System.out.println("Items in cart: " + items.size());
162 items.forEach(item -> System.out.println(" " + item.getName() + " - $" + item.getPrice()));
163 System.out.println("Total amount: $" + total);
164 System.out.println();
165
166 boolean paymentSuccess = paymentStrategy.pay(total);
167
168 if (paymentSuccess) {
169 System.out.println("Order completed successfully!");
170 items.clear();
171 } else {
172 System.out.println("Order failed. Please try again.");
173 }
174
175 return paymentSuccess;
176 }
177
178 public void displayCart() {
179 System.out.println("\n=== Shopping Cart ===");
180 if (items.isEmpty()) {
181 System.out.println("Cart is empty");
182 } else {
183 items.forEach(item -> System.out.println(item.getName() + " - $" + item.getPrice()));
184 System.out.println("Total: $" + getTotalAmount());
185 }
186 System.out.println();
187 }
188
189 // Item class
190 public static class Item {
191 private final String name;
192 private final double price;
193
194 public Item(String name, double price) {
195 this.name = name;
196 this.price = price;
197 }
198
199 public String getName() { return name; }
200 public double getPrice() { return price; }
201
202 @Override
203 public boolean equals(Object o) {
204 if (this == o) return true;
205 if (o == null || getClass() != o.getClass()) return false;
206 Item item = (Item) o;
207 return Double.compare(item.price, price) == 0 && Objects.equals(name, item.name);
208 }
209
210 @Override
211 public int hashCode() {
212 return Objects.hash(name, price);
213 }
214 }
215}
216
217// Usage example
218public class StrategyPatternExample {
219 public static void main(String[] args) {
220 ShoppingCart cart = new ShoppingCart();
221
222 // Add items to cart
223 cart.addItem(new ShoppingCart.Item("Laptop", 999.99));
224 cart.addItem(new ShoppingCart.Item("Mouse", 29.99));
225 cart.addItem(new ShoppingCart.Item("Keyboard", 79.99));
226
227 cart.displayCart();
228
229 // Try checkout without payment method
230 cart.checkout();
231
232 // Set payment strategy and checkout
233 System.out.println("\n--- Trying Credit Card Payment ---");
234 cart.setPaymentStrategy(new CreditCardPayment("1234567890123456", "12/25", "123"));
235 cart.checkout();
236
237 // Add more items and try different payment method
238 cart.addItem(new ShoppingCart.Item("Monitor", 299.99));
239 cart.addItem(new ShoppingCart.Item("Speakers", 149.99));
240
241 System.out.println("\n--- Trying PayPal Payment ---");
242 cart.setPaymentStrategy(new PayPalPayment("user@example.com", "password"));
243 cart.checkout();
244
245 // Add items and try bank transfer
246 cart.addItem(new ShoppingCart.Item("Webcam", 89.99));
247
248 System.out.println("\n--- Trying Bank Transfer Payment ---");
249 cart.setPaymentStrategy(new BankTransferPayment("9876543210", "123456789"));
250 cart.checkout();
251
252 cart.displayCart();
253 }
254}
๐ Pattern Comparison and Selection Guide
๐ฏ When to Use Each Pattern
graph TD
A[Design Pattern Selection] --> B{Problem Type?}
B -->|Object Creation| C[Creational Patterns]
B -->|Object Composition| D[Structural Patterns]
B -->|Object Interaction| E[Behavioral Patterns]
C --> F[Singleton - Single instance needed]
C --> G[Factory - Multiple object types]
C --> H[Builder - Complex object construction]
D --> I[Decorator - Add behavior dynamically]
D --> J[Adapter - Interface compatibility]
E --> K[Observer - State change notifications]
E --> L[Strategy - Algorithm selection]
style C fill:#ff6b6b
style D fill:#4ecdc4
style E fill:#feca57
Pattern | Complexity | Use Cases | Performance Impact |
---|---|---|---|
Singleton | Low | Configuration, Logging, Database connections | Minimal |
Factory | Medium | Object creation abstraction, Plugin systems | Low |
Builder | Medium | Complex object construction, Immutable objects | Low |
Decorator | Medium | Feature addition, Middleware systems | Low-Medium |
Adapter | Low | Legacy system integration, API compatibility | Minimal |
Observer | Medium | Event systems, MVC architectures | Medium |
Strategy | Medium | Algorithm selection, Payment processing | Low |
๐ฏ Conclusion
Design patterns are essential tools in a Java developer’s toolkit, providing proven solutions to common design problems. Each pattern serves specific purposes and offers unique advantages:
Key Takeaways:
- Creational Patterns help manage object creation complexity
- Structural Patterns organize object relationships and composition
- Behavioral Patterns define communication and responsibility distribution
Best Practices:
- Don’t overuse patterns - Apply them when they solve real problems
- Understand the trade-offs - Each pattern has advantages and disadvantages
- Consider performance implications - Some patterns add overhead
- Maintain code readability - Patterns should improve, not complicate code
- Test thoroughly - Complex patterns require comprehensive testing
Master these fundamental patterns and you’ll be well-equipped to design robust, maintainable, and scalable Java applications.