Architecture With Nico

Software Architecture and Design for iOS.

Published on 14 December 2023

A word on naming (for iOS Engineers)

In the intricate dance of iOS development, one of the often-overlooked performers is the name we give to our code components. It's not just a string of characters; it's a crucial communicator of intent and functionality. So, let's talk about naming, especially for protocols and classes, and how a well-chosen name can be the unacknowledged hero of your codebase.

The Redundancy of "Protocol" in Protocol Names

Picture this: ClientsRepositoryProtocol. Does it sound familiar? It's a pitfall many of us stumble into. We already know it's a protocol – no need to shout it.

This is also somethimes a shortcut to avoid naming conflicts. For example, if you have a ClientsRepository class and a ClientsRepositoryProtocol protocol, you can use the same name for both. However, this is a code smell. If you have a class and a protocol with the same name, it's a sign that you're not abstracting the right thing. This is because a class and a protocol are two different things. A class is a concrete implementation, we need to be specific about what it does. A protocol is an abstraction, we need to be specific about what it represents. By removing the Protocol suffix, we are forced to think about what we're abstracting and also forces us to be specific in our implementation. You can think of this when you're tring to name your classes that conform to a protocol:

What kind of implementation is this? How does this class achieve the functionality described by the protocol?

Let's put some effort into thinking about what we're abstracting. Consider naming your implementations more concretely. For instance, if your protocol for dealing with clients, like ClientsRepository, implementations for it can involve using URLSession to fetch clients, name it URLSessionClientsRepository.

Now, that's clarity!

Maybe you have an implementation that uses Alamofire instead and you want to experiment between the 2. You can name it AlamofireClientsRepository. That's too much? You can name it NetworkingClientsRepository. The point is, you're being specific about what you're abstracting from and how you're implementing it.

The Pitfalls of "Impl" and Generic Naming

Now, about "Impl" – it's a quicksand of confusion. Names for classes like ClientsRepositoryImpl don't tell us much. The same goes for generic names like ProductHandler. Instead, aim for names that paint a clear picture – PackagingProductProcessor or NetworkingProductsRepository leave no room for ambiguity.

Let's take another step back. If you have a generic name for a class like class ClientsRepositoryImpl, it's an open invitation for any code to settle in. Want to add caching to that repository? With a generic name, you might be tempted to squeeze it in there. However, if it's named class URLSessionClientsRepository, you'll think twice before adding unrelated functionality. Try to inforce the Single Responsibility Principle (SRP) as much as possible, and naming is a great way to help do that.

Imagine navigating through a codebase with classes like ProductHandlerImpl, ClientManager or similar. – it's a wild ride. What does it handle? What does it implement? It's a mystery. Let's not make our code a mystery novel. Let's make it a clear, concise, and delightful read.

Let's take a look at an example of how can we take an unclear piece of code and improve it:

Unclear approach

protocol ProductHandler {
    func handleProduct(_ product: Product)
}

class ProductHandlerImpl: ProductHandler {
    func handleProduct(_ product: Product) {
        // Implementation details
    }
}

Improved approach

protocol ProductProcessor {
    func process(product: Product)
}

class QualityCheckingProductsProcessor: ProductProcessor {
    func process(product: Product) {
        // Implementation details specific to quality checking
    }
}

class PackagingProductsProcessor: ProductProcessor {
    func process(product: Product) {
        // Implementation details specific to packaging
    }
}

class ProductProcessorComposer: ProductProcessor {
    private let processors: [ProductProcessor]

    init(processors: [ProductProcessor]) {
        self.processors = processors
    }

    func process(product: Product) {
        processors.forEach { $0.process(product: product) }
    }
}

Abstract Conceptually, Name Accordingly

When we extract something to a protocol, let's not just focus on lines of code. Abstract conceptually – think about what that code represents. Let's take an example: instead of extracting the code for applying a discount, abstract the code to calculate the price altogether. Now, you can have implementations like DiscountedPriceCalculator, RegularPriceCalculator, or PremiumUserPriceCalculator. It's not just code; it's a narrative of functionality.

Check out this example and how we can improve it while also making it more extensible:

Unclear approach

protocol DiscountApplier {
    func calculateDiscount(to product: Product) -> Double
}

This limits us to only abstracting the discount functionality. What if we want to abstract the price calculation functionality as well? We would need to create another protocol, which would be redundant and confusing.

Improved approach

protocol PriceCalculator {
    func calculatePrice(of product: Product) -> Double
}

class DiscountedPriceCalculator: PriceCalculator {
    func calculatePrice(of product: Product) -> Double {
        // Implementation details
    }
}

class RegularPriceCalculator: PriceCalculator {
    func calculatePrice(of product: Product) -> Double {
        // Implementation details
    }
}

class PremiumUserPriceCalculator: PriceCalculator {
    func calculatePrice(of product: Product) -> Double {
        // Implementation details
    }
}

By abstracting to a PriceCalculator protocol, we encourage a more comprehensive approach to pricing. Now, we can have implementations for calculating the price of a product with or without a discount, for premium users, or for regular users. It's a more flexible and extensible approach, not to mention how much decoupled the calling code is from the implementation details (and how much easier it is to test).

Wrapping it Up

In the realm of iOS development, a name is not just a name. It's a guide, a storyteller, and a guardian against code chaos. As we navigate the complexities of our projects, let's not underestimate the power of a well-chosen name. By steering clear of redundancies, generic terms, and vague "Impl" suffixes, we pave the way for a codebase that's not just functional but also a pleasure to read and maintain.

So, the next time you find yourself typing out Protocol in a protocol name, or tempted to use "Impl" as a suffix, take a step back, think conceptually, and let your code speak a language of clarity and purpose. It might just be the key to unlocking a more maintainable and delightful iOS development journey. Happy coding!