DEV Community

Cover image for Design Patterns: Composite
Tamerlan Gudabayev
Tamerlan Gudabayev

Posted on • Updated on

Design Patterns: Composite

Composite is one of the easier patterns in this series, but this does not mean it's not popular.

In fact, it's one of the most useful patterns because it can be expressed in many different business domains.

So without further ado, let's begin.

Today you will learn:

  • Structure and theory of the composite pattern
  • Problems that the composite pattern solves
  • Solution it provides
  • Implementation of the solution
  • Recognize opportunities to use the composite pattern
  • Benefits and downsides of the composite pattern

Definition

Alt Text

Formally speaking the composite pattern allows us to treat composites and individual objects equally.

But what does that mean?

Let's say you work for an e-commerce site, there are two kinds of products:

  • Product — Typical individual object (toothpaste, apple, etc...)
  • Box — Group of products or even other boxes

Visually speaking it would look more like this:

Alt Text

So in a nutshell, the composite pattern allows us to compose objects into hierarchical structures, and then work with these structures like individual objects.

Let's take a look at another example.

Problem

Let's imagine you are building the next Spotify.

What do you offer the user?

You would most obviously say music.

But the catch is that your app allows you to have playlists of songs or even other playlists.

The question is now how do we represent this in code?

One way would be to simply loop and check if the object is a song or a playlist.

It would look like this:

function play(object){
    if (object instanceOf Song){
        const song = object
        return song.play()
    }

    playlist = object

    // else it is a playlist
    for(stuff in objects){
        play(stuff)
    } 
}
Enter fullscreen mode Exit fullscreen mode

This seems weird to write, why does my client have to know whether this is a song or a playlist?

Your client should not know, and this is the problem that the composite pattern solves.

Solution

Before we transform our code above, let us quickly go over the components of the composite pattern.

Alt Text

  1. Component — The uniform interface that both the composite and the leaf will abide by.
  2. Composite — Our "group" object that implements the component interface, and has a list of leaf objects.
  3. Leaf — Our individual object, that directly implements the component interface.

Now that we know the core components of the composite pattern, let us transform our previous code into something abiding by the composite pattern:

Step 1: Define our component interface:

interface SongComponent {
    function play();
    // other uniform functions
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Define our leaf class:

class Song implements SongComponent {
    function play(){
        // play the damn song!!
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Define our composite class:

class Playlist implements SongComponent {
    songs = []

    constructor(songs){
        this.songs = songs
    }

    function play(){
        for(song in this.objects){
            song.play(stuff)
        } 
    }

    function add(object){
        this.songs[] = object
    }

    function pop(object){
        this.songs.pop()
    }

}
Enter fullscreen mode Exit fullscreen mode

Step 4: Test the code:

function main(){
  song_one = new Song()
    song_two = new Song()

    playlist_one = new Playlist([song_one, song_two])
    playlist_two = new Playlist([song_one, song_two, playlist_one])


    // we treat of all these equally
    song_one.play()
    song_two.play()
    playlist_one.play()
    playlist_two.play()
}
Enter fullscreen mode Exit fullscreen mode

Congratulations, you have just implemented the composite pattern.

Rewards: Respect + 10

PS. You might be asking why didn't we add functions like add() or pop() to our component interface? This is a very popular debate, both are right. It's a question if you want your code to be uniform or type-safe. I chose type-safe for this example.

When to use this pattern?

This pattern is pretty simple, you would only use it when:

  • You want to express your data hierarchically.
  • You want to treat individual objects and composites equally.

Benefits and Downsides

It's not all sunshine and rainbows, but let's go over the rainbows first:

  • You work with tree-like structures more conveniently.
  • You can introduce new element types without breaking existing code.

Now comes the downside:

  • It's difficult to create a uniform interface to classes whose functionality differs greatly, this sometimes forces to over-generalize classes.

Conclusion

I'm glad you reached this far.

Now you can leverage the composite pattern in your next or existing projects.

If you did, leave a comment down below if you like the pattern or not.

Also, I also publish small snippets of these articles and other good stuff on Twitter @tamerlan_dev.

Thanks for reading!

Further Readings

If you want to learn more about the design patterns, I would recommend Diving into Design Patterns. It explains all 23 design patterns found in the GoF book, in a fun and engaging manner.

Another book that I recommend is Heads First Design Patterns: A Brain-Friendly Guide, which has fun and easy-to-read explanations.

Discussion (0)