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.

Roadrunner cartoon character on a thin rock ledge jumping up while saying "meep meep!" then speeding away

"beep beep!"

Come say "Hi!" on Twitter or Mastodon! Or don't. 😄

Did you find this article valuable?

Support Scott Smith Dev by becoming a sponsor. Any amount is appreciated!