Our iOS modularization story
Some years ago … As a well-known retailer in The Netherlands for decades, wehkamp had already successfully made the transition from a catalog company to 100% e-tailer. Now it was time to explore the app universe at wehkamp.
We started with a SCRUM team with 1 Android developer, 1 iOS developer, 1 tester, some back-enders, a Scrum Master and a Product Owner. We filled our backlog and started building the app(s) from scratch. The apps were backed by a .NET API which was running on on-premise IIS servers. The .NET environment was already running for a significant amount of years now and has evolved into a big ass monolith.
Microservices and micro-site
In the years that followed, we as a team focused mainly on extending the app by adding features, improving existing features and optimizing the app as much as possible for our users. At the same time, the back-end .NET application was reaching its limits regarding maintainability, scalability, reliability, etc. and the teams around us were doing their jobs to get rid of this and make the switch to microservices and micro-sites. It was the right solution and it worked out very well.
The early days
In the early days of the app, the revenue part of it was negligible, so there was not much pressure from the organization on the app team. We just did our thing and had a lot of possibilities to experiment. I really don’t want to start a (native) app vs web war, but after a while, the figures were telling that a significant amount of users were more inclined to use an app. The conversion was higher, higher order values, more engagement. So within the organization, we got more and more attention and the need to grow was rising.
At the moment … our team has 3 iOS developers and 2 Android developers and we are still searching for more.
But … history repeats. Where the web was some years ago, the app evolved kind of into the same direction; a big ass monolith.
Working in a one-man-team it is not that necessary to think about maintainability, scalability, readability a lot. Of course I knew that the day would come that the team would grow, so I tried to implement every part of the app with this in mind. But sometimes the pressure is that high, you take shortcuts. It’s impossible to avoid this, but I tried to take those shortcuts as less as possible.
Inspired by the other teams creating microservices and micro-site and these guidelines, we decided to refactor our application and go modularizing! This is our story …
Our goal was to go from this:
But let’s first tell something about our starting point. This of course impacts the steps to take into the modularization direction, it makes it easier so to say …
Dependency Injection FTW
As I already mentioned, when I was just on my own the first couple of years I always kept in mind that one day we should grow and tried to make decisions that prevent disasters in the future as much as possible.
One principle that really helped me with this is Dependency Injection. I won’t go in much detail on this, there are a lot of sources available. But the idea is that you inject instantiations to a class instead of creating those in the class itself. This way you separate responsibilities instead of tighten things up, loosely coupled vs tightly coupled. I use dependency injection in combination with Protocol Oriented Programming where it makes sense. This approach kind of forces me to avoid coupling parts of the code that shouldn’t be coupled at all.
Another approach that helped was separating code into separate ‘feature-groups’. I put all code related to a specific feature in its own group. As you can see here:
This in combination with dependency injection resulted in pretty independent parts of the code. Every feature has its own storyboard (or sometimes multiple to prevent storyboard-team-merge-conflict-hell) and all other stuff related to that feature. As you can see in the picture there are for example features ‘Shopping’ and ‘Favorites’. The feature ‘Favorites’ shows us a list of products and you can imagine that a user wants to see product details when tapping on a product in that list. The product detail viewcontrollers are in feature ‘Shopping’. To achieve this I added a storyboard reference to the Shopping storyboard into the Favorites storyboard. And … gone is our separation!
First step; Core and CoreUI vs independency
An important part of the microfeatures implementation is that the features should be able to live on their own. This sometimes can conflict with the DRY (Don’t Repeat Yourself) principle. There are situations that you will have to copy-paste code from one feature into another. I used to re-use as much as possible code. For example; imagine a list of products for a specific category, like ‘Women’s clothing’. This shows us a list of products like this:
It’s a collectionview with cells that contain an imageview and some labels.
Now imagine that we want to add a new awesome feature to our app; a wishlist! The designs show us that it’s basically the same design as we already have for a ‘normal’ product list. So my first thought; let’s re-use that! To do that we create a class ProductListCollectionView. We have to define a datas ource, so let’s create a protocol ProductListCollectionViewDatasource. On the existing productlist we have a heart icon to add a product to wishlist, but we don’t want that if you are in the wishlist. So let’s add a protocol ProductListCollectionViewDelegate with a method shouldShowHeartIcon.
Wow! That’s working like a charm. Some sprints later we want to add an icon in wishlist to add the product to the basket. It’s an A/B test so we don’t want it on the product overview for now. We should add an extra delegate method shouldShowAddToCart. You can imagine that this will evolve into an unmaintainable, ugly, if-else polluted giant. That in the end, you don’t have a lot of common UI left. We should have started creating it as an independent element without sharing things. Don’t feel bad to sometimes copy-paste from one feature into another.
But there’s a trade-off. If you find yourself copying a specific part of code into all features, this could be a signal that it’s valid to create something to share this code. This is where modules ‘core’ and ‘coreUI’ come into play.
These are the modules where you put code that is used by all features. Sometimes it’s hard to decide if a specific piece of code should live in ‘core’ or that it needs its own module. I decided to put these kinds of things in core:
- AB Testing
- Extensions on Foundation classes
The same count for UI elements, things that you will find here are:
- UI elements
- Collectionview layouts
- Base ViewController
- Input validation
- State full ViewController
- Extensions on UIKit classes
A thumb of rule for myself is that we should put as less as possible code in these modules, copy/pasting every now and then isn’t that bad…
Next step is to create a new module with only the code for that specific feature. I’ll explain how to do this for your iOS project in Xcode step by step in another post. (Edit; because of time issues I created an example app for now, another post will maybe come later. Source: https://github.com/martijnschoemaker/modularizesampleapp). For now, it’s only about the concept. In our case when you open a feature project you will see these groups:
- UI; with all UI related files; ViewControllers, Views, Storyboards and NIBs.
- AB Tests; here we define AB tests we use in this feature.
- Handlers; here are implementations of handlers we can define. For example, it’s possible to register a handler for ApplicationContinueActivity delegate this way a module can decide itself if it should handle this and if so what to do.
- Model; for all model classes
- Network operations; we define our network request as HttpOperations. HttpOperation is a protocol, for a specific request we implement a HttpOperation, here you can find these operations.
In other words, all stuff related to the feature should be in the feature module, not more not less.
Remember that example where we wanted to show a product detail screen when tapping on a product in the wishlist? In the past, we just separated features into different groups within one project. So this gave us the possibility to use code from other features.
We implemented the features in separate modules and ended up with multiple modules that don’t know anything about each other anymore. But we still want to be able to open that product detail screen when tapping on a product in the wishlist.
But … how? All these dependency magic is done by adding a ‘man-in-the-middle’ module; dependencies. This module will take care of the dependencies between features. Features don’t reference each other but only reference that dependency module. The dependency module shouldn’t contain any implementation classes. It’s only glue between features.
If we want to open a product detail view when tapping on a product wishlist we need something like a ProductViewControllerProvider
So what we could do is introduce a class ProductViewControllerProvider in dependencies which can provide the wishlist module with the right viewcontroller so it can show it. But this way we give dependencies module too much knowledge about the implementation. It shouldn’t know how to create a viewcontroller to show a product. The only one who knows this is the Shopping feature where the viewcontroller really lives.
So we need some abstraction here …
That’s where we can benefit from using dependency injection and protocol oriented approach from the beginning. The protocols are the glue, and the dependencies module exists for the glue. So dependencies should contain protocols, not implementations.
For our product detail from wishlist example this results in something like this:
- In Wishlist feature, we implement the list of products that are on the wishlist.
- In dependencies we have a protocol ProductViewControllerProvider with method getProductViewController() -> UIViewController.
- In shopping feature, we have the implementation of ProductViewControllerProvider that knows how to create the viewcontroller and returns it.
- Via dependency injection, we define in shopping module that for protocol ProductViewControllerProvider the implementation in shopping module should be used.
From the moment we created a base implementation by introducing core, coreUI, and dependencies we decided to modularize feature by feature. Picking up one or 2 features per sprint. At the moment we have done about 80% of existing features. For new features, we will create a new module.
I created a sample app with some modules. It doesn’t contain much features, but the idea of modularization and depedency injection should be clear.
Thank you for taking the time to read this story! Feel free to leave your comments and ask questions! And share your own experiences with modularizing your app.