Single responsibility. Separation of concerns. Code isolation. Data encapsulation.
These concepts are so universal. They are easy to accept and take for granted. But do you actually know what they mean? Can you explain them?
I’ll attempt to crystalize these buzzwords/theories/principles by taking a different angle.
There are two types of isolations.
- Isolating components using protocols to declare inputs and outputs, and expose only external behaviors.
- Isolating features using structs to encapsulate data in separate requests, responses, and view models.
Let’s explore each to understand how clean architecture can facilitate unit testing.
Component Isolation
You can change the data or methods of a component, whether it’s a view controller, interactor, or presenter. Only the unit tests for that component should fail because of that change. The unit tests for other components in the rest of your app should still pass, because their external dependencies are mocked.
This makes refactoring very manageable, when requirements need to change, or when performance needs to be optimized.
The steps you should undertake are:
- First, you update this one component to make it work with the new change.
- Next, you pick another component, update the mock for that last component to reflect the new change, watch its unit tests fail, and fix them based on the new change.
- Rinse and repeat until all components are updated to work with the new change.
You can read about how to unit test your view controllers in these two-part posts: Part 1 and Part 2.
Swift protocols are used to accomplish component isolation.
Feature Isolation
To help you visualize, take a look at the following figure. The black box represents a massive view controller. Each pair of yellow arrows going in and out represents a feature, for a total of six features. Each red line represents a specific implementation of the feature.
As you can see, the six red lines of the six features get tangled up into a giant hairball. Touching each hair will move the entire hair ball. This interdependence makes it impossible to separate them out when you need to make a change. The intertwined nature of the implementation makes unit testing difficult, resulting in very fragile tests.
Features are too coupled. Let’s decouple them. In the next figure, the blue lines separate the black box into six individual channels.
Each channel represents a single feature, divided by the blue lines. The red lines are specific implementations of the features. They can take many forms and turns passing through the channels. Notice the red lines never cross the blue lines. So the implementation are isolated from one another. This keeps the code in the component very clean. You’re free to change your implementation by redrawing the red lines while staying within the channels. So changes to one feature doesn’t affect other features.
Swift structure are used to accomplish feature isolation.
In the big picture, the black box can be the view controller, interactor, or presenter. And the channels then represent requests, responses, and view models, respectively.