When should one use swift extensions? [how to avoid over using it?]

I find that it is best not to impose any kind of dogma on the use of extensions or separate files. Group things in a way that makes sense to you - do what feels right. It's not permanent; you can always change it later. That applies to most things, actually; it's not always best to follow strict rules decided by somebody who doesn't even know your project or team members.

One thing that I will note about the "one type per file" rule is that, in my experience, the additional effort can discourage developers from creating new types for things (especially simple wrapper types). That's a shame; types can be useful modelling tools. I generally find it better not to impose such a rule, so as to keep the burden of introducing new types as low as possible.

If a particular type grows in complexity and it is possible to reason about it independently, we can decide to split it off in to a separate file. But that is done on a case-by-case basis rather than as a blanket rule.

12 Likes

@hgraves @DavidSReich @WeZZard @Karl @taylorswift @tera
I feel like we took this discussion too much to the "where do declare it?" point of view, rather than "when to use it?"
I want to share with you another concrete example:
a project contains 3 modules/targets: Core, Views, Mobile.
while Core defines the networking and models data etc..
Views is the pure UI components and Mobile is the designated mobile logic and screens.
Now please consider the next diagram used in the project:

In a nutshell: MyAppModel is extended inside Mobile so it can be used "easily" in myView component that resides in Views.

and I am asking, is this the "poet intent"?

  1. why not to initiate another intermediate class that will adopt myProtocol and use it?
    As the author of MyAppModel ,I don't want the extended fields to show up as available to other developers just because someone decided to extend it...

  2. regardless, if MyAppModel should have more fields, why not to add them to the original object in the first place?

It just feels like extension should be used more in scenarios where one have no access to original object it wishes to extend, and less in scenarios when you are lazy....

The general rule of thumb should be: use extensions to minimise changes to existing stuff, which implies putting extensions in separate files. This also makes reviewing changes much easier.

1 Like

The author of the "Mobile" module uses their best judgement and knowledge of the needs of downstream clients to expose the best interface for them. As you have pointed out, they could simply wrap the upstream type and defeat any technical or policy restrictions on extensions anyway - so for the client (the "Views" module) there is no practical difference either way. The only difference is that the "Mobile" module would have to duplicate a large part of "Core"'s API so it can vend its wrapped types everywhere (e.g. returning a MobileAppModel instead of a MyAppModel).

The stream flows around an obstacle, and the "Core" module's reward for frustrating their users and lowering their productivity is that they will become marginalised, and the API they were so protective of will become less relevant.

Then there is the question of whether it is appropriate to give the author of MyAppModel this much control over users of their type, and I would suggest that it isn't. The phrase that you used, "just because someone decided to extend it", illustrates why - that someone is not anyone. They are not strangers behaving randomly; they are the authors of a module which is actually closer to the ultimate client than the "Core" module is. Their job is precisely to add domain-specific expertise for the benefit of the "View" module, so the authors of "Core" really have no business second-guessing their judgement or putting up barriers in order to frustrate them.

In other words:

I don't want the extended fields to show up as available to other developers

Sorry, but you don't get to decide that.

If this were a problem in an organisation, it would seem to suggest a lack of trust between colleagues. Perhaps they should reflect on how they can better work together. I don't think coding standards or technical best practices will solve it.

Developers split their code in to modules along a variety of axes and for a variety of reasons. It can easily happen that the basic type definition needs to live in one module, but some non-essential functionality lives in a downstream extension.

  • Perhaps "Core" has several clients, and the additional API would be misleading in other contexts, or
  • Perhaps it makes use of large or platform-specific dependencies which those other clients would like to avoid, or
  • Perhaps it uses types defined in "Mobile" and cannot be defined in "Core", or
  • Perhaps it's just convenient for the authors of "Mobile" and is not (yet) useful to anybody else.

Of course, those factors can change over time, so an API can start as an extension in a downstream module and later move up to a parent module. Happens all the time.

4 Likes

i think there is an important difference between extensions of generic types and extensions of non-generic types. when you have a really general-purpose generic struct like Pair<T> and everyone is adding convenience API where T is substituted with some type someone wants to sugar, then the namespace of the generic type can get quite cluttered. IDE autocomplete can use knowledge of the local type substitutions to filter out irrelevant extensions, but ā€œbirds-eyeā€ tooling like documentation codeces cannot.

I donā€™t like using extensions to organize a type (i.e. a way to mimic C#ā€™s partial class). Iā€™ve gotten the impression thatā€™s a fairly unpopular opinion among Swift developers. I make no attempt to enforce my preference in teams I work on but it dictates how I introduce them.

In my mind there is a clear distinction between core functionality of a type and extensions, which are syntactic sugar for method call notation (i.e. thereā€™s a global function doThisThing that takes an X as its first parameter, and for various reasons I prefer writing thisX.doThisThing(ā€¦) instead of doThisThing(thisX, ā€¦)). The distinction has nothing to do with making it easier to read the source code of the type. It has to do with whether a capability is essential to the type (it would be incomplete without it) or not.

This usually is equivalent to a simple and easy to verify rule: an essential capability accesses the non-public members of a type, extensions access only public members. This isnā€™t set in stone, there are exceptions, but the exceptions are exceptional. Correspondingly, if it were up to me, I would not have allowed extensions in Swift to access non-public members, regardless of where the extensions are defined (and itā€™s not up to me, so again I donā€™t expect other people to do what I do and pretend this rule exists). Among other things, following this rule makes it easier to promote a concrete type to a protocol later.

Why donā€™t I like using extensions to organize a typeā€™s core functionality into sections? Not only does that feel to me as a misuse of the concept (the type isnā€™t being ā€œextendedā€, itā€™s being ā€œcompletedā€), more importantly itā€™s a big red flag that the type is too big. What Iā€™d rather do is take each of those sections that have been put into extensions, and refactor them into smaller types. This allows the instance members relevant only to the methods in that extension to be placed with those methods, and even better to be encapsulated from the rest of the type. This, to me, is a wholly superior form of organization that actually draws more encapsulation boundaries, improves cohesion and raises the abstraction level. If reshaping a type into extensions is a preparation for doing this, thatā€™s totally fine.

As soon as a type has so much code in it (core functionality code that accesses non-public members) that anyone would want to start sectioning it off, thatā€™s when I want to start breaking it into smaller types entirely.

6 Likes

I love your answer and I agree with every word you wrote. To me, extensions in Swift are overused and abused. In my mind extensions should allow you to extend a type with new capabilities, ideally - extend a type that you're not the owner of and don't have access to. An arguably not a great example might be giving an optional string extension that will return empty string ("") if nil. Such an extension is project independent and can be pulled out and placed again in a different project with no issues.

Instead what I see are so many examples where it's just used to "structure" the code. Things that should reside in the type's body are just scattered in files and it makes it harder to understand where things are coming from. I do wish there were more strict guidelines as there is much to be desired.

4 Likes

I think extension should be declared at fileprivate access level by default. Possible contamination should be explicitly declared when an internal or higher access level is desired.

My opinion about thisā†‘.....
I assume that all accessible resources are well thought out.
If a developer makes a member public or open, the developer should allow it to be extended. And that's not too much to ask for a language feature that can only extend computed properties and functions.

Yes. And common sense.

If you don't like extensions and don't need to use them, just don't use them. But sometimes you have to. For example you may need to extend a type which source code is unavailable to you.

Nothing prevents any developer to turn their code into a mess, even without extentions. Just use the boundaries that work for you.

Oh, sorry. I started to reply to old posts again. When someone revives an old topic, I read the entire thread from the beginning under the impression that the topic is new.