Effectively using static and class methods and properties
Published on: December 7, 2019Swift allows us to use a static prefix on methods and properties to associate them with the type that they’re declared on rather than the instance. We can also use static properties to create singletons of our objects which, as you have probably heard before is a huge anti-pattern. So when should we use properties or methods that are defined on a type rather than an instance? In this blog post, I’m going to go over several use cases of static properties and methods. Once we’ve covered the hardly controversial topics, I’m going to make a case for shared instances and singletons.
Sometimes you'll run into a Swift Concurrency errors stating that your static vars aren't concurrency safe. In this post we explore how you can solve this and why.
Using static properties for configuration
A very common use case for static properties is configuration. All over UIKit
you can find these properties whose sole purpose is to configure other objects. The main reason they are defined as static strings is quite possibly because making them static provides a namespace of sorts. A common example I like to use for explaining static properties for configuration is when you use objects with static properties as a style guide. For example, it’s much nicer to define colors or font sizes in a single place than having their values scattered throughout your code. When you want to change a certain color or font size across your app you would have to go through your entire app and then replace the appropriate values. If this is the approach you take it’s only a matter of time before you forget one or more properties. Now consider the following configuration object:
enum AppStyles {
enum Colors {
static let mainColor = UIColor(red: 1, green: 0.2, blue: 0.2, alpha: 1)
static let darkAccent = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1)
}
enum FontSizes {
static let small: CGFloat = 12
static let medium: CGFloat = 14
static let large: CGFloat = 18
static let xlarge: CGFloat = 21
}
}
If you have to slightly tweak your main color, you only have to change a single place in your code. By naming the color using a descriptive name, your properties are not bound to the visual representation they end up having on-screen.
Note that I’m using an enum with static properties here. The reason for that is because I don’t want to be able to create instances of my configuration objects. And since enums don’t have initializers they are well suited for this purpose. A struct with a private initializer would do the trick as well, I just happen to think enums are nicer for this job.
Static properties also make sense if you want to define a finite set of strings that you might want to use for notifications that you’re sending through the Notification Center on iOS, or for any other time where you want to have some kind of global configuration in your app that should be constant across your entire app without having to pass around a configuration object.
Using static properties for expensive objects
Another use of static properties is to use them as a cache. Creating certain objects in your app might be quite expensive, even though you might be able to create an instance once and reuse it throughout the lifetime of your app. A good example of an expensive object that you might want to keep around in a static property is a dateformatter:
struct Blogpost {
private static var dateFormatter = ISO8601DateFormatter()
let publishDate: Date?
// other properties ...
init(_ publishDateString: String /* other properties */) {
self.publishDate = Blogpost.dateFormatter.date(from: publishDateString)
}
}
No matter how many times we want to create a blogpost instance, the date formatter we use to convert the string will always be an ISO8601DateFormatter
, we can create a static property on Blogpost
that holds the date formatter we need. This is useful because date formatters are expensive to create and can be reused without any consequences. If we would associate the date formatter with an instance of Blogpost
rather than making it static and associating it with the type, a new date formatter would be created for every instance of Blogpost
. This can lead to many identical dateformatters being created which is pretty wasteful.
So any time you have objects that are expensive to create, and that can be safely reused many times, it might be a good idea to define them statically so you only have to create an instance once for the type rather than creating a new expensive object for every instance that uses said expensive object.
Creating a factory with static methods
A common pattern in programming is the factory pattern. Factories are useful to create complex objects using a simple mechanism while hiding certain details about the target object’s initializer. Let’s look at an example:
enum BlogpostFactory {
static func create(withTitle title: String, body: String) -> Blogpost {
let metadata = Metadata(/* metadata properties */)
return Blogpost(title: title, body: body, createdAt: Date(), metadata: metadata)
}
}
What’s nice is that we can use this BlogpostFactory
to create new instances of Blogpost
. Depending on your use case you might not want to build factories this way. For example, if there is some kind of state associated with your factory. In simple cases like this, however, it might make sense to have a simple static method to create instances of an object on your behalf. Another example is using a default
static method on a type to create some kind of basic starting point for a blog post or form:
extension Blogpost {
static func sensibleDefault() -> Blogpost {
return Blogpost(title: "Hello, world!",
body: "Hello, sample body",
createdAt: Date())
}
}
You could use this default()
static method to create a placeholder object whenever a user is about to create a new blog post.
Static methods are useful if you want to associate a certain method with a type rather than an instance. Nothing stops us from creating a free function called defaultBlogpost()
that creates a blog post instance. However, it’s much nicer to associate the default()
method directly with Blogpost
.
Understanding how class methods differ from static methods
In the previous examples, I always used static methods. In Swift, it’s also possible to define class methods on classes. Class methods are also associated with types rather than instances, except the main difference is that subclasses can override class methods:
class SomeClass {
class func date(from string: String) -> Date {
return ISO8601DateFormatter().date(from: string)!
}
}
class SubClass: SomeClass {
override class func date(from string: String) -> Date {
return DateFormatter().date(from: string)!
}
}
In the preceding example, SubClass
overrides the date(from:)
from its superclass so it uses a different date formatter. This behavior is limited to class methods, you can’t override static methods like this.
Understanding when to create shared instances
So far you have seen that you can use static properties for configuration, or expensive objects and how you can use static methods to create simple factories. A more controversial topic is the topic of shared instances. We have probably all used at least a couple of the following examples in our code at some point:
URLSession.shared
UserDefaults.standard
NotificationCenter.default
DispatchQueue.main
These are all examples of shared instances.
Each of the above objects has a static property that holds a default, or shared instance of the type it’s defined on. If you think of these objects as singletons, you are mistaken. Let me explain why.
A singleton is an object that you can only ever have one instance of. It’s often defined as a static property in Swift, but in other languages, you might simply use the types initializer and instead of getting a new instance every time, you would get the same instance over and over.
The static properties on the types listed earlier are all shared instances rather than singletons because you are still free to create your own instances of every object. Nothing is preventing you from creating your own UserDefaults
store, or your own URLSession
object.
The shared instances that are offered on these objects are merely suggestions. They are fully configured, useful instances of these objects that you can use to quickly get up and running. In some cases, like DispatchQueue.main
or NotificationCenter.default
, the shared instances have specific purposes in your app. For example like how DispatchQueue.main
is used for all UI manipulations, or how NotificationCenter.default
is used for all notifications sent by UIKit
and the system.
Whenever you use a shared instance, try to immediately add a built-in escape hatch for when you might decide that you want to use a different instance than the shared one. Let me show you an example of how you can do this:
struct NetworkingObject {
let urlSession: URLSession
init(urlSession: URLSession = URLSession.shared) {
self.urlSession = urlSession
}
}
The NetworkingObject
uses a URLSession
to make requests. Its initializer accepts a URLSession
instance and has URLSession.shared
as its default value. This means that in most cases you can create new instances of your networking object without passing the URL session explicitly. If you decide that you want to use a different URL session, for example in your unit tests, you can simply pass the session to the networking object’s initializer and it will use your custom URL session.
Shared instances are very useful for objects that you’ll likely every only need a single instance of, that is preconfigured and easily accessible from throughout your app. Their main benefit is that they also allow you to create your own instances, which means that you get the benefits of having a shared instance with shared state without losing the ability to create your own instances that have their own state if needed.
Knowing when to use a singleton
Singletons are universally known as an anti-pattern throughout the development community. I personally tend to prefer shared instances over singletons because with a shared instance you can still create your own instance of an object if needed while using the shared one when it makes sense to do so.
I do think, however, that there are responsible ways to use the singleton pattern in Swift. Given that a singleton’s only real requirement is that you can only ever have one instance of a singleton, you might write some code like this:
protocol Database {
/* requirements */
}
struct AppDatabase: Database {
static let singleton = AppDatabase()
private init() {}
}
struct UserProvider {
let database: Database
}
When used as described above, the singleton conforms to a protocol. In this case Database
. The object that uses the database has a property that requires an object that conforms to Database
. If we don’t access the singleton’s singleton
property when we access the database but instead inject it into the UserProvider
and other users of the database, the singleton is used like any other dependency.
So why make AppDatabase
a singleton? You might ask. The reason is simple. If I have two instances of my database, it might be possible for two objects to write to my underlying storage at the same time if I don’t have a very good read/write mechanism in place. So to make sure that you can only ever create one instance of AppDatabase
you can implement it as a singleton.
The major drawback to this approach for me is that this might encourage people to use the singleton property even though they should be using dependency injection to inject the singleton instance to hide the fact that we’re using a singleton. This is what code reviews are for though, and if everybody on your team agrees that it’s okay to use singletons like this you can go ahead and do it.
Keep in mind that singletons are still an anti-pattern though, all I provided here is a use case where I think the downsides are limited and isolated.
In summary
In this post, I showed you how you can use static properties to drive configuration on a type-level rather than an instance level. I also showed you that static properties are great for storing objects that are reused often and are expensive to create. Next, you saw how static methods can be used to implement a factory of sorts and how class methods are different from static methods.
Then we took a turn into a more controversial realm by exploring shared instances and singletons. I argued that shared instances are often nicer than singletons because you can still create your own instances of objects that offer a shared instance if needed. I then showed you that a singleton might not be so bad if you make it implement a protocol and inject the singleton into the initializer of an object that depends on a protocol rather than the singleton’s explicit type.
If you have any feedback, suggestions or if you want to talk to me about singletons and shared instances, my Twitter for you.