This talk is about clean architecture and our experiences with introducing it into our Android app at Buffer.
My name is Joe, and I’m an Android Engineer at Buffer. At Buffer, we create a Social Media scheduling tool so you can write posts and send them out to multiple social networks throughout the week at any time.
When There Is No Architecture
With Buffer there was a rush to get features to production to test the market. Without a period to plan the architecture, it can be easy to make the wrong decisions. The issues we faced included:
-
Having a high level of technical debt. This means that issues take longer to resolve and new features take longer to ship as a consequence of bad choices in the past.
-
Classes were over coupled to each other.
-
No unit or UI tests.
-
Not knowing the origin of bugs.
If your project isn’t structured or architected properly, as the team grows, it’s going to get more complicated for you and developers to work on the project.
Buffer for Android
Joining Buffer, it was difficult for me to jump in and know what was going on where. New people joined and they didn’t understand what was going on; they were building on top of stuff that was already at fault. The debt was getting even greater.
There was little structure, and the Activities were often bloated with multiple responsibilities. There was a lot of code duplication - custom views and custom logic. The bugs we fixed would often appear elsewhere where we did not expect weeks later.
Moving to MVP (Model View Present)
With a fear of changing too much, when I joined we decided to start using MVP (Model View Present). We introduced MVP to keep it simple and not change too much at once. This has allowed us to create a clear line of separation between our framework and our presentation logic. We moved all our data operations into data manager classes. In turn, this allowed us to increase our test coverage.
But MVP Alone Was Not Enough
But we had this data manager class, which was already becoming bloated with multiple responsibilities. It was accessing the API, accessing the cache. It felt a bit like oh we got this data operation, let’s stick it in that data manager class. It’s messy.
There was a data manager class with multiple responsibilities - it was accessing the API and accessing the cache alongside preference helper classes. Despite splitting these out into a composer and user data manager, it was still bloated and unorganized.
In the instance of the data manager, if we wanted to take the app offline in the future, how would we be able to accomplish that based on its current state? As a way to solve this, we decided we needed to change the architecture.
Architecture
It was clear we needed something that allowed us to be flexible, and allowed us to maintain the app and have a higher level of test coverage. Having a good architecture on Android should be intuitive. For example, you should be able to look at a package structure and understand how it’s organized. I often think about this from the perspective of writing open source code: would my code be apparent to I don’t know, and who isn’t on my team?
Clean Architecture
There is no specific way for implementing good architecture. But, there is a classic diagram of clean architecture. In particular, there are four sections:
- The domain logic: entities, data, models.
- Business rules: use cases.
- Interface adapters: presenters and presentation logic.
- Frameworks/drivers: UI, networking, databases.
With clean architecture, dependencies have to point inward. For example, presenters cannot know about interfaces, and entities cannot know about use cases.
Separation of Concerns
You can have more focused test classes, with each independent. In our Android app, the only layer that knows anything about the Android framework in our project is the UI layer. Our presenters and entities are unaware of the Android framework. As a result of the refactor:
-
Our code is more testable, and our classes are and more focused and maintainable.
-
Our inner layers are independent of the user interface. Because the UI can be the most sensitive part of Android development, these decoupled makes a big difference.
-
The inner layers are independent of any external parties.
-
Our bugs are more isolated because everything’s layered and everything has its own responsibilities. This helped out track down the origin of the bugs.
Layer Models
Enterprise Business Rules
The first layer is the enterprise business rules
, the core business rules of our application. For example, Twitter’s rules might be a profile or a tweet. Unless the needs of your business change, you should never need to touch these once they have been created. These can also be created before you write your UI.
public class TournamentModel {
public String id;
public String name;
@SerializedName("full_name")
public String status;
@SerializedName("date_start")
public String dateStart;
@SerializedName("date_end")
public String dateEnd;
public int size;
}
Application Business Rules
The application business
rules are rules that are specific to our application. These are defined by use cases, e.g. tasks that our application will carry out. For example, you might want to save a photo or get a profile.
An example of a use case in our application is where users have to get schedules.
public class GetSchedules extends UseCase<ProfileSchedules> {
private final SchedulesRepository schedulesRepository;
@Inject
public GetSchedules(SchedulesRepository schedulesRepository) {
this.schedulesRepository = schedulesRepository;
}
@Override
public Single<ProfileSchedules> buildUseCaseObservable() {
return schedulesRepository.getSchedules();
}
}
In this layer, we provide an interface to define how the outside layers will communicate with this layer. The outside layer will pass in a concrete implementation of that repository. This makes things more clear and easier to test.
Interface Adapters
The third layer is interface adapters
. This contains the presentation logic of our application. In our case, it contains the MVP side of things, e.g. the views and presenters. These presenters contain callbacks to define how we retrieve the data from the other layers. In our case we are using RxJava:
public class UpdatesPresenter extends BasePresenter<UpdatesMvpView>
implements SingleObserver<List<Update>> {
@Override
public void onSubscribe(Disposable d) {
...
}
@Override
public void onSuccess(List<Match> matches) {
...
}
@Override
public void onError(Throwable e) {
...
}
}
Frameworks & Drivers
Our final layer is the frameworks and drivers
. This contains all of our UI components such as the activities, fragments, and views. These can be outside layers, and eventually, these can be moved into other layers with a repository pattern to help with the interaction.
Layer Boundaries
With our different layers, we now have a clear layer boundary - interfaces with concrete implementations that we pass in to provide more flexibility. These interfaces dictate how others will communicate with them. This is known as the dependency inversion principle.
With clean architecture we’re not limited to these layers. You can easily have three layers instead, and you do not need to have a separate enterprise business rules (these can be merged into application business rules). In our case, we may separate out the database and analytics and have five layers instead of four.
Test Coverage
The great advantages that good architecture brings are test coverage. We are now able to write much more focused test classes, as everything has been separated out into its own responsibilities.
You are able to test use cases along with the models in each layer, including tests that cross the layer boundaries.
Lessons
We are still learning and are making mistakes as we go. Clean and good architecture is a general term, but it means using more abstractions and writing focused classes. Even if you don’t use clean architecture as a whole, there are things you can learn to benefit from it in your projects.
Advantages of Clean Architecture
- A higher percentage of code test coverage.
- Increased ease to navigate the package structure.
- The project is easier to maintain, as classes are more focused.
- Clean architecture allowed for being able to add features more quickly.
- Future proofing implementations.
- Clean architecture is a clear discipline and overall a good practice to follow.
Disadvantages of Clean Architecture
Trying to implement clean architecture adds substantial overhead in the beginning. New interfaces and implementations needed to be added, along with separate modules. During our transition, it was often we forget to reuse models throughout our project. These habits can take time to form.
Resources
We have a blog post about clean architecture and how have used it. The google samples is good if you want a solid sample of clean architecture. Clean Architecture: A Craftsman’s Guide to Software Structure and Design (Robert C. Martin), is also good.
Questions
Q: Could talk more about the models and implementing them in every single layer, and what the adapters between those different layers look like? Joe: The enterprise business rules have models, and the layer outside has a mapper class in addition to its own models, so that mapper class will have a builder pattern. The debate here will be about efficiency - because if you’re doing that with multiple models it’s not too efficient.
Q: Do you use clean architecture on iOS, and what’s the experience? Joe: We don’t at the moment, but I’m trying to push this.
Q: What happened to the data manager classes, and where do you now do the API calls and caching? Joe: In the repositories, the presenters have been injected with a use case and that use case has reference to the repositories. Those repository implementations are passed in.
Our data stores have a remote data store and a cache data store which will both implement this interface, and they have exactly the same operations.
Q: For your outer two layers, the presentation layer and your UI layer, do you expose everything in that, or give exposure to Android for both those layers?? Joe: We pass. The presentation layer we do have participle to pass that through to the view layer.
Q: With all this data transfer between layers, are you doing a parcelization or are you doing a capsulation between the layers? Joe: Not as of yet. We’re guilty of that. We’re passing them through, and then converting them using mapper classes.
Receive news and updates from Realm straight to your inbox