The general structure of the Composite pattern will be instantly familiar to Flutter developers, because Flutter's widgets are constructed with an expanded version. Regular Flutter widgets, defined as those that can have only one child, and collection Flutter widgets, defined as those that take a list of children, share a common interface, allowing client code to handle each similarly. With the Composite pattern, you can create part-whole hierarchies, tree structures in which individual objects and compositions of those objects can be treated uniformly.
About structural design patterns: Structural patterns help us shape the relationships between the objects and classes we create. These patterns are focused on how classes inherit from each other, how objects can be composed of other objects, and how objects and classes interrelate. In this series of articles, you'll learn how to build large, comprehensive systems from simpler, individual modules and components. The patterns assist us in creating flexible, loosely coupled, interconnecting code modules to complete complex tasks in a manageable way.
This pattern is ideal whenever you need to represent a hierarchy of objects, such as when you're modeling a computer file system. The basic components of a file system are files and directories, and directories can contain both files and other directories. You could use the Composite pattern to ensure all of your file system entities share a common interface, meaning that files and directories would have many of the same features, though each would do what's appropriate for its type.
The code for this article was tested with Dart 2.8.4 and Flutter 1.17.5.
A simple Composite example
To demonstrate the Composite design pattern with Dart, we'll look at a data model supporting generic items and containers, such as for a package shipping application. Items will have properties for their monetary value and their weight, and containers will also support those attributes. Any container can contain both items and other containers:
class Item {
final double price;
final double weight;
const Item(this.price, this.weight);
}
class Container implements Item {
final List<Item> items = [];
void addItem(Item item) => items.add(item);
double get price =>
items.fold(0, (double sum, Item item) => sum + item.price);
double get weight =>
items.fold(0, (double sum, Item item) => sum + item.weight);
}
An Item is a simple, immutable class that stores an item's price
and weight
.
The Container class implements the implicit interface exported by Item, which means Container must provide implementations of the price
and weight
getters from the Item class. Dart creates implicit getters and setters for every variable and property that doesn't have explicit accessors defined. In the case of final
properties, no implicit setter is created, as these properties are protected from reassignment after initialization. Therefore, Container need only define getters that match the Item API. The Container versions of price
and weight
use the List class's fold()
method to return the total price and weight of all items within the container, as containers don't have those attributes themselves.
Because Container implements the Item interface, it can be treated as an Item, and it can be included in a List<Item>
collection:
final container1 = Container()
..addItem(Item(5.95, 1.5))
..addItem(Item(9.99, 2))
..addItem(Item(25, 2.3));
final container2 = Container()
..addItem(Item(16.5, 9));
container1.addItem(container2);
print("Price: ${container1.price}");
print("Weight: ${container1.weight}");
Here, we create a container and add three items to it. Next, we create another container with a single item inside. Then we add the second container into the first, so the first container now contains three items and a container. When we print out the price and weight of the first container, the result is an aggregate of all prices and weights contained therein.
Conclusion
It can be very convenient to be able to treat objects and collections of those objects the same way. Client code does not need to know which it's dealing with, since they both expose the same properties and/or behavior. A tree of containers and items can easily be created and managed with the Composite pattern.
To read more about structural design patterns in Dart, check out these related articles: