One of the most common things while developing an application is the need of adding external dependencies to your project to cover a requirement, and don’t get me wrong, I’m not just talking about third party libraries, even if we stick to the native frameworks, we will be adding dependencies to our project that escape from our control, and if there is something you don’t want to lose, is the control over your product because it will drive you through the wrong lane until there is no way to turn back.
In this article I will explain what I believe is a good approach (not the only one) to implementing an external dependency and still maintain control of our code base. It won’t matter which architecture your project implements since the main goal of this implementation is to isolate the dependency from our project and include it as a “plugin”. So you could be working with MVC, MVVM, MVP, VIPER or whatever you use, and this will still work because we are going to apply basic OOP theory and SOLID principles to solve this problem.
To be able to explain this, we are going to pretend that we are improving an application by adding some persistency mechanism. Could be whichever you like, a database like Core Data or Realm, a file based system or even a remote server, it doesn’t matter because the important part is how we will do it.
The app we are going to pretend to improve is a simple TO-DO app, and like any app of this type, the goal is to allow the user to add some tasks so we can remind him about them. So far the application works with a server, retrieving the information from it whenever the user launches the app.
||It doesn’t work without internet connection.
||Since the information is stored in a server, the app supports multiple devices, meaning that the information might change from different sources.
||Add a database so we can store the information locally and use the server as backup to sync the data.
You might be thinking right now on implementing Core Data and your NSManageObject entities – or if you are more into Realm your RLMObject entities –, initializing your database in the app delegate or through a singleton maybe so it will be available as soon as possible, but the truth is, you don’t need any of this yet, because your app doesn’t depend – and shouldn’t depend – upon which persistency mechanism you implement. Could be any of the above or even non of them, like in fact is now. The app should work the same without knowing where the information comes from, and here is where the magic of SOLID principles comes to scene.
This is what I would do in your case, I would start by thinking about what my project should expect from its persistency mechanism and define an interface that does exactly that. Don’t worry just yet about how the implementation should be, lets keep it abstract for now and write down what we need before going into the adventure of implementing it.
A basic swift interface might look like this. Let’s comment a little about it:
- Notice that we added all the CRUD methods to the same interface. Some of you might be thinking that this is a clear violation of the “S” and “I” principles, but because this a simple app and the requirements for this module aren’t that complicated, it would be easier and faster to implement this way. If in the future you need to take some of these responsibilities out of here and into its own interface, you will be always able to do it without any issues.
- Note that the object we are passing and receiving as “Task” is a simple struct and not a subclass of NSManageObject or any superclass of the persistency framework we are using. This might be the most important part of all, because it will allow us to keep things separate, that means, that our code will not know anything about how an object of the database might look like.
- And finally, just to mention that if you are working with Swift and you wish to have a better understanding of what goes wrong during each of this process, you could benefit from the Swift error handling and instead of invoking the failure callback with an error parameter, just throw the error and catch it when needed. Just make sure you do this in a DRY (Don’t Repeat Yourself) way.
Now getting back on track. This simple interface defines everything my app expects from a persistency mechanism, and is a good starting point for what’s coming next. But before we dive into the implementation of the connection between this interface and our code, lets declare a couple more of interfaces that will define the structure of our persistency module.
As mentioned on the second bullet point above, the interface is returning us a simple “Task” struct that is available throughout our app, this means that somewhere inside this module we need to map the persisted object into a “Task” object and that’s what we are going to define next.
Note: The third function is just a convience method that behave like the second one on it’s implementation.
Note that this will only be necessary if you require an object of a certain subclass inside this module, like a NSManageObject. The interface will allow us to implement a class that is only responsible for mapping the module object into our “Task” object and vice versa. This will allow us, as mentioned above, to keep things separate, whenever we cross the boundary of the persistency module we should only use the module object, and when we are getting back to our app, we should only return our main “Task” object.
Next we will need to define the interface for the class that will only be responsible for synchronizing the data with the server.
With these three interfaces we practically covered all we need to accomplish our goal. So the next step is proceeding with the implementation of the module and understand how to connect it with the rest of the app. To do so, I will take advantage of the “Repository” pattern to add another layer of abstraction to the module and to respect the single responsibility principle. This step will allow us to add the business logic to retrieve and sync the data in the repository and not inside our app.
As you can see the interface of the repository is pretty much the same as the Data Store’s, after all, this is just an extra layer of abstraction for it, the actual change will be in the implementation, where we should add the logic for sync the data.
With this in mind, let’s see how an implementation of this might look like. First I will go over the MV(X) architectures because of their similarities and present the VIPER one later.
In these MV(X) architectures we would add the dependency of this module in the middle level of each one of them, making the Controller, Presenter or ViewModel responsible for updating and retrieving the data from the dependency. Before going deeper on the implementation and the benefits of it, lets see first the VIPER diagram.
In VIPER the connection will be done by the Interactors, since they are the responsible for all the application specific logic (aka Domain Layer) and they know when to retrieve or update the data. The actual process of updating and retrieving the data will be done by our new module, which will be part of the business specific layer (aka Data Layer).
Now that we have a better image of how to connect this dependency on each architecture lets go and explain the benefits of it:
- Because we separated our Domain model from the Data model, the first one is completely independent from the second one, meaning that we can use it in the whole app without any worries. The dependency will need to map its own model into the one that our app recognizes and knows how to use in order to be able to work with us, making our app completely independent from any dependency class specific.
- Thanks to the use of the repository to abstract the implementation of the database and the server, we don’t need to take care of the sync process on each class where we modify the data because it’s all taking care off by the repository.
- Another benefit from the repository is that it also helps to separate the database from the server, meaning that a change in one of them won’t affect the other.
- We can add this dependency on every class that needs it and we will be reusing the code in each one of them, making it easier to refactor later on.
These are just a few benefits from this implementation and I’m sure you will be able to find more to add to the list. And as you can see, it doesn’t matter which architecture you use, it doesn’t matter either which language you use, I presented the examples for the interfaces using Swift but you could easily do it in another technology and it will still work the same, because the important part is that we applied the theory and good practices out there to achieve a better organized code.
At the beginning I mentioned that we were going to apply SOLID principles to do this, and if you stop for a minute and think, we actually applied them all:
- “Single Responsibility” is all over the module we created to solve this problem. Every object is responsible of only one thing. The exception might be the CRUD interface, but as I mentioned above, since the complexity of this application isn’t to high, we could merge them all together and if later on the complexity increases we can do a refactor to separate them and nothing will break.
- “Open/Closed” might not be clear in this example but suppose you add another type of task later on, you could easily extend your interfaces and main classes to add more functionalities without needing to modify any of your current classes.
- “Liskov Substitution” because you applied the S and the O you shouldn’t have any problems applying this.
- “Interface Segregation” is like the Single Responsibility, we created four specific interfaces to solve our problem, with the exception being the CRUD, but as mentioned above, if we needed to separate this into more more specific interfaces, there shouldn’t be any problems.
- Finally, “Dependency Inversion” we didn’t implement any classes in this example and yet you already know how the module will work, the responsibilities of every class and how they are connected together. Meaning that we successfully manage to depend upon abstraction rather than the actual implementation.
As you can see, by applying these principles and a common software design pattern we were able to implement the database and the server connection in our app without propagating all of this inside it. This way we can maintain our code base and all the dependencies separated, making it easy to refactor, maintain and improve in the future.
There aren’t any rules written in stone that we should follow to do this, because in the end, the only things that matters is that we write good quality code, following the standards and good practices that instead of giving us headaches, they make ours lives easier and improves our products.