Building your own Design Pattern
Hybrid MVC + MVVM
Ever
so often, developers face the dilemma of choosing from multiple design
patterns for their code. Yes, there are deadlines to adhere to, there
are changing needs of the product, the code needs to be testable and
what not! But as a developer, you always want to write code that’s
scalable, modular and easy to debug for any issues, if not bug-free
now — not an easy task by any means! You have to be very smart in
locking down the foundation for your code. After all, it’s only on a
strong foundation, that you can build a scalable structure.
What you’ll learn in this post:
- What MVC, MVVM etc. are all about and some of their shortcomings
- Why design patterns are an important part of development lifecycle
- How you can alter some of the aspects of these patterns to come up with your own version, as per your use case
- How we design high quality, stable features at Hike using our Hybrid design pattern
MVC
The standard MVC pattern has three major components:
Model — The
Model is usually the data source of the system. It interacts with the
controller to provide the current state of your database. Data can
reside locally in your system, or fetched from the servers. In any case,
the model provides you with the relevant information.
View — View
is what you see on your screen. All the UI components together
constitute the view. In standard MVC implementations, views are usually
pretty dumb with no business logic to them. They get directions from the
controller and populate themselves accordingly. Similarly, any actions
that the user takes on the UI are passed on to the controller to handle.
Controller — This
is where all the action happens! The Controller takes care of multiple
things. Firstly, it instantiates both the Model and the View components.
All business logic in response to user actions, asking the model to
update itself, and listening to any changes in the model that might need
refreshing the view happens in the controller!
Few details of MVC:
- The Model and the View only interact with the controller, and NEVER talk to each other directly. All communication happens via the Controller only.
- As a result, the Controller usually becomes one massive class, handling multiple responsibilities.
- All components are tightly coupled with each other. This makes reusability of these components difficult, replacing any of them later on, or making changes to them is a tough ask!
- Writing tests and debugging intricate bugs in one massive controller class can be tricky.
MVVM
The following components form the main aspect of this pattern:
Model — The
Model in MVVM works similar to MVC. It gives you the data you need,
which can be present locally, or fetched from servers behind the scenes,
just like in MVC.
View — Implemented
as a View/View Controller subclass, the view here talks to the view
model to gather all information, that is needed to display all UI
components.
View Model — This
is the major differentiating component from a MVC. The view model acts
as the intermediate between the view and the model. Unlike MVC, the
model is owned by View Model.
Few details of MVVM:
- The view here is a UIKit independent implementation. The view controller itself can be treated as the view component.
- There’s complete isolation of the Model and the View, which makes them loosely coupled with one another.
- The View Model invokes all changes to the model and listens to any changes that the Model makes behind the scenes.
- The View and View Model interact via bindings, so any change in the Model notifies the View Model, which in turn updates the View.
- This clearly helps break the massive controller of MVC, with the View and View Model now sharing responsibilities of updating UI and the coordination between components, respectively.
Discussion:
As
we saw earlier, while MVC works for small contained components, it
fails to meet the requirements of an evolving and growing product.
Adding new code to an MVC system is difficult without breaking existing
parts. Debugging issues in an MVC system can be time consuming, and
sometimes can lead you clueless, as your controller is taking care of a
million things at the same time.
While
MVVM was great in taking care of some of those concerns, we thought
there was still scope for some more modularity, some more scalability.
We could do better! We could break these components into further
subcomponents. The idea was to evolve the architecture in such a way
that more and more components became reusable. It should be easier to
replace one component without affecting the whole system.
So
we went through these and a few more design patterns like VIPER, RIBs,
evaluated their advantages/disadvantages, and compared them with our use
case. We went through the common limiting factors encountered while
scaling our systems. The final pattern we came up with was a hybrid
version of the MVC and MVVM patterns.
Hybrid MVC and MVVM pattern
The following diagram shows the various components and their interaction within this pattern:
Let’s go over the functionalities of each of these:
- Data Source — The Data Source is responsible for providing data to the entire system. This can be any generic class conforming to a protocol that is used by the Controller to ask for data. The data can be stored internally in any way. All that detail is internal to the implementation of Data Source. Conforming to a protocol ensures that we can replace the data source implementation without affecting any other component. All we need is for the new class to conform to the data source protocol, and we won’t have to change any other aspect. Given the Controller owns the Data Source, any changes in the underlying data are conveyed to the Controller that can then initiate appropriate action for it.
- Controller — The controller is at the centre of it all though much of its responsibility is to coordinate other items to enable them to function together. Usually this is a View Controller subclass, which is initialised with a Data Source object. Suppose we have to implement a chat screen, which shows all messages and has ability to send messages. The main chat view controller will act as the Controller. The table view to show all messages would be a part of the chat view controller. The initialisation would look something like this:
- View — The independent UI components can be separated out in a separate View layer. The Controller instantiates and holds references to them.The View is passed as a View Model object that the View uses to populate itself. In case of our example, the cells of the table View can be initiated with this pattern, where we pass them a View model object to populate with:
- View Model — The View Model is instantiated by the Controller for the independent View components that need not be part of the Controller class. Identifying such View-View Model components can help keep your Controller light and code modular. The View Model is initialised with a Model object that contains all information needed by the view to populate itself. The other important aspect to it is that we don’t expose class names anywhere, we only expose id<some_protocol> type of objects. This makes it easier to replace these components without affecting any other components:
- Action Handler — The view reports all user actions taken on it back to the Controller. In order to keep the Controller independent of the business logic behind all such user actions, we place all this logic in a separate component called Action Handler. This component gets the information regarding the type of action (say single touch, long press etc.) from the Controller, and applies all business logic to handle that event:
It
is important here to note that while the Action Handler has all the
logic to execute in response to any user action, it doesn’t actually
perform any UI operations itself. Any UI operation, be it
adding/removing a subview, or pushing/presenting any other View
Controller should only be done by the Controller. As we can see in the
above snippet, the Action Handler returns a Response Model object. This
object contains all information about the kind of UI task that needs to
be performed:
Based
on the value of the action type, the Controller picks the appropriate
UIView or UIViewController from the Response Model object and performs
necessary operations on it. Thus, there is a clear separation of
responsibility between the Controller and the Action Handler.
Conclusion:
This hybrid design pattern offers us multiple advantages over other patterns. When we applied it in app
- Our Controller classes have become very light weight, with its only responsibility being that of connecting all other components together. This has improved the testability of this component
- The Data Source kept the implementation of how data is stored abstracted, and this could be changed at any later point of time without affecting other components
- The Views/ View Model become reusable as they were only identified by adhering to a protocol
- Unit Testing becomes easier and could be done only for classes with a business logic in them.
- Most if-else checks in the code were minimised by using factory pattern to link different components.
- The readability of the code increased, helping other developers understand what’s going on in your features :)
Impact at Hike:
- The architecture helped us improve the overall app stability. We started implementing it at one place at a time. We began with chat, and this helped us make chat more robust and easy to work upon. For instance, here’s how our app stability grew over time:
- We were able to experiment internally with different versions of chat, as it was easy to just plug and play different components. This helped us make decisions faster.
Key takeaways:
- The decision of choosing a design pattern will go a long way with you. Make sure you give enough thought to it
- There are enough patterns out there, but you don’t have to necessarily choose from them. Feel free to come up with your own version based on your requirements