Design Patterns: Composite

Design Patterns are the foundation of software development. In an abstract form, they can help solve complex issues without specifying a programming language.

What is a Design Pattern?

As described in the Design Patterns book, a design pattern describes a problem that occurs repeatedly. With the pattern, you identify the core of the solution to that problem that you can use a million times without ever doing it the same way twice [1].

We can now dive into the composite design pattern and how it can be helpful in our development.

Composite Design Pattern

The composite pattern was first introduced in the book: "Design Patterns: Elements of Reusable Object-Oriented Software." The definition is as follows:

Composite: Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly [2].

This definition might sound confusing. Essentially, composition is the process of making an object composed of multiple objects of the same type.

Let's use a visual example for this.

Imagine a matryoshka, which, if you don't know, is a doll that when you open, you find another doll under it.

Now, let's take this example and change it a little. Imagine that when you open one matryoshka, you may get two matryoshkas. In the end, they are the same component (doll). But they can be individual dolls or compose a bigger doll, the matryoshka.

As you can see from the diagram above, the composite of matryoshkas can scale indefinitely.

In this design pattern, there are two types of participants [2]:

  • The Composite: Defines behavior for components having children.
  • The Leafs: Represents leaf objects in the composition. A leaf has no children.

Now, after you open everything, you can place all of the dolls next to each other, and they are still dolls even though they are part-whole hierarchies.

Advantages of Using the Composite Design Pattern

The main advantage of using this design pattern is that you can apply the same operations to both composites and individual objects. This allows us to ignore the differences between the composition of objects and individual objects [3].

In this case, the matryoshka is a composite with all of the dolls inside and laid out the same way.

This example translates to the composite design pattern. We can now move on to a practical case that can be used in software development.

Practical Example

Now, imagine that you're asked to build software for a client. They want a social media app similar to Instagram. When the user is logged in and does not have Internet access, you should show them a cached feed; otherwise, you should show the feed that you receive from the API [4].

Before we implement this design pattern, let's consider the architecture of the feed.

Even though logic will be used to create the remote and cached feeds, there is clearly a common point between them: the loading of the feed.

This feed loader will load (from somewhere) a list of components shown in the feed (for Instagram, it would be images). The remote and cache feed loaders share this common point. Let's represent this relationship with a diagram.

After some time, your team implements Remote Feed Loader and Cache Feed Loader. They both work, but you still haven't implemented what the client asked for:

When the user doesn't have internet show cached feed, and when it does, show the feed that comes from the API.

How can you implement this?

Both Remote and Cache Feed Loader implement the protocol FeedLoader, which gives us a unique advantage in implementing the composite.

You can create another class for the composite on the main module that implements the Feed Loader.

You can take a step back and see the similarities from the example of the matryoshkas. RemoteFeedWithCacheFallback, RemoteFeedLoader and CachedFeedLoader will implement the Feed Loader Protocol, which can be used independently.

However, they can also be composed into a complex structure (RemoteFeedWithCacheFallback) that effectively creates the composite.

What does the composite look like in code?

Here's a small snippet of a possible implementation of the example I discussed in this article.


public class RemoteFeedWithCacheFallback: FeedLoader {
	private let primary: FeedLoader
	private let fallback: FeedLoader

	public init(primary: FeedLoader, fallback: FeedLoader) {
		...
	}

	public func load(...) {
		primary.load { result in
			switch result {
			case .success:
				...

			case .failure:
				fallback.load(...)
			}
		}
	}
}

In this case since the composite is expecting to receive two implementations of the protocol and not the concrete class implementations (remote and cache) we can even switch them when we initialize the class (instead of the primary being the remote we can use the cache loader first and then the remote loader).

The fact that we use dependency injection here gives us a great advantage in tackling different situations in the future with minimal changes.

Conclusion

The composite design pattern is very useful for representing part-whole hierarchies of objects. This pattern can be implemented when the differences between object compositions and individual objects can be ignored [2].

The practical example in this article came initially from the Essential Developer course. I was first introduced to the composite design pattern during the course, so I thought it would be appropriate to use it to explain how powerful this pattern can be in software.

I hope you enjoyed reading this article and especially that you learned something new today! 🌞

I'm delighted to be able to write content to help other developers and discuss different topics. If you want to suggest another topic for an upcoming article, let me know by connecting with me via LinkedIn or Twitter.

Have a great day! 🪆

References

[1] Introduction: Design Patterns: Elements of Reusable Object-Oriented Software by Enrich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, page 2.

[2] Design Patterns: Elements of Reusable Object-Oriented Software by Enrich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, pages 163-173.

[3] The Iterator and Composite Patterns: Head First Design Patterns: Building Extensible & Maintainable Object-Oriented Software by Eric Freeman and Elisabeth Robson, page 361.

[4] Project Example from the Essential Developers Academy by Caio & Mike.