The Composite pattern is used to represent objects in a hierarchical structure, where objects can have other objects as children, forming a tree-like structure. On the other hand, the Decorator pattern is used to dynamically add behavior or features to an object at runtime, allowing for flexible and modular code.
Let’s consider a scenario where we are building a music application that allows users to create playlists of songs, and also apply filters to those playlists. The application supports different types of playlists such as regular playlists, smart playlists, and radio playlists. Each playlist can contain a list of songs, and the smart playlist can filter songs based on user preferences.
To build this complex hierarchy of objects, we can use the Composite pattern to represent the playlists and their songs. We can create a base interface or abstract class called PlaylistComponent to represent both the playlist and individual songs. Here’s an example:
public interface PlaylistComponent {
void play();
String getName();
}
The interface has two methods ‘play()‘ to play the component, and ‘getName()‘ to get the name of the component.
The ‘Playlist‘ class represents the composite object that can have other ‘PlaylistComponent‘ objects as children:
public class Playlist implements PlaylistComponent {
private List<PlaylistComponent> components;
private String name;
public Playlist(String name) {
this.name = name;
components = new ArrayList<>();
}
public void addComponent(PlaylistComponent component) {
components.add(component);
}
public void removeComponent(PlaylistComponent component) {
components.remove(component);
}
@Override
public void play() {
System.out.println("Playing playlist " + name);
for (PlaylistComponent component : components) {
component.play();
}
}
@Override
public String getName() {
return name;
}
}
The ‘Playlist‘ class has a list of child components, and it implements the ‘play()‘ and ‘getName()‘ methods to play the playlist and get the name.
The ‘Song‘ class represents the leaf object that does not have any child components:
public class Song implements PlaylistComponent {
private String name;
private String artist;
private String album;
public Song(String name, String artist, String album) {
this.name = name;
this.artist = artist;
this.album = album;
}
@Override
public void play() {
System.out.println("Playing song " + name + " by " + artist + " from " + album);
}
@Override
public String getName() {
return name;
}
}
The ‘Song‘ class implements the ‘play()‘ and ‘getName()‘ methods to play the song and get the name.
Now, to add filters to the playlists, we can use the Decorator pattern. We can create a base decorator interface called ‘PlaylistFilter‘:
public interface PlaylistFilter extends PlaylistComponent {
List<PlaylistComponent> getFilteredChildren();
}
The interface has a ‘getFilteredChildren()‘ method that returns a filtered list of child components.
We can create different types of filters that implement this interface, such as ‘GenreFilter‘, ‘ArtistFilter‘, and ‘AlbumFilter‘. Each filter class takes a ‘PlaylistComponent‘ object and filters its child components based on some criteria.
Here’s an example of the ‘GenreFilter‘ class:
public class GenreFilter implements PlaylistFilter {
private PlaylistComponent component;
private String genre;
public GenreFilter(PlaylistComponent component, String genre) {
this.component = component;
this.genre = genre;
}
@Override
public void play() {
component.play();
}
@Override
public String getName() {
return component.getName();
}
@Override
public List<PlaylistComponent> getFilteredChildren() {
List<PlaylistComponent> filtered = new ArrayList<>();
for (PlaylistComponent child : component.getFilteredChildren()) {
if (child instanceof Song) {
Song song = (Song) child;
if (song.getGenre().equals(genre)) {
filtered.add(child);
}
} else {
filtered.add(child);
}
}
return filtered;
}
}
The ‘GenreFilter‘ class takes a ‘PlaylistComponent‘ object and filters its child components based on the genre of the songs. The ‘getFilteredChildren()‘ method returns a filtered list of child components.
To create a smart playlist with filters, we can create a ‘SmartPlaylist‘ class that extends the ‘Playlist‘ class and adds filtering behavior at runtime:
public class SmartPlaylist extends Playlist {
private PlaylistFilter filter;
public SmartPlaylist(String name, PlaylistFilter filter) {
super(name);
this.filter = filter;
}
@Override
public void play() {
System.out.println("Playing smart playlist " + getName());
for (PlaylistComponent component : filter.getFilteredChildren()) {
component.play();
}
}
}
The ‘SmartPlaylist‘ class extends the ‘Playlist‘ class and overrides the ‘play()‘ method to play the filtered child components instead of all child components.
Now, we can create a hierarchical object structure by combining ‘Playlist‘ and ‘Song‘ objects using the Composite pattern. We can also create a smart playlist object by decorating a ‘Playlist‘ object with filters using the Decorator pattern:
Playlist playlist = new Playlist("Playlist");
playlist.addComponent(new Song("Song 1", "Artist 1", "Album 1"));
playlist.addComponent(new Song("Song 2", "Artist 2", "Album 2"));
playlist.addComponent(new Song("Song 3", "Artist 3", "Album 3"));
PlaylistFilter genreFilter = new GenreFilter(playlist, "Rock");
Playlist smartPlaylist = new SmartPlaylist("SmartPlaylist", genreFilter);
smartPlaylist.play();
In conclusion, we can combine the Composite and Decorator patterns to build complex object hierarchies that are both flexible and modular. The Composite pattern allows us to create a tree-like structure of objects, and the Decorator pattern allows us to add behavior or features to those objects at runtime.