Namespacing Swift code with nested types
While Swift does not yet feature a dedicated namespace
keyword, it does support nesting types within others. Let’s take a look at how using such nested types can help us improve the structure of our code.
Many Swift developers are used to do namespacing through including structural levels in the actual name of a type — using names like PostTextFormatterOption
(an Option
for a Text Formatter
used to format Posts
). This is probably because this was pretty much the only way to do sort of a “poor man’s namespacing” in Objective-C & C, and like many other conventions it has carried over to Swift.
Let’s use the type mentioned above as an example, and take a look at the implementations of Post
, PostTextFormatter
& PostTextFormatterOption
:
struct Post {
let id: Int
let author: User
let title: String
let text: String
}
class PostTextFormatter {
private let options: Set<PostTextFormatterOption>
init(options: Set<PostTextFormatterOption>) {
self.options = options
}
func formatTitle(for post: Post) -> String {
return post.title.formatted(withOptions: options)
}
func formatText(for post: Post) -> String {
return post.text.formatted(withOptions: options)
}
}
enum PostTextFormatterOption {
case highlightNames
case highlightLinks
}
Now let’s take a look at how the above types change if we instead structure them as nested types inside of Post
:
struct Post {
class TextFormatter {
enum Option {
case highlightNames
case highlightLinks
}
private let options: Set<Option>
init(options: Set<Option>) {
self.options = options
}
func formatTitle(for post: Post) -> String {
return post.title.formatted(withOptions: options)
}
func formatText(for post: Post) -> String {
return post.text.formatted(withOptions: options)
}
}
let id: Int
let author: User
let title: String
let text: String
}
One big advantage of the nested types approach is that we can now clearly see the structure and relationship between our types just by taking a quick look at our code. We have also reduced the verbosity in our initializer, making it shorter and easier to read (the options
argument is now simply of type Set<Option>
instead of Set<PostTextFormatterOption>
).
We now also get a clear sense of hierarchy at the call site — everything related to a Post
is now neatly structured under the Post
. namespace. Here’s what formatting a post’s text looks like:
let formatter = Post.TextFormatter(options: [.highlightLinks])
let text = formatter.formatText(for: post)
However, using nested types like above also has a pretty significant downside. The code is kind of “backwards” vertically, where the actual content of the parent type gets pushed all the way down to the bottom. So let’s try to fix that by flipping our structure — moving our nested types down to the bottom, instead of having them at the top (we also throw in some MARKs
, for good measure).
struct Post {
let id: Int
let author: User
let title: String
let text: String
// MARK: - TextFormatter
class TextFormatter {
private let options: Set<Option>
init(options: Set<Option>) {
self.options = options
}
func formatTitle(for post: Post) -> String {
return post.title.formatted(withOptions: options)
}
func formatText(for post: Post) -> String {
return post.text.formatted(withOptions: options)
}
// MARK: - Option
enum Option {
case highlightNames
case highlightLinks
}
}
}
Whether you prefer the nested types to be on the top or bottom is definetly going to be a personal preference. I kind of like how it lets me keep the actual content of the parent type at the top — while still giving the code the hierarchical benefits of nested types.
However, like with many things in Swift, it turns out there’s a couple of more ways to implement namespacing & nested types.
Nested types in extensions
One option is to use extensions to implement your nested types. This gives you a clearer separation between the types, while still retaining the hierarchy both in the implementation and at the call site.
Here’s what that looks like for our types:
struct Post {
let id: Int
let author: User
let title: String
let text: String
}
extension Post {
class TextFormatter {
private let options: Set<Option>
init(options: Set<Option>) {
self.options = options
}
func formatTitle(for post: Post) -> String {
return post.title.formatted(withOptions: options)
}
func formatText(for post: Post) -> String {
return post.text.formatted(withOptions: options)
}
}
}
extension Post.TextFormatter {
enum Option {
case highlightNames
case highlightLinks
}
}
Using typealiases
You can also add typealiases to the original code (that didn’t use nested types) to achieve a nested type-ish behavior. While this doesn’t give you the same level of hierarchy in the implementation, it does help reduce the verbosity — and it also provides the same benefits at the call site as when using nested types.
Here’s what that option looks like:
struct Post {
typealias TextFormatter = PostTextFormatter
let id: Int
let author: User
let title: String
let text: String
}
class PostTextFormatter {
typealias Option = PostTextFormatterOption
private let options: Set<Option>
init(options: Set<Option>) {
self.options = options
}
func formatTitle(for post: Post) -> String {
return post.title.formatted(withOptions: options)
}
func formatText(for post: Post) -> String {
return post.text.formatted(withOptions: options)
}
}
enum PostTextFormatterOption {
case highlightNames
case highlightLinks
}
Conclusion
Using nested types can help you create a really nice structure & hierarchies in your code, to help make it clearer what the relationships are between your various types — both in the implementation and at the call site.
However, depending on what technique you choose to implement them, you may face other challenges and side effects — so I think picking the technique depending on the situation becomes really important, in order to end up with a net win.
What do you think? Which one of the above techniques to you prefer for namespacing your code? Or have you found another one? Let me know, along with any questions or comments, on Twitter @johnsundell.
(Oh, and by the way, as of Swift 3.1 nested types can also be used inside generics! 🎉 …but more on that in an upcoming post 😉)
Thanks for reading! 🚀