This article is also available in Chinese.

Refactoring is continual process. However, this process needs to leave the code in a functional state at frequent intervals. Refactorings that don’t stay functional can’t be deployed regularly, and it’s harder for them to be kept up-to-date with the rest of your team’s code.

Some refactorings are tougher than others, though. The nature of singletons in particular causes them reach their tendrils into many different objects, and this can make them difficult to remove from your project.

Lots of singletons, especially the poorly named ones, have a tendency to accumulate unrelated behaviors, data, and responsibilities, simply because its easier to put them there than anywhere else.

If you want to break up a far-reaching singleton, or if you want to be able to test code that uses that singleton, you have a lot of work to do. You want to slowly replace references to your singleton with smaller, better objects, but you can’t remove the singleton itself until you are completely finished, since other objects rely on it.

Worst of all, you can’t extract a singleton’s behavior and methods into another object, because they’re reliant on the shared state within the singleton. Put another way, if the singleton didn’t have any shared state, you could just make a new instance at each call site and your problem would go away instantly.

So we have a singleton with many disparate responsibilities and a bunch of shared state, being touched from many parts of your app. How can we remove this singleton without actually removing it?

We need a new way to refer to our singleton: a view on the singleton that represents a slice of its many responsibilities, without actually changing the structure of the singleton itself. This slice of behavior and data can be represented with a protocol.

Imagine this kind of singleton in a hypothetical shopping app:

class SessionController {

	static let sharedController: SessionController

	var currentUser: User
	
	var cart: Cart
	
	func addItemToCart(item: Item) { }
	
	var fetchedItems: [Item]
	
	var availableItems: [Item]
	
	func fetchAvailableItems() { }
}

This singleton has at least three responsibilities. We’d love to break it up, but dozens of classes from all over the codebase refer to the properties and functions on this object. If we make a protocol for each “slice” of responsibility, we can start breaking this up.

protocol CurrentUserProvider {
	var currentUser: User { get }
}

protocol CurrentCart {
	var cart: Cart { get }
	
	func addItemToCart(item: Item)
}

protocol ItemFetcher {
	var fetchedItems: [Item] { get }
	
	var availableItems: [Item] { get }
	
	func fetchAvailableItems()
}

The SessionController can conform to these protocols without any extra work:

class SessionController: CurrentUserProvider, CurrentCart, ItemFetcher {
	//...

Because of Swift’s protocol extensions, we can move anything that relies purely on the things provided in the protocol into the extension. For example, availableItems might be any items in the fetchedItems array that has a status of .available. We can move that out of the singleton and into into the specific protocol:

extension ItemFetcher {
	var availableItems: [Item] {
		return fetchedItems.filter({ $0.status == .available })
	}
}

By doing this, we begin the process of slimming down the singleton and extracting irrelevant bits.

Now that we have these protocols, we can begin using them around the app. A fine first step is to extract any usage of the singleton into an instance variable:

class ItemListViewController {
	let sessionController = SessionController.sharedController
	
	//...
}

Next, we can change its type to the type of a specific protocol:

class ItemListViewController {
	let itemFetcher: ItemFetcher = SessionController.sharedController
	
	//...
}

Now, while the class technically still accesses the singleton, it’s done in a very limited way, and it’s clear that this class should only be using the ItemFetcher slice of the singleton. We’re not done, however. The next step is intialize this class with an ItemFetcher:

let itemFetcher: ItemFetcher

init(itemFetcher: ItemFetcher) {
    self.itemFetcher = itemFetcher
    super.init(nibName: nil, bundle: nil)
}

Now, the class has no idea what kind of ItemFetcher it was intialized with. It could be the singleton, but it could also some other type! This is called dependency injection. It lets us inject alternate dependencies into our view controllers and other objects, which will let us test these objects more easily. The places where this view controller is initialized across the app have to be updated to use this new intializer.

Ideally, view controller initalization is only done inside of Coordinators, which should simplify how much you need to pass those singletons around. If you don’t use coordinators, any dependency that the fourth view controller in a navigation controller might need has to be passed through the first, second, and third view controllers as well. This is not ideal.

Now, this is the hard part: this process has to be repeated across every reference to this singleton across your app. It’s a tedious process, but it’s mostly rote work, once you’ve figured out what the various responsibilities and protocols are. (One tip: if you have a CurrentUserProvider, you might want a separate MutableCurrentUserProvider. The objects that only need to read from the current user don’t need to have access to write to that storage as well.)

Once you’ve changed all your references to your singleton, you’ve removed a lot of its teeth. You can remove its static singleton accessor property, and then view controllers and other objects will only be able to play with the objects that they are passed.

From here, you have a few options. You can now easily actually break up the singleton itself into all of the little responsibilities that you’ve created.

  • You can make ItemFetcher a class instead of a protocol, and move all the code from SessionController into the new ItemFetcher class, and pass the class around instead of the singleton.
  • You can keep ItemFetcher as a protocol, and make a class called ConcreteItemFetcher, and move the singleton code into this new class. This solution gives you a few more options, since you can inject other objects that conform to the ItemFetcher protocol, suitable for unit testing, screenshot testing, demoing the app, and other purposes. This is a little more work, but it also has more flexibility.

By creating “slices” of responsibility on top of your singletons, you can break them up into their constitutive responsibilities without changing the structure of the singleton itself. From there, you can use dependency injection to give objects only those things that you expect them to use. Finally, you can make your singletons no longer have singleton accessors, and ride off into the glorious sunset of well-factored code.