App Architecture and Object Composition in Swift
Object composition is the core concept of Object-Oriented programming. Objects can contain other objects, sometimes creating complex hierarchies.
In this article I would like to show how to design such architecture. On a real-life example, you will see that working with complex hierarchy is simple, while objects and hierarchies can be reusable.
Composition
Object-Oriented design process involves planning of how objects connect and interact to create a part or a complete system. We all used to Model-View-Controller architecture, that defines the controller object that contains the view and the model. Together this composition creates the backbone for an app.
The controller is a composite object that has a view and a model. This can be generalized to “has-a” relationship:
The object Object
is a composite type — has an object of the AnotherObject
type.
Things become interesting with recursive types that form a tree structure.
The Object
class contains the array of Object
type children forming a tree. We call objects in a tree nodes, and a node without children — leaf.
Recursive composition is simple and can be used to represent any potentially complex, hierarchical structure.
Composite Pattern
The composite pattern is a structural pattern described in Design Patterns: Elements of Reusable Object-Oriented Software book. It describes how to build a class hierarchy made up of classes for two kinds of objects: primitive and composite.
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
Design Patterns: Elements of Reusable Object-Oriented Software
The composite pattern defines next participants:
- Component is an abstract interface. It declares functions and used by a client.
- Leaf implements functions declared by Component interface.
- Composite implements composition. It contains array of child components. Composite implements functions declared by the Component interface by delegating (forwarding) calls to its children.
- Client is the code that uses composition through the Component interface.
We can implement the Composite pattern like so:
Real life implementation may be slightly different. Common variation is when Component, Leaf, and Composite are the same object.
Composition using tree structure is a very powerful pattern with many benefits for the app architecture:
- Break complex task into small components. Components solve small tasks. Composed together in a tree to solve a larger task.
- Interact with a complex structure same way as you would interact with a single instance. A tree of components has the same interface as a single component.
- Single Responsibility principle. Component has responsibility over a single function. It can be domain specific or composition specific (like arranging children in a specific order).
- Reusability. Components are reusable, tree of components can be used in larger hierarchy to solve larger tasks.
- Testability. Because components and trees of components are self contained they can be unit tested.
- Opportunity to apply various design patterns. Tree structure works well with variety of creational, structural, and behavioral design patterns.
- Simple. Last but not least. The composite pattern, in special cases, takes onle one class to implement. Trees and operations on trees are well known to developers.
Too good to be true? Let’s take a look at some examples.
UIView
UIView
’s hierarchy is probably the most prominent example of the tree structure.
Views are the fundamental building blocks of an app’s user interface. Views can be nested inside other views to create view hierarchies. E.g. we construct complex trees using small UIView
nodes.
We can group views by function:
- Base
UIView
class. It defines the behaviors that are common to all views, renders content, and handles any interactions with that content. - Displaying content.
UILabel
,UIImageView
,UIButton
and other, used to display domain specific content. This views are typically leafs (UILabel
andUIImageView)
and can be composites (UIButton
andUITextField)
. - Managing subviews.
UIScrollView
,UITableView
,UICollectionView
and other, used to arrange other views.UIStackView
is a great example — this view is a non-rendering subclass ofUIView
, used only to manage layout.
UIViewController
Starting with iOS 5.0, UIViewController
’s form hierarchy similar to UIView
’s.
Apple defines two types of view controllers:
- Displaying content. Content view controller manages a discrete piece of an app’s content. This are your
UIViewController
subclasses. - Facilitating structure and navigation. Container view controller arranges child view controllers in a specific way.
UINavigationController
,UITabBarController
,UISplitViewController
and others, this controllers define navigation and structure of an app.
Once more we see a tree where leafs implement domain specific functions.
Asynchronous Execution, GCD
Original motivation of the composite pattern, as described in The “Gang of Four” book is graphic application and documents. But application goes beyond — how about something asynchronous?
Common challenge when writing asynchronous code is controlling execution order and completion. Let’s assume we have a task that can perform asynchronous work and report completion using a closure (network operation for instance).
We have a list of tasks to perform and we want to know when all tasks complete.
One solution is to use DispatchGroup
, like so:
This is an opportunity to apply composition. Let’s create a class for a group of tasks that require a single completion handler:
Now all we need to do is provide list of tasks to run:
From here, we can build complex hierarchies of asynchronous tasks solving a large problem using small building blocks. If we want to, we can create ordered composite task class. This classes are simple and reusable.
There is a great session about using composition with NSOperation from WWDC 2015 — Advanced NSOperations.
Fetching Data, Persistance
Another great example of composition is fetching and persisting data. Suppose we want to fetch a model and we have two sources: local and remote. We want to prioritize local store. If model is not there — load it from the network. Here is how we can organize them using composition:
CompositeStore
iterates over the list of stores till result model is fetched. DispatchSemaphore
used to wait when every fetch operation completes before proceeding. Because order is defined when instantiating CompositeStore
, it will first try to load model from the local store, and if it fails — from the network.
UI Data Source
One of the most interesting application of composition is data source protocols for UITableView
, UICollectionView
, and MKMapView
.
Implementing this composition is challenging, but if you have complex UI based on one of this views — it definitely pays off.
This topic is covered in Advanced User Interfaces with Collection Views WWDC session.
Implementing Composition
As you can see, composition and recursive structure has wide applicability. Things not covered here include formatting, input validation, observing, data processing, state machines, etc.
The composite pattern is very simple, in some cases implemented with a single class. One thing to remember is to follow the roles. Component defines interface, Leaf implements small functions, and Composite combine everything together.
For more advanced architecture with tree structure — computer science is very helpful. Invest some time in learning basic algorithms on trees. Especially tree traversal: Breadth-first search and Depth-first search. This will pay off in long term.
In the end it boils down to identifying complex problems and breaking into smaller tasks. This is a much harder task, but this is were difference between good and great lies.