Encapsulate and Generalize in Swift
An approach to depending less on your dependencies
Whenever I use a 3rd-party dependency in my app, I try my best to import
that dependency only one single time in the entire project. I also never use the actual dependency name in my variable names, class names, or any other names. In other words, I encapsulate and generalize.
Come along if you'd like to see what I mean. 😊
Let's start with "this works" (and then we'll improve it)
(To be clear, the next few code snippets are not the final result. They'll be improved down below.) Here's an example "service" class whose job is to use APIs from AcmeSupport to implement customer support features directly in your app ("AcmeSupport" is a fake 3rd-party dependency name I thought of because I have Roadrunner cartoons on my mind).
import AcmeSupport
class AcmeSupportService {
...
func fetchAcmeTickets() -> async [AcmeTicket] {
// Call into the AcmeSupport API here
...
}
}
As you can see, our method calls into Acme's API and returns an array of AcmeSupport's own type called AcmeTicket
. It looks like we could use that type nicely, so we'll go ahead and use this service class in some other part of the app, like this:
import Foundation
import AcmeSupport
class DataLogicPlace {
...
private let acmeService = AcmeSupportService()
...
func loadAcmeTickets() async -> [AcmeTicket] {
let acmeTickets = await acmeService.fetchAcmeTickets()
// Here you can minpulate the `AcmeTicket` objects
// from the `acmeTickets` array
...
}
...
}
It's possible you may want to do other things with the AcmeTicket
type from the acmeTickets
array result that could require you to import AcmeSupport
. So the DataLogicPlace
class imports it. And since we want to display these tickets in our UI, we can even use these AcmeTicket
types right inside our View
:
import SwiftUI
import AcmeSupport
struct AcmeSupportScreen: View {
...
var body: some View {
...
ForEach(acmeTickets) { acmeTicket in
acmeTicketRowView(forAcmeTicket: acmeTicket)
}
...
}
private func acmeTicketRowView(forAcmeTicket: AcmeTicket) -> some View {
...
}
}
And for this View
(i.e. "Screen" 😉), we have to import AcmeSupport
so we can access its AcmeTicket
type to use as the argument type in our acmeTicketRowView
method.
All of this code may already look great to you. And that's totally OK. This works. As planned though, we'll improve it all by encapsulating and generalizing. Readysetgo.
Encapsulate
The goal here is to only import AcmeSupport
once in the app so that there's only one file that depends on it, and the rest can compile happily without it. To do this, let's create our own version of AcmeSupport's type AcmeTicket
and we'll name ours something generalized like "SupportTicket
" (foreshadowing, anyone? 😄)
struct SupportTicket: Identifiable {
...
}
Cool! Let's use our new type right away so you can see the effect it will have!
import AcmeSupport
class AcmeSupportService {
...
func fetchAcmeTickets() -> async [SupportTicket] { // 👀
// Call into the AcmeSupport API here
// ✅ THEN map THEIR type to our OWN type to return 😃
...
}
}
Notice in the snippet that we're now returning an array of SupportTicket
instead of AcmeSupport's type AcmeTicket
. We do this by quickly mapping their type to our own type and walking away. This is really great, you know why? Because now all the other files in our app that were previously consuming an array of the AcmeTicket
type will now be consuming an array of our very own SupportTicket
type—which meeeeans all those files can immediately deleteimport AcmeSupport
because they won't need to access AcmeSupport's AcmeTicket
type anymore! And just like that, we've successfully encapsulated the AcmeSupport dependency and its API calls into one single place in our app. Party time? ...Yeah. Party time. 🥳
Generalize
Alright, in the original 3 code snippets, if you count the number of times the word "acme" appears in the code—excluding the import
statements and the AcmeTicket
type name—it comes out to around 15 times (and that's only in 3 tiny snippets representing 3 tiny files).
So the goal now is to generalize all of our naming (don't worry, I'm not talking about Swift generics 😅) so that it doesn't mention the name of the company, "Acme", anywhere. If we do this, the code instantly takes on a totally different feeling. Check it out:
import AcmeSupport // ⬅️ imported only once, right here
class SupportService {
...
func fetchTickets() -> async [SupportTicket] {
...
}
}
import Foundation
class DataLogicPlace {
...
let service = SupportService()
...
func loadTickets() async -> [SupportTicket] {
let tickets = await service.fetchTickets()
...
}
...
}
import SwiftUI
struct SupportScreen: View {
...
var body: some View {
...
ForEach(tickets) { ticket in
rowView(forTicket: ticket)
}
...
}
private func rowView(forTicket ticket: SupportTicket) -> some View {
...
}
}
And would you look a that! We've successfully generalized our naming, bringing the number of times we see "acme" in our naming from ~15 all the way down to zero! All without compromising the readability of the code. In fact, I'd say the readability has improved! Mission: accomplished.
Is this really necessary?
You decide. Over the years, I've seen lots of code where devs tightly couple 3rd-party dependency names and their types to the rest of the codebase, making it unnecessarily intertwined and dependent on some other thing/company. And then the Product team asks the devs to a/b test 2 different dependencies/companies, or even completely replace a dependency with a different one that offers the same functionality as the original but perhaps at a better rate. Well, in these situations, if your code looks like the original 3 snippets in this article, then you'll have a lot of naming-refactoring ahead of you, and likely a lot of logic-refactoring, and also type-replacing, like in that original acmeTicketRowView
method above that depended on the 3rd-party AcmeTicket
type.
So if we thoughtfully encapsulate and generalize from the get-go, then our code can be a lot more pleasant to work with; scaling will be easier, and we'll be more likely to code more confidently when we need to do something drastic like completely replaceAcmeSupport
with BeepBeepSupport
.
"beep beep!"