We have already reviewed use of a singleton object, but you may not yet realize that this strategy is a design pattern, and one of the most used at that.

In fact, there are a lot of these patterns, and they can be classified according to their specific purpose.

Pattern classification

Pattern type Application
Creational A type that solves the object creation problem
Structural Patterns that let us build a correct and extensible class hierarchy in our architecture
Behavioral This cluster of patterns facilitates safe and convenient interaction between objects in a program.

Typically, a pattern is characterized by the problem it solves. Let's take a look at a few patterns that we encounter most often when working with Java:

Pattern Purpose
Singleton We are already familiar with this pattern — we use it to create and access a class that cannot have more than one instance.
Iterator We are also familiar with this one. We know that this pattern lets us iterate over a collection object without revealing its internal representation. It is used with collections.
Adapter This pattern connects incompatible objects so that they can work together. I think the name of the adapter pattern helps you imagine exactly what this it does. Here's a simple example from real life: a USB adapter for a wall outlet.
Template method

A behavioral programming pattern that solves the integration problem and allows you to change algorithmic steps without changing the structure of an algorithm.

Imagine that we have a car assembly algorithm in the form of an sequence of assembly steps:

Chassis -> Body -> Engine -> Cabin Interior

If we put in a reinforced frame, a more powerful engine, or an interior with additional lighting, we don't have to change the algorithm, and the abstract sequence remains the same.

Decorator This pattern creates wrappers for objects to give them useful functionality. We will consider it as part of this article.

In Java.io, the following classes implement patterns:

Decorator pattern

Let's imagine that we are describing a model for a home design.

In general, the approach looks like this:

Initially, we have a choice of several types of houses. The minimum configuration is one floor with a roof. Then we use all sorts of decorators to change additional parameters, which naturally affects the price of the house.

We create an abstract House class:


public abstract class House {
	String info;
 
	public String getInfo() {
    	return info;
	}
 
	public abstract int getPrice();
}
    

Here we have 2 methods:

  • getInfo() returns information about the name and features of our house;
  • getPrice() returns the price of the current house configuration.

We also have standard House implementations — brick and wooden:


public class BrickHouse extends House {
 
	public BrickHouse() {
    	info = "Brick House";
	}
 
	@Override
	public int getPrice() {
    	return 20_000;
	}
}
 
public class WoodenHouse extends House {
 
	public WoodenHouse() {
    	info = "Wooden House";
	}
 
	@Override
	public int getPrice() {
    	return 25_000;
	}
}
    

Both classes inherit the House class and override its price method, setting a custom price for a standard house. We set the name in the constructor.

Next, we need to write decorator classes. These classes will also inherit the House class. To do this, we create an abstract decorator class.

That's where we'll put additional logic for changing an object. Initially, there will be no additional logic and the abstract class will be empty.


abstract class HouseDecorator extends House {
}
    

Next, we create decorator implementations. We will create several classes that let us add additional features to the house:


public class SecondFloor extends HouseDecorator {
	House house;
 
	public SecondFloor(House house) {
    	this.house = house;
	}
 
	@Override
	public int getPrice() {
    	return house.getPrice() + 20_000;
	}
 
	@Override
	public String getInfo() {
    	return house.getInfo() + " + second floor";
	}
}
    
A decorator that adds a second floor to our house

The decorator constructor accepts a house that we will "decorate", i.e. add modifications. And we override the getPrice() and getInfo() methods, returning information about the new updated house based on the old one.


public class Garage extends HouseDecorator {
 
	House house;
	public Garage(House house) {
    	this.house = house;
	}
 
	@Override
	public int getPrice() {
    	return house.getPrice() + 5_000;
	}
 
	@Override
	public String getInfo() {
    	return house.getInfo() + " + garage";
	}
}
    
A decorator that adds a garage to our house

Now we can update our house with decorators. To do this, we need to create a house:


House brickHouse = new BrickHouse();
    

Next, we set our house variable equal to a new decorator, passing in our house:


brickHouse = new SecondFloor(brickHouse); 
    

Our house variable is now a house with a second floor.

Let's look at use cases involving decorators:

Example code Output

House brickHouse = new BrickHouse(); 

  System.out.println(brickHouse.getInfo());
  System.out.println(brickHouse.getPrice());
                    

Brick House

20000


House brickHouse = new BrickHouse(); 

  brickHouse = new SecondFloor(brickHouse); 

  System.out.println(brickHouse.getInfo());
  System.out.println(brickHouse.getPrice());
                    

Brick House + second floor

40000


House brickHouse = new BrickHouse();
 

  brickHouse = new SecondFloor(brickHouse);
  brickHouse = new Garage(brickHouse);

  System.out.println(brickHouse.getInfo());
  System.out.println(brickHouse.getPrice());
                    

Brick House + second floor + garage

45000


House woodenHouse = new SecondFloor(new Garage(new WoodenHouse())); 

  System.out.println(woodenHouse.getInfo());
  System.out.println(woodenHouse.getPrice());
                    

Wooden House + garage + second floor

50000


House woodenHouse = new WoodenHouse(); 

  House woodenHouseWithGarage = new Garage(woodenHouse);

  System.out.println(woodenHouse.getInfo());
  System.out.println(woodenHouse.getPrice());

  System.out.println(woodenHouseWithGarage.getInfo());
  System.out.println(woodenHouseWithGarage.getPrice());
                    

Wooden House

25000

Wooden House + garage

30000

This example illustrates the benefit of upgrading an object with a decorator. So we didn't change the woodenHouse object itself, but instead created a new object based on the old one. Here we can see that the advantages come with disadvantages: we create a new object in memory each time, increasing memory consumption.

Look at this UML diagram of our program:

A decorator has a super simple implementation and dynamically changes objects, upgrading them. Decorators can be recognized by their constructors, which take as parameters objects of the same abstract type or interface as the current class. In Java, this pattern is widely used in I/O classes.

For example, as we already noted, all subclasses of java.io.InputStream, OutputStream, Reader and Writer have a constructor that accepts objects of the same classes.