We at QuickBird Studios are often faced with a wide variety of problems. Some of them are very easy to solve, some of them are very, very hard and some are … let’s say… „very unique“ 😅. We tried a lot of different patterns and strategies to solve those problems and one pattern that we fell in love with is the Composite Pattern. We call our own approach to this pattern Composite by Induction (short Induction) and it enables you to use the composite pattern in very complex situations with very low effort.
Here is a very quick refresh of the composite pattern. Feel free to skip this part if you are already familiar with it!
„The composite pattern describes a group of objects that are treated the same way as a single instance of the same type of object.“ Wikipedia
As a developer, whether or not you thought about it, you most likely encountered the composite pattern many times in your career. One of the simplest and most straightforward examples would be Android’s View-System. Any
View can do stuff like draw yourself to a canvas, setAlpha or animate. You can group or layout those
Views by putting them into a certain Layout which extends
ViewGroup. Obviously, this
ViewGroup itself can draw itself to a canvas, setAlpha, or animate, because a
ViewGroup itself is just an implementation of
Let’s imagine you are building an app to track the weight of your customers. One feature you want to implement is the detection of certain data-patterns to help your users to better understand their health and behaviors. So you start thinking about potential patterns that could add value to your customers when detected. You quickly came up with many ideas: Maybe you want to detect your customers cheat days 🤔. You definitely want to find up & downtrends in the data and maybe even encourage „good“ trends inside your app.
After you defined your
Pattern interface, you start modeling the detection of those and you came up with a very simple interface:
It is a good idea to split the problem of detecting patterns into smaller problems. It sounds clever to have a certain
PatternDetector for each pattern you want to support. So you start implementing a
UpTrendPatternDetector, etc. which obviously all implement the same interface
But wait 🧐. Obviously, there is a very simple way that you can represent all those implementations as a single instance of
PatternDetector instead of a
List<PatternDetector>. Induction makes this possible by solving this problem for the simplest case. Maybe, if we combine two
PatternDetector instances, we have a new
PatternDetector which is just more powerful? Maybe we could combine an instance of
DownTrendPatternDetector with an instance of
UpTrendPatternDetector to get a more powerful PatternDetector able to detect down- & uptrends. Here is how you could do this using induction.
A Connector is a class which knows how to combine two instances of an interface and itself implements this very own interface itself.
Let’s take our weight-app example and think how a combination of two
PatternDetector instances should behave. This is very simple ☝! We just expect the combination to return a list containing all patterns found by the first detector and all patterns found by the second connector as a list!
We do not want to deal with the connector type directly. Instead, we create a combine function as extension function on
PatternDetector which only exposes the desired interface.
If we would now combine the
DownTrendPatternDetector with an instance of
UpTrendPatternDetector we would get a new
PatternDetector able to detect up- and downtrends in our weight-data:
Now, one can easily get the final combined instance by chaining all specific instances together.
The given weight-app example is very simple and connecting two instances of a certain type can get very difficult sometimes. Some problem, that we encountered is that the trivial combine logic is not always desired. What if we want to favor a certain
PatternDetector over another? What if we want to prevent two pattern detectors to respond on the same area on the data?
This logic can be expressed by using different connectors and different extension functions.
Let’s have a look back up where we combined an instance of
DownTrendPatternDetector with one of
UpTrendPatternDetector: We notice that both instances are declaring a small shared area as UPTREND as well as DOWNTREND which is not that cool 🤔
Let’s implement an alternative Connector that alters detected patterns to avoid overlays. We could do this by splitting the overlapping area in half, making the overlapping patterns smaller in size!
We can now compose our final production-ready instance of
PatternDetector by using our two distinct combine-methods!
We are using Composite by Induction in a wide variety of projects and circumstances:
- We combine multiple
ShapeDetectorinstances to find basic shapes like circles or polygons in images
- We model the data layer of apps by combining multiple
- We build beautiful animations by combining multiple
Obviously, it is almost always easier to combine n instances of a certain type by combining always 2 instead of combining them all together at once. That’s why this pattern can even simplify your thinking process. You solve a problem for two instances and you’re afterward able to apply it to n instances.
If you want to know more about how we architect mobile apps, check out this article about our approach to MVVM, a modern UI architecture pattern.