|
| 1 | +--- |
| 2 | +title: Compound Pattern |
| 3 | +created: 2026-02-10 |
| 4 | +tags: |
| 5 | + - behavioral |
| 6 | + - creational |
| 7 | + - structural |
| 8 | +--- |
| 9 | +## Definition |
| 10 | + |
| 11 | +Compound Patterns combine two or more patterns into a solution that solves a recurring or general problem. |
| 12 | + |
| 13 | +--- |
| 14 | +## Real World Analogy |
| 15 | + |
| 16 | +**Compound Patterns** combine two or more patterns to solve a problem. Design patterns are created to solve problems, but they are not limited to using only one pattern at a time. While implementing your own solution, you may notice that multiple patterns are working together. When different patterns are combined to solve a single problem, it is called a **Compound Pattern**. In the next section, we will see the best practices for applying design patterns. |
| 17 | + |
| 18 | +For this analogy, let’s take a simple example of a **Logger**, which is widely used in different applications. In real systems, loggers do not rely on a single design pattern. Instead, multiple design patterns are used to reuse code and keep the implementation flexible. In a real implementation of a logger, you can configure where the logs are written, such as the console, a file, or a database. You can also format the logs as needed without changing the internal implementation. We are going to implement a similar idea here. |
| 19 | + |
| 20 | +[[#Real World Analogy - 2 (MVC)|Next Section]] explains how MVC patterns are used and why they are called a **Compound Pattern**. |
| 21 | + |
| 22 | +In our implementation, the Logger will contain four design patterns: |
| 23 | +- [[Strategy Pattern]]: Used for selecting the type of logger, such as `Console`, `File`, or `Db`. |
| 24 | +- [[Iterator Pattern]]: Used to go through all added loggers and write logs to each one. |
| 25 | +- [[Factory Method Pattern]]: Used for object creation, where initialization takes place. |
| 26 | +- [[Composite Pattern]]: A logger can contain multiple types of loggers. |
| 27 | + |
| 28 | +--- |
| 29 | +## Design |
| 30 | + |
| 31 | +To implement this, we first define an `Interface` called `Logger`, which is responsible for logging. It does not matter what type of logger it is; it will log the message for the loggers added in the `LoggerCollection`. |
| 32 | + |
| 33 | +The following class diagram shows how different classes are related and how multiple patterns are combined. |
| 34 | + |
| 35 | +```mermaid |
| 36 | +classDiagram |
| 37 | +
|
| 38 | +class Logger { |
| 39 | + <<interface>> |
| 40 | + +loginfo(String) |
| 41 | + +logerror(String) |
| 42 | + +logwarning(String) |
| 43 | + +logdebug(String) |
| 44 | +} |
| 45 | +
|
| 46 | +class BaseLogger { |
| 47 | + <<abstract>> |
| 48 | + -LoggerCollection loggercollection |
| 49 | + +addLogger(BaseLogger) |
| 50 | + +log(LogLevel, String) |
| 51 | + +write(String, LogLevel)* |
| 52 | +} |
| 53 | +
|
| 54 | +class ConsoleLogger { |
| 55 | + +write(String, LogLevel) |
| 56 | +} |
| 57 | +
|
| 58 | +class FileLogger { |
| 59 | + +write(String, LogLevel) |
| 60 | +} |
| 61 | +
|
| 62 | +class ApplicationLogger { |
| 63 | + +write(String, LogLevel) |
| 64 | +} |
| 65 | +
|
| 66 | +class LoggerCollection { |
| 67 | + -ArrayList~BaseLogger~ loggers |
| 68 | + +addLogger(BaseLogger) |
| 69 | + +iterator() |
| 70 | +} |
| 71 | +
|
| 72 | +class LoggerFactory { |
| 73 | + -Logger logger |
| 74 | + +getLogger() |
| 75 | +} |
| 76 | +
|
| 77 | +Logger <|.. BaseLogger |
| 78 | +BaseLogger <|-- ConsoleLogger |
| 79 | +BaseLogger <|-- FileLogger |
| 80 | +BaseLogger <|-- ApplicationLogger |
| 81 | +
|
| 82 | +BaseLogger --> LoggerCollection : contains |
| 83 | +LoggerCollection --> BaseLogger : iterates |
| 84 | +
|
| 85 | +LoggerFactory --> Logger : returns |
| 86 | +LoggerFactory --> ApplicationLogger : creates |
| 87 | +``` |
| 88 | +_Class diagram for the Logger as a Compound Pattern_ |
| 89 | + |
| 90 | +--- |
| 91 | +## Implementation in Java |
| 92 | + |
| 93 | +```java title="LogLevel.java" |
| 94 | +enum LogLevel { |
| 95 | + INFO, |
| 96 | + WARNING, |
| 97 | + DEBUG, |
| 98 | + ERROR |
| 99 | +} |
| 100 | +``` |
| 101 | +This enum defines different log levels. Each log message is tagged with a level so that logs can be filtered or formatted based on their severity. |
| 102 | + |
| 103 | +```java title="Logger.java" |
| 104 | +interface Logger { |
| 105 | + void loginfo(String message); |
| 106 | + void logerror(String message); |
| 107 | + void logwarning(String message); |
| 108 | + void logdebug(String message); |
| 109 | +} |
| 110 | +``` |
| 111 | +This interface defines the common operations that every logger must implement. Any class implementing this interface can log messages of different levels. |
| 112 | + |
| 113 | +```java title="LoggerCollection.java" |
| 114 | +class LoggerCollection implements Iterable<BaseLogger> { |
| 115 | + private final ArrayList<BaseLogger> loggers = new ArrayList<>(); |
| 116 | + |
| 117 | + public void addLogger(BaseLogger logger) { |
| 118 | + this.loggers.add(logger); |
| 119 | + } |
| 120 | + |
| 121 | + @Override |
| 122 | + public Iterator<BaseLogger> iterator() { |
| 123 | + return this.loggers.iterator(); |
| 124 | + } |
| 125 | +} |
| 126 | +``` |
| 127 | +This class stores multiple logger objects. It also implements `Iterable`, which allows us to loop through all loggers one by one. This helps in writing the same log message to multiple loggers. |
| 128 | + |
| 129 | +```java title="BaseLogger.java" |
| 130 | +abstract class BaseLogger implements Logger { |
| 131 | + public abstract void write(String message, LogLevel level); |
| 132 | + |
| 133 | + protected LoggerCollection loggercollection = new LoggerCollection(); |
| 134 | + |
| 135 | + public void addLogger(BaseLogger logger) { |
| 136 | + this.loggercollection.addLogger(logger); |
| 137 | + } |
| 138 | + |
| 139 | + private void logMessage(String message, LogLevel level) { |
| 140 | + Iterator<BaseLogger> loggeriterator = this.loggercollection.iterator(); |
| 141 | + |
| 142 | + while (loggeriterator.hasNext()) { |
| 143 | + BaseLogger logger = loggeriterator.next(); |
| 144 | + logger.log(level, message); |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + public void log(LogLevel level, String message) { |
| 149 | + write(message, level); |
| 150 | + } |
| 151 | + |
| 152 | + @Override |
| 153 | + public void logdebug(String message) { |
| 154 | + logMessage(message, LogLevel.DEBUG); |
| 155 | + } |
| 156 | + |
| 157 | + @Override |
| 158 | + public void logerror(String message) { |
| 159 | + logMessage(message, LogLevel.ERROR); |
| 160 | + } |
| 161 | + |
| 162 | + @Override |
| 163 | + public void loginfo(String message) { |
| 164 | + logMessage(message, LogLevel.INFO); |
| 165 | + } |
| 166 | + |
| 167 | + @Override |
| 168 | + public void logwarning(String message) { |
| 169 | + logMessage(message, LogLevel.WARNING); |
| 170 | + } |
| 171 | +} |
| 172 | +``` |
| 173 | +This abstract class provides the base behavior for all loggers. It keeps a collection of child loggers and forwards log messages to each of them. The `write` method is left abstract so that each concrete logger can decide how to store or display the log. |
| 174 | + |
| 175 | +```java title="ConsoleLogger.java" |
| 176 | +class ConsoleLogger extends BaseLogger { |
| 177 | + |
| 178 | + @Override |
| 179 | + public void write(String message, LogLevel level) { |
| 180 | + System.out.printf("%s - %s%n", level.toString(), message); |
| 181 | + } |
| 182 | +} |
| 183 | +``` |
| 184 | +This class writes log messages to the console. It formats the log level and message and prints them to standard output. |
| 185 | + |
| 186 | +```java title="FileLogger.java" |
| 187 | +class FileLogger extends BaseLogger { |
| 188 | + |
| 189 | + @Override |
| 190 | + public void write(String message, LogLevel level) { |
| 191 | + try { |
| 192 | + FileWriter writer = new FileWriter("./app.log",true); |
| 193 | + writer.write("%s - %s\n".formatted(level.toString(), message |
| 194 | + )); |
| 195 | + writer.close(); |
| 196 | + } catch (IOException e) { |
| 197 | + throw new RuntimeException(e); |
| 198 | + } |
| 199 | + |
| 200 | + } |
| 201 | +} |
| 202 | +``` |
| 203 | +This class writes log messages to a file named `app.log`. Each time a log is written, the message is appended to the file. |
| 204 | + |
| 205 | +**Note:** Here we are hardcoding the log file name as `app.log`, but in a real implementation the user usually selects the file name and log format. For simplicity, these values are fixed in this example. |
| 206 | + |
| 207 | +```java title="ApplicationLogger.java" |
| 208 | +class ApplicationLogger extends BaseLogger { |
| 209 | + @Override |
| 210 | + public void write(String message, LogLevel level) { |
| 211 | + System.out.printf("App - %s - %s%n", level.toString(), message); |
| 212 | + } |
| 213 | +} |
| 214 | +``` |
| 215 | +This class represents the main or root logger. It adds a prefix so logs can be identified as application-level logs. |
| 216 | + |
| 217 | +```java title="LoggerFactory.java" |
| 218 | +class LoggerFactory { |
| 219 | + private static Logger logger; |
| 220 | + |
| 221 | + public static Logger getLogger() { |
| 222 | + if (logger == null) { |
| 223 | + BaseLogger rootlogger = new ApplicationLogger(); |
| 224 | + rootlogger.addLogger(new ConsoleLogger()); |
| 225 | + rootlogger.addLogger(new FileLogger()); |
| 226 | + |
| 227 | + logger = rootlogger; |
| 228 | + } |
| 229 | + return logger; |
| 230 | + } |
| 231 | +} |
| 232 | +``` |
| 233 | +This factory class creates and returns a single logger instance. It initializes the root logger and attaches console and file loggers to it. This centralizes object creation and avoids creating multiple instances unnecessarily. |
| 234 | + |
| 235 | +```java title="Logging.java" |
| 236 | +public static void main(String[] args) { |
| 237 | + Logger logger = LoggerFactory.getLogger(); |
| 238 | + |
| 239 | + logger.logdebug("Application Started"); |
| 240 | + logger.loginfo("Application Initiated"); |
| 241 | + logger.logerror("Application Failed"); |
| 242 | + logger.logwarning("You Cannot Start Application Like These"); |
| 243 | +} |
| 244 | +``` |
| 245 | +This is the entry point of the program. It gets the logger from the factory and writes log messages of different levels. |
| 246 | + |
| 247 | +**Output:** |
| 248 | +```txt |
| 249 | +DEBUG - Application Started |
| 250 | +INFO - Application Initiated |
| 251 | +ERROR - Application Failed |
| 252 | +WARNING - You Cannot Start Application Like These |
| 253 | +``` |
| 254 | +The same output is also written to the `app.log` file because both console and file loggers are attached to the main logger. |
| 255 | + |
| 256 | +--- |
| 257 | +## Real World Analogy - 2 (MVC) |
| 258 | + |
| 259 | +**MVC** stands for **Model View Controller**. These concepts are commonly used in web frameworks like **SpringBoot**, where responsibilities are separated into different layers. |
| 260 | + |
| 261 | +- **Model**: It holds all the data, state, and application logic. |
| 262 | +- **View**: It provides a representation of the model. |
| 263 | +- **Controller**: It takes user input and decides how it should affect the model. |
| 264 | + |
| 265 | +![[Pasted image 20260210071446.png]] |
| 266 | +_Model View Controller Architecture Diagram_ |
| 267 | + |
| 268 | +![[pattern_mvc.png]] |
| 269 | +Let’s understand how MVC internally uses a set of patterns (Compound Pattern). |
| 270 | + |
| 271 | +**Strategy:** The view and controller implement the Strategy Pattern. The view focuses only on visual representation and delegates decisions about behavior to the controller. This keeps the view loosely coupled from the model. |
| 272 | + |
| 273 | +**Composite:** The display is made up of nested components such as windows, panels, buttons, and labels. Each component can contain other components. Updating the top component automatically updates all child components. |
| 274 | + |
| 275 | +**Observer:** The model uses the Observer Pattern to notify interested objects when its state changes. This keeps the model independent from views and controllers and allows multiple views to use the same model. |
| 276 | + |
| 277 | +This is why MVC is also known as a **Compound Pattern.** |
| 278 | + |
| 279 | +--- |
| 280 | +## Real World Example |
| 281 | + |
| 282 | +As we implemented a logger system, similar approaches are used in real logging libraries where a logger can be configured with multiple outputs and formats. Internally, these libraries make use of several design patterns. **Examples:** `Log4j`, `Logback`. |
| 283 | + |
| 284 | +In UI frameworks, when you add components such as `JPanel` or `JFrame`, they contain multiple UI elements inside them, which is an example of the Composite Pattern. Event listeners attached to components such as `JButton` behave similarly to the Observer Pattern. |
| 285 | + |
| 286 | +Compound Patterns are widely used in many frameworks such as `SpringBoot`, `Angular`, and `Django`. |
| 287 | + |
| 288 | +--- |
| 289 | +## Design Principles: |
| 290 | + |
| 291 | +- **Encapsulate What Varies** - Identify the parts of the code that are going to change and encapsulate them into separate class just like the Strategy Pattern. |
| 292 | +- **Favor Composition Over Inheritance** - Instead of using inheritance on extending functionality, rather use composition by delegating behavior to other objects. |
| 293 | +- **Program to Interface not Implementations** - Write code that depends on Abstractions or Interfaces rather than Concrete Classes. |
| 294 | +- **Strive for Loosely coupled design between objects that interact** - When implementing a class, avoid tightly coupled classes. Instead, use loosely coupled objects by leveraging abstractions and interfaces. This approach ensures that the class does not heavily depend on other classes. |
| 295 | +- **Classes Should be Open for Extension But closed for Modification** - Design your classes so you can extend their behavior without altering their existing, stable code. |
| 296 | +- **Depend on Abstractions, Do not depend on concrete class** - Rely on interfaces or abstract types instead of concrete classes so you can swap implementations without altering client code. |
| 297 | +- **Talk Only To Your Friends** - An object may only call methods on itself, its direct components, parameters passed in, or objects it creates. |
| 298 | +- **Don't call us, we'll call you** - This means the framework controls the flow of execution, not the user’s code (Inversion of Control). |
| 299 | +- **A class should have only one reason to change** - This emphasizes the Single Responsibility Principle, ensuring each class focuses on just one functionality. |
0 commit comments