Our way to good software architecture
In this article, I wanted to share an experience of how we arrived at the architecture we are using now in our Android application and why we consider it well suited for our project. From my point of view, it is easier to describe what makes software design poor than list features that make it good. And to know what is poor software design one must have some experience with projects where the results were not very satisfying but the lessons learned were valuable. Therefore let me share some things I learned that stay on the way to good software architecture:
- hard to add new functionality or feature:
- there is no code to reuse for functionality that shares common traits with already implemented functionality and you are forced to copy existing code to implement a new feature
- the only solution how to implement a feature is to rewrite existing code to fit your needs
- it is easier to use a hack to implement a feature rather than find a place for the feature in the existing project structure
- when you add a new code, you break existing functionality
- it is hard for a team to work simultaneously on the same codebase
- it is hard to read the code, even if only one developer works on code
- hard to test added functionality:
- you can’t isolate software units for tests
- it is easier to test application manually as whole rather than write unit and integration tests for separate peace of software
As result writing and the testing code becomes more time consuming and more expensive.
Once we start to notice one of these things it is time to start to think about improving or introducing the architecture in our software. The architecture probably is not a big deal if you are building a 1000 line app or writing a prove of concept app, but since our project is neither of those cases, it was clear from the very beginning that we need a solid design for our application. I already wrote about the technologies we decided to use and how we arrived at those particular conclusions. And now when we are close to the finish we can look back and see whether our choices paid off. But first let me make clear, it is not possible to make a perfect design with the first time unless you already have developed an application with very similar user requirements and even then you should revise the technologies used. And that was one of the first lessons we learned about the app’s architecture in our project. Once we noticed that adding new features and testing them became much harder than the ones we implemented in the past we started to investigate the existing design. And if we found a better way how to organize our application we adjusted the architecture accordingly. Some of the biggest changes we made in our application was when we switched from Model View Presenter (MVP) pattern to Model View View Model (MVVM) and upgraded Dagger to the latest version. The other lesson we learned was, there might be cases when there isn’t only one good solution. That was the case when we were designing the database and data access layer in our Android app. We had several options like:
We were not sure which would fit better in our design and even now we think any of them would work well in our application.
At the time we started to design our application Android didn’t have yet Architecture Components nor Guide to App Architecture, so we had to start somewhere else but eventually we arrived to almost the same architecture model as suggested here https://developer.android.com/jetpack/docs/guide
Contrary to ASP.NET Core (we use it for our back end) where developer can choose a project template in Visual Studio and start to work with well established patterns just by extending the template, in Android world we had to create the project structure by ourselves by choosing the most appropriate technologies and architectural patterns. Nowdays you can start with Architecture Components and recommended app architecture (https://developer.android.com/jetpack/docs/guide), since most likely it will become most common way how to build complex applications in Android. But this is just a reference model — the backbone, base — where we can start to add our first classes. The next step was to extend our backbone in such way that we could keep avoiding pitfalls and shortcomings of poor design (the things I mentioned in the beginning). And this is the place where the classic Object Oriented Programming skills come in to play. First to make it easy to extend our application with new features we applied SOLID principles. Where S stands for Single-responsibility principle, O — Open-closed principle, L — Liskov substitution principle, I — Interface segregation principle, D — Dependency Inversion Principle. Next when we noticed technologies that allowed us to follow these principles we included it in our project. For example Dagger was one of such libraries that allowed us to follow Dependency Inversion Principle and Android data binding library was another one that made it easier to follow Single responsibility principle. Once we started to apply SOLID principles we were able to identify the standard design patterns in our application code and so we started to organize the parts of the code around the design patterns (https://www.geeksforgeeks.org/software-design-patterns/).
To make a good architecture for complex application is not an easy task. It requires experience, good understanding of Object Oriented programming and knowledge of technologies in platform you are developing for.