Writing maintainable code should be one every Software Engineer ought to be conscious of because it pays off in the long run. Often times, in a bid to get things done quickly, we tend to repeat ourselves. This usually involves copying and pasting our similar implementation of maybe a function and adjusting to suite what we want.
Duplication to some extent adds a bit of complexity and clogs readability. A good approach would always be to adopt the DRY(Don’t Repeat Yourself) principle as much as we can. In our code, it is very important we give good thoughts on how we design our API and the app architecture we adapt as it impacts on the maintainability of the code at the long run.
So, back to the topic of discuss. Generics in Swift. With Generic code, we can write reusable functions and data types that can work with any type that matches the constraints we define. The aim is to factor out shared functionality and reduce boilerplate code.
We have constrained and unconstrained generics. For unconstrained generics, we can’t really do much with them. Constrained generics are expressed with protocols. Protocol as we know, declares a set of requirements on their conforming types.
At this point, lets dig deeper into the real issue with concrete examples. Enough of the explanation huh? Yeah 🤓😃 lets do this.
We will be using Xcode Playground. So, feel free to test out the code in the playground as we move along. We will be starting with unconstrained generics but we won’t waste time on it because we can’t really do much with unconstrained generics.
The parameterized function above can take input of any type. We can copy the input value to a local variable and return it from the function. We can also pass the function into another function. That is just the much we can do with this unconstrained generic function.
So, lets look at the constrained generic function now. First, we will do the normal implementation without the use of generics. Then, we will use generics to factor out shared functionality and reduce our boilerplate. Our example will be fetching data from the network and decoding the
json response with the help of
We will be fetching data from https://jsonplaceholder.typicode.com/ which contains dummy data API for our testing.
On a close look at the functions above, we can easily see that both
fetchComments functions have great things in common. The only difference between them lies on different
Comment model objects that conforms to the decodable protocol we passed to
JSONDecoder. Hence, this is a nice place we can apply generics to avoid code duplication.
Note: We are fetching data synchronously from the above functions. In production code, it is advisable to fetch data asynchronously to avoid freezing the UI while waiting for the download to complete.
So, lets go ahead to extract a generic function we can easily reuse without repeating ourselves.
So with generics, we have been able to extract shared functionality between the two functions. Thus, making
loadResources reusable. We constrained the function to decodable protocol so as to help us decode the json data based on the model object type passed through the completion handler from the user specific functions
loadComments respectively. The compiler would be able to infer at run-time, the parameter type based on the types we passed to the completion handler.
One more thing before we wrap up on generics, is to have a look at generic types or data types. We will implement a generic type to perform the same operation of fetching data from the network but this time, we would be performing it both synchronously and asynchronously, using structs. Just like functions, structs can also be generic.
From the above, we have managed to reduce
commentResource instances to describe where the resources are located . We have fully separated out the network part from it. Hence, with this approach, testing becomes simpler because we can easily test the instance of the struct to know if it is fully formed.
From the above code, we also have two functions, one that performs the loading of resources synchronously and the other that performs the operation asynchronously. The difference between the two basically, aside from using URLSession async API, is the use of the annotation keyword
@escaping in the async function. The usage is to ensure that the completion handler escapes the method scope because we are dealing with asynchronous request.
So, finally, we have been able to explain how generic functions and types are implemented in Swift. I know right now you are feeling happy you have learnt something new today. YEAH 😀😃 we did it together.
If however, you have any concerns or questions, feel free to drop a comment below. You can also drop me a message on twitter.