Edit Page

Lab 6: The Decorator Design Pattern

The goal of this lab is to use the decorator design pattern, a structural design pattern, to solve a real-world problem.

Design patterns are proven solutions to solve recurring design problems to design flexible and reusable object-oriented software; that is, objects that are easier to implement, change, test and reuse.

Structural ­pat­terns deal with the composition of classes or objects while keep­ing them extensible, flex­i­ble and efficient.

The decorator design pattern is one of the twenty-three well-known Gang of Four (GoF) design patterns. It is classified under the category of structural patterns as it deals with structuring objects in a way that supports adding additional features dynamically.

The decorator pattern provides provides us with a new mechanism for adding new behavior to an object at run-time. It composes objects recursively to allow adding additional responsibilities to an object dynamically in a more flexible and efficient manner. It provides a better alternative compared to subclassing for extending functionality.

In this lab, we will work on the problem of creating a comprehensive logging library and attempt to answer the following key questions behind the Decorator pattern:

  • How to add new responsibilities to objects without making any code changes to the underlying classes?
  • How can we extend the class’ run-time behavior without making modifications to the underlying class?

Video

Objectives

In this lab you will

  1. understand a real-world scenario and choose when to apply the appropriate design pattern.
  2. design and implement the decorator design pattern.
  3. write unit tests and apply Test-Driven Development (TDD).

Requirement and Tools

Getting Started

If your instructor is using GitHub classroom, you will need to accept the assignment using the link below, clone the template repository, and import it as a project into your IDE.

If your instructor is not using GitHub classroom, clone and import the template project at https://github.com/cpit252/lab-06 ↗.

Problem Statement

A developer is working on a comprehensive logging library, which allows writing log messages to multiple storage devices called Transports (e.g., Console, File, HTTP, Syslog, databases, etc.). Log message may be written in multiple formats (e.g., plaintext, JSON, CSV, YAML, HTML, Syslog, colorize, prettyPrint, etc.). These formats are common in logging libraries (e.g., log4j is a popular logging library that supports multiple logging formats).

The library is getting popular and users are always demanding adding new logging devices/transports (e.g., MySQL, Slack, FTP server, etc.) with custom formats:

  • Base logger with no formatter: BaseLogger
  • File logger with JSON formatter: FileJSONLogger
  • File logger with Colorized formatter: FileColorizedLogger
  • Console logger with YAML formatter: ConsoleYAMLLogger
  • HTTP logger with HTML formatter: HTTPHTMLLogger
  • HTTP logger with HTML and YAML formatter: HttpHtmlYamlLogger
  • File logger with JSON and YAML formatter: FileJsonYamlLogger
  • File logger with HTML and XML and JSON and YAML formatter: FileHtmlXmlJsonYamlLogger

The developer is rethinking his design as the current implementation is simply a maintenance nightmare! He needs to restructure his code in a way that allows adding additional behavior without making changes to the underlying classes. He realized the need to design the library with the following goals in mind:

  1. Add new responsibilities to objects without making any code changes to the underlying logging classes (Transport and Formatters classes).

  2. The design should support the open-closed principle: “Classes should be open for extension but closed for modification”.

In his attempt to improve the design of his library, he went through two attempts:

First Attempt: One subclass per a combination of Transport and Formatter

He added formatters (JSON, CSV, plaintext, YAML, prettyprint, Colorize, etc.) to transports (File, HTTP, Database, Console, etc.) using inheritance as follows:

First attempt using inheritance

Whoa, this is a class explosion! Without even implementing this, we can see that this looks really bad and is a nightmare to maintain. He has realized how bad this solution is.

  • Why do we make heavy use of inheritance?
  • What happens when logging levels (“info”, “warning”, “error”) changes to include (“debug”, “fatal”) or additional settings are added (e.g., appending log messages with a text)?
  • How can we create different combinations of objects with different mix-and-match responsibilities, without ending up with N subclasses for each combination?

This approach is indeed problematic as minor changes will require changes to related subclasses. He is now considering another approach.

Second Attempt: Transports handle Formatters

In the second attempt, he decided to use instance variables and inheritance in the superclass to keep track of formatters. He created an abstract class with all the required instance variables and used methods that return boolean values (hasCSVFormat, hasJSONFormat ,etc.)

Second attempt using inheritance

Whoa, this is much better!, Six classes in total! This looks like a great improvement and the best solution he can ever have! However, do all concrete Loggers (File, Console, Database, HTTP, FTP) need these formatters? Format may change. New formats are added, some are gone, layouts change and their internal details will also change. Also, how do you get the number of combination loggers for a Transport with boolean variables?

  • New formatter will force us to add new methods and alter the superclass.
  • We may have Transports with no formatters needed. Yet, they will inherit methods like hasHTMLHeader hasCSV, etc.

Enter the Decorator Pattern

He has seen that creating different combinations of objects for our logger and formatter objects with inheritance has not worked out very well. He got class explosions and complex designs. The second approach did not work out well as he added functionality to the base class that isn’t appropriate for some of the subclasses.

Given these failed two approaches in mind, he thought that this is indeed a good use case for the decorator pattern.

So, here’s what he should do instead: we’ll start with a logger and “decorate” it with the formatters at runtime. For example, if the databse logger wants to log messages in a database in two formats JSON and YAML, then we’ll:

  • Start with a database logger.
  • Decorate it with JSON
  • Decorate it with YAML

Third attempt using the decorator design pattern

  • We start with an abstract type BaseLogger.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    public abstract class BaseLogger {
    	String label = "Unknown label";
      
    	public String getLabel() {
    		return label;
    	}
     
    	public abstract String getLevel();
    }
  • Concrete Loggers should implement the abstract type BaseLogger.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    public class ConsoleLogger extends BaseLogger {
    	public ConsoleLogger() {
    		label = "Console logger";
    	}
     
    	public String getLevel() {
    		return "info";
    	}
    }
  • Then, the decorator Formatter class should also implement the abstract type BaseLogger.
    1
    2
    3
    4
    5
    
    public abstract class FormatDecorator extends BaseLogger {
    	BaseLogger logger;
    	// All format decorators have to reimplement the getLabel method
    	public abstract String getLabel();
    }
  • We pass BaseLogger to Formatter’s constructor and then pass Formatter to the Logger’s client.
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    public class CSVFormatter extends FormatDecorator {
    	public CSVFormatter(BaseLogger logger) {
    		this.logger = logger;
    	}
    
    	public String getLabel() {
    		return logger.getLabel() + ", CSVFormatter";
    	}
    
    	public String getLevel() {
    		return "Info " + logger.getLevel();
    	}
    }
  • The client thinks it’s talking to Logger but it’s actually talking to Formatter. Formatter’s methods can provide additional behavior to BaseLogger’s methods. Clients of the library should be able to combine formatters as listed below:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    public class LoggerDemo {
     
    	public static void main(String args[]) {
    		BaseLogger logger = new FileLogger();
    		System.out.println(logger.getLabel() 
    				+ ". Level: " + logger.getLevel());
    
    		// create a console logger
    		BaseLogger logger2 = new ConsoleLogger();
    		// decorate it with a CSV and HTML formatters
    		logger2 = new CSVFormatter(logger2);
    		logger2 = new HTMLFormatter(logger2);
    		System.out.println(logger2.getLabel() 
    				+ ". Level: " + logger2.getLevel());
    
    		// create a file logger
            BaseLogger logger3 = new FileLogger();
    		// decorate it with a JSON, CSV, and YAML formatters
    		logger3 = new JSONFormatter(logger3);
    		logger3 = new CSVFormatter(logger3);
    		logger3 = new YAMLFormatter(logger3);
    		System.out.println(logger3.getLabel() 
    				+ ". Level: " + logger3.getLevel());
    	}
    }

Now, this will enable us to create different combinations of objects with different mix-and-match responsibilities, without ending up with N subclasses for each combination and without adding inappropriate responsibilities to concrete implementations.

Questions:

  1. Complete the current implementation using the decorator design pattern as shown in the last UML diagram.
  2. Explain how the decorator design pattern is a better alternative to the previous two attempts.

Deliverables and Submission

Extra Task [Optional]

If you are done with this activity, you may enable a continuos integration tool such as CircleCI ↗ to automatically run your JUnit test upon code changes. You may also add more unit tests to increase code coverage. Please embed the badge that shows the status of your build and test (passing/failing) as well as the coverage percentage into your README file (e.g., passing status image and failing status image). Please be sure to fork the repository or push to a remote repository under your own account, so you can enable the integration of CI tools in your own account.

You may refer to the lecture notes from the prerequisite course CPIT-251 on Continuous Integration (CI) and adding a code coverage badge.