Structural Design Patterns

Structural Design Patterns

2020-03-15T13:46:37.681Z

Sources:

Composite

Have an element and a container share functionality.

Think of a group of shapes. You can have both the shape and the group implement an interface with the common method move(), so that you can move each shape or each group of shapes. The method can be called on both the element and on the container, so the container is treated as just another object in the hierarchy.

Create two classes: an element called Leaf and an container called Composite for containing leaves and composites. Create an interface called Component with an operation that is common among leaves and composites.

Component.java
public interface Component {
 	void move();
}
Shape.java
// Leaf
public class Shape implements Component {
	@Override
	public void move() {
		System.out.println("Moving shape...");
	}
Group.java
// Composite
public class Group implements Component {
	private List<Component> components = new ArrayList<>();

	public void add(Component component) {
		components.add(component)
	}

	public void move() {
		for (var component : components)
			components.move();
	}

	@Override
	public void move() {
		System.out.println("Moving group...");
	}
}

Adapter

Make an object compatible with another.

Think of an image renderer that applies filters to images, as long as the filter implements the Filter interface. You can create your own filters that implement that interface and use your own filters with the image renderer, but what if you want to use a third-party filter that does not implement the Filter interface?

Convert the interface of an object for use by another object.

Filter.java
public interface Filter {
	void apply(Object image) {
		System.out.println("Applying filter to image...");
	}
}
VividFilter.java
public class BlackAndWhiteFilter implements Filter {
	@Override
	public void apply(Object image) {
		System.out.println("Applying B&W filter to image...");
	}
}
ImageRenderer.java
// Class where filter is used
public class ImageRenderer {
	private Image image;

	public ImageRenderer(Image image) {
		this.image = image;
	}

	public void useFilter(Filter filter) {
		filter.apply(image);
	}
}
CaramelLayer.java
// Adaptee (class not implementing interface)
public class CaramelLayer {
  // `CaramelLayer` does not have the `apply` required in the interface.
	public void put(Image image) {
		System.out.println("Putting Caramel layer on image...");
	}
}
CaramelFilter.java
// Adapter (wrapper for class not implementing interface)
public class CaramelFilter implements Filter {
	private CaramelLayer caramelLayer;

	public CaramelFilter(CaramelLayer caramelLayer) {
		this.caramelLayer = caramelLayer;
	}

	@Override
	public void apply(Image image) { // adapting the third-party API
		caramelLayer.put(image);
	}
}

title=Main.java

public class Main {
  public static void main(String[] args) {
    var image = new Image(); // imagine implementation
    var imageRenderer = new ImageRenderer(image);
    var adaptedCaramel = new CaramelFilter(new CaramelLayer());
    imageRenderer.useFilter(adapterCaramel);
  }
}

Or using inheritance:

CaramelFilter.java
// Adapter (wrapper for class not implementing interface)
public class CaramelFilter2 extends CaramelLayer implements Filter {
	@Override
	public void apply(Image image) {
		put(image); // inherited method
	}
}

Decorator

Add functionality to an object through a wrapper.

The intent is to add behavior to ("decorate") an object by placing it inside a wrapper that has the additional behavior.

Think of a cloud storage service that, besides storing data in the cloud, offers additional functionality like pre-storage encryption and pre-storage compression. To do this, you might create two subclasses EncryptedStorage and CompressedStorage, overriding the parent's store() method in each with a new method. But what if you need to override the parent's storage() method with both functions? You would need to create a subclass that is the combination of both subclasses and override the parent's storage() method there. Combining subclasses is not maintainable.

To fix this, favor composition over inheritance. Create:

  • an interface with an operation in common among the base servce and specialized services,
  • a class for the base service that implements that interface (wrappee), and
  • classes for specialized services implementing that interface and constructed with the base service (wrapper).

Finally, to use specialized version, create the specialised version passing in the base service and call the common operation.

The Decorator and the Adapter pattern differ in that the Adapter changes a class to conform to an interface, whereas the Decorator changes a class to add behavior to it. Both patterns rely on composition.

Stream.java
public interface Stream {
  	void write(String data);
}
CloudStream.java
// Base version of service, to be wrapped when necessary
public class CloudStream implements Stream {
	public void store(String data) {
		System.out.println("Storing:" + data);
	}
}
CompressedCloudStream.java
// Decorator
// Specialized version, to wrap base version when necessary
public class CompressedCloudStream implements Stream {
	private stream;

	public CompressedCloudStream(Stream stream) {
		this.stream = stream;
		// when creating this specialized version, pass in the base version
	}

	@Override
	public void write(String data) {
		var compressed = compress(data); // additional behavior
		stream.write(data); // parent's functionality
	}

	public void compress(String data) {
		return "compressed " + data;
	}
}
EncryptedCloudStream.java
// Decorator
// Specialized version, to wrap base version when necessary
public class EncryptedCloudStream implements Stream {
	private stream;

	public EncryptedCloudStream(Stream stream) {
		this.stream = stream;
		// when creating this specialized version, pass in the base version
	}

	@Override
	public void write(String data) {
		var compressed = compress(data); // additional behavior
		stream.write(data); // parent's functionality
	}

	public void compress(String data) {
		return "encrypted " + data;
	}
}
Main.java
public class Main {
	public static void main(String[] args) {
		var baseVersion = new CloudStream();
		baseVersion.write("hello"); // "hello"

		var compressedVersion = new CompressedCloudStream(baseVersion);
		compressedVersion.write("hello"); // "compressed hello"

		var encryptedVersion = new EncryptedCloudStream(baseVersion);
		encryptedVersion.write("hello"); // "encrypted hello"

		var fullVersion = new EncryptedCloudStream(
			new CompressedCloudStream(
				new CloudStream("hello")));
		fullVersion.write("hello"); // "encrypted compressed hello"
	}
}

Facade

Make a simple interface for a complex system.

Example of long, complex and highly coupled procedure for sending a message:

Main.java
public class Main { // directly talking to many classes
	public static void main(String[] args) {
		var server = new NotificationServer(); // NotificationServer class
		var connection = server.connect("ip"); // Connection class
		var authToken = server.authenticate("appID", "key"); // AuthToken class
		var message = new Message("hello"); // Message class
		server.send(authToken, message, "john");
		connection.disconnect();
	}
}

Example of facade for hiding away procedure:

NotificationService.java
// Facade talking to many other classes
public class NotificationService {
	send(String messageText, String target) {
		var server = new NotificationServer();
		var connection = server.connect("ip");
		var authToken = server.authenticate("appID", "key");
		server.send(authToken, new Message(messageText), target);
		connection.disconnect();
	}
}
Main.java
public class Main {
	public static void main(String[] args) {
		var notificationService = new NotificationService();
		notificationService.send("hello", "john");
	}
}

Flyweight

Store a shared object in a single place in memory.

State in an object may be instrinsic (visible and modifiable only inside the object) or extrinsic (read from outside the object and modified outside the object). If you store extrinsic state inside the object, you are making an unnecessary copy of it. The Flyweight pattern dictates that we should not make a copy of extrinsic state. Objects should only differ in their intrinsic state, which is unique to each object, and so only intrinsic state should be stored inside the object. The intent is to reduce memory usage. The flyweight is the object that we can share.

To store a shared object in a single place in memory, separate the tata to be shared, encapsulate it in an object called flyweight and create a factory for caching many flyweights.

Point.java
// Main element (point)
public class Point {
	private int x;
	private int y;
	private PointIcon icon;

	public Point(int x, int y, PointIcon icon) {
		this.x = x;
		this.y = y;
		this.icon = icon;
	}

	public void draw() {
		System.out.printf("%s at (%d, %d)", icon.getType(), x, y);
	}
}
PointType.java
// Simple enum for point types
enum PointType {
	CAFE,
	HOSPITAL,
	SCHOOL
}
PointIcon.java
// Flyweight, piece of main element (icon of point)
public class PointIcon {
	private final PointType type;
	private final byte[] icon;

	public PointIcon(PointType type, byte[] icon) {
		this.type = type;
		this.icon = icon;
	}

	public PointType getType() {
		return type;
	}
}
PointIconFactory.java
// Cache of flyweights
// This factory creates PointIcon objects, only one of each!
// Each PointIcon object is stored only once, in a single place in memory.
// To ensure this, the factory uses a cache implemented as a HashMap.
public class PointIconFactory {
	// Cache implemented as HashMap, mapping PointType to PointIcon.
	private Map<PointType, PointIcon> icons = new HashMap<>();

	// Method for building a PointIcon from a type  and returning the PointIcon.
	public PointICon getPointIcon(PointType type) {
		// Before returning a PointIcon, hit the cache first.
		if (!icons.containsKey(type)) {
		var icon = new PointIcon(type, null); // `null` here stands for icon file
		icons.put(type, icon);
		}

		return icons.get(type);
	}
}
PointService.java
// Organizer of elements (points)
public class PointService {
	private PointIconFactory pointIconFactory;

	PointService(PointIconFactory pointIconFactory) {
		this.pointIconFactory = PointIconFactory pointIconFactory;
	}

	public List<Point> getPoints() {
		List<Point> points = new ArrayList<>();
		var point = new Point(1, 2, pointIconFactory.getPointIcon(PointType.CAFE));
		points.add(point);

		return points;
	}
}
Main.java
public class Main {
	public static void main(String[] args) {
		var service = new PointService(new PointIconFactory());
		for (var point : service.getPoints())
			point.draw();
	}
}

Bridge

Split and connect two hierarchies.

Think of two dimensions: shape (implemented as a class) and color (implemented as a subclass). If you have two shapes, and you want to have those shapes in two colors, you generate four subclasses, two colors for each shape. Adding colors would cause an unsustainable increase in the number of subclasses.

To fix this, favor composition over inheritance, i.e. make one of the dimensions a separate class having its own subclasses, and give the main dimension a reference field pointing to the new class. The reference field is the bridge, which connects two independent hierarchies. Now adding colors does not require changing the shape hierarchy, and vice versa!

This example of colors and shapes as separate dimensions serves to clarify the difference between the abstraction hierarchy (e.g. GUI layer of an app) and implementation hierarchy (e.g. OS API of an app). The abstraction GUI layer delegates work to the implementation API layer via an interfaced reference. The implementation API layer, as a separate hierarchy, can have subclasses for each of the major operating systems.

When you intend to combine two conceptual hierarchies, create an interface like Device. Then create a class, no matter how general or specific, like SonyTV, that implements that interface. Finally, create a class hierarchy that also implements that interface, like RemoteControl, add a field pointing to an object of the type interface that you created, and add methods that delegate work to that referenced object.

Now, when creating an object in the class hierarchy, you can pass in any object complying with the interface, no matter how general or specific, and so you can control that object from inside the class hierarchy. If in the future you need to add a new object for use in the class hierarchy, just add an object that implements the interface.

Key takeaway: Use the Bridge pattern when you have a structure that can grow in two different dimensions. You:

  1. split them, creating a single class hierarchy and turing the other hierarchy into multiple implementations of an interface, and
  2. connect them, adding to the base class of the hierarchy a field pointing to an interface object and delegating work to the interfaced object.
Device.java
public interface Device {
	void turnOn();
	void turnOff();
	void setChannel(int number);
}
SonyTV.java
public class SonyTV implements Device {
  	@Override
	public void turnOn() {
		System.out.println("Sony: Turned on");
	}

	@Override
	public void turnOff() {
		System.out.println("Sony: Turned off");
	}

	@Override
	public void setChannel(int number) {
		System.out.println("Sony: Setting channel to " + number);
	}
}
RemoteControl.java
// Hierarchy 2
public class RemoteControl {
	protected Device device;
	// This field is the bridge between `RemoteControl` and any object passed in
	// that complies with the `Device` interface. If you pass in a `SonyTV` object,
	// the `RemoteControl` object will delegate to the `turnOn` method in that
	// `SonyTV object`.

	RemoteControl(device) {
		this.device = device;
	}

	public void turnOn() {
		device.turnOn(); // delegation
	}

	public void turnOff() {
		device.turnOff(); // delegation
	}
}
AdvancedRemoteControl.java
public class AdvancedRemoteControl extends RemoteControl {
	public AdvancedRemoteControl(Device device) {
		super(device);
	}

	public void setChannel(int number) {
		device.setChannel(number); // delegate to device
	}
}
Main.java
public class Main {
	public static void main(String[] args) {
		var remoteControl = new RemoteControl(new SonyTV());
		remoteControl.turnOn(); // "SonyTV: Turned on"

		var advancedRemoteControl = new AdvancedRemoteControl(new SonyTV());
		advancedRemoteControl.setChannel(2); // "Sony: Setting channel to 2"
	}
}

Proxy

Intercept a call to an object to add functionality.

A proxy is an intermediary that sits between a primary class with costly business logic or real subject and its calling class or client. Whenever a call is made, the proxy intercepts the call and adds functionality right before or after.

Think of an EbookLibrary object and an Ebook object. Whenever an e-reader is turned on, the EbookLibrary object creates all the Ebook objects by reading the data from disk and loads it all into memory, but this is costly in terms of memory. A proxy fixes this by letting you read and load a book on demand, only when you need it. This is called lazy initialization or lazy loading.

Create an interface that declares common methods shared by the real subject and the proxy. Whenever the client calls the costly operation, we pass in the proxy (since it complies with that interface) and add functionality—in this case, lazy initialization, but it could also be logging or authentication.

Ebook.java
public interface Ebook {
	void show();
}
RealEbook.java
// Real subject
public class RealEbook implements Ebook {
	private String filename;

	public RealEbook(String filename) {
		this.filename = filename;
		load();
	}

	private void load() {
		System.out.println("Loading ebook: " + filename);
	}

	@Override
		public void show() {
		System.out.println("Showing ebook: " + filename);
	}
}
EbookProxy.java
// Proxy
public class EbookProxy implements Ebook {
	private String filename;
	private RealEbook ebook;
	// The latter is reference to RealEbook to forward calls to it.

	public EbookProxy(String filename) {
		this.filename = filename;
	// no initialization of reference here
	}

	@Override
	public void show() {
	// proxy addition: initialization of reference here, not in constructor!
		if (ebook == null)
			ebook = new RealEbook(filename);

		ebook.show() // forward call to RealEbook
	}
}
Main.java
public class Main {
	public static void main(String[] args) {
		var library = new Library();
		String[] filenames = { "a", "b", "c" };
		for (var filename : filenames)
			library.add(new ProxyEbook(filename));

		library.openEbook("a");
		// In the console:
		// "Loading ebook: a"
		// "Showing ebook: a"

		// `openEbook()` triggers the `show()` method in the added `ProxyEbook`.
		// This call to the `show()` method now initializes the reference.
		// Result: Loading was deferred until opening. Benefit: No need to
		// pre-load all, only one at opening.
	}
}

Or a logging proxy:

LoggingEbookProxy.java
// New proxy
public class LoggingEbookProxy implements Ebook {
  	private String filename;
	private RealEbook ebook;

	public LoggingEbookProxy() {
		this.filename = filename;
	}

	public void show() {
		if (ebook == null)
		ebook = new RealEbook(filename);

		System.out.println("Logging that " + filename + "was shown.");
		// logging after loading but before showing!

		ebook.show():
	}
}
Main.java
public class Main {
	public static void main(String[] args) {
		var library = new Library();
		String[] filenames = { "a", "b", "c" };
		for (var filename : filenames)
			library.add(new LoggingProxyEbook(filename));

		library.openEbook("a");
		// In the console:
		// "Loading ebook: a"
		// "Logging that a was shown."
		// "Showing ebook: a"

		// `openEbook()` triggers the `show()` method in the added `ProxyEbook`.
		// This call to the `show()` method now initializes the reference.
		// Result: Loading was deferred until opening. Benefit: No need to pre-load
		// all, only one at opening.
	}
}