Wayfair Tech Blog

Thoughts on Dependency Management at Wayfair

IMG_5905_Fotor

Introduction

Dependency injection has always been a hot topic for debate. At Wayfair, we have always chosen the simpler approach: passing the parameters right into the initializer. While this approach was easy to follow, scalability became a concern once the app started growing to a complicated machinery.

Some examples picked up by our SonarQube:

(Just look at this massive dependency bundle we created for one of our classes makes my head spin, I think I need some tea!)

class DependencyBundle {
...
init(dataBundle: SomeDataClass?,
someService: WebService,
displayManager: SomeDisplayManager? = nil,
moreService: SomeService =
Dispatcher.currentAppTarget().someManager.someService,
displayTypeManager: SomeOtherDisplayManager? = nil,
someCustomGraphics: Graphics? = nil,
optionManager: SomeOptionManager? = nil,
somePaymentManager: PayManagerProtocol? =
PaymentManager.sharedInstance,
addToCartManager: AddToCarProcessable =
AddToCartManager.sharedInstance,
trackingDelegate: SomeTrackableDelegate? = nil,
featureTogglesRepository: FeatureToggleRepository? = nil,
displayType: DisplayType = .default,
trackingManager: TrackingManager = TrackingManager.shared,
childViewProvider: SomeViewProviderProtocol = SomeCardView(),
loggingManager: LoggingManager = LoggingManager.shared,
customCartButtonAdapter: NavigationBarConfigurationDelegate =
CustomCartButtonAdapter(),
anotherConfigurationDelegate: NavigationBarConfigurationDelegate =
CustomCartButtonAdapter(),
storeId: StoreID =
SomeConfigManager.sharedInstance().storeConfig.storeID,
notificationRegistrationManager:
NotificationRegistrationManagerProtocol =
NotificationRegistrationManager.sharedInstance,
tracingClient: TracingClientProtocol =
ApplicationDelegate.sharedTrace,
statusMessageHandler: WFStatusMessageHandler =
WFStatusMessageHandler.shared,
replaceProduct: RegistryItem? = nil,
productCollectionDelegate: ProductCollectionDelegate? = nil)

The Problem

While certainly, we are not arguing ditch initializing parameters, we should re-consider passing Singletons around in it! 

In the example I showed you above, we defined DependencyBundle and it’s used by SomeContainer. SomeContainer requires DependencyBundle but doesn’t actually use most of those dependencies (if it does you have another problem of certain classes being too powerful!) Those dependencies simply get passed down to the children stacks. 

What’s the issue you might ask? Well consider these points: 

  1. Any class that tries to use SomeContainer is required to provide a massive list of singletons, this burden cascades up the call hierarchy and suddenly even classes that are very far removed from SomeContainer require dependencies that it doesn’t use. 😱
  2. Reuse-ability is reduced to near zero as all the child classes to SomeContainer is tightly coupled with DependencyBundle and it’s now a mess to reuse any child classes to  SomeContainer if you don’t have an instance of DependencyBundle 😱

The Proposal

At Wayfair we are keen on building our own tools, so we came up with a simple 2 piece system for a proposed solution. 

A resolver interface: It accepts requests for dependencies. It acts as the interface between the dependencies and the class. 

A set of providers: Providers are creators of the dependencies. They create the dependencies and give it to the resolver when asked. Some providers can even depend on other providers to create a robust dependency graph.

We’ll create our own property wrapper @Injected to automatically resolve the dependencies when the property is declared.

Show me the Money

While there are several approaches to the implementation of the resolver/provider protocols, this is how we envisioned this should work.

First, let’s set up our contracts via protocols and property wrapper.

protocol Injectable {
///Name is used to look up an injected entity
static var name: String { get }
}

extension Injectable {
static var name: String {
return "\(Self.self)"
}
}

///Resolver's main job is to resolve request to access entities
protocol Resolver {
init()
func setup()
func resolve(name: String?) -> Injectable?
}

///Provider's main job is to provide entities to resolver
protocol Provider {
associatedtype T: Injectable
func provide() -> T?
}

@propertyWrapper
struct Injected<T: Injectable> {
private var target: T?
public var resolver: Resolver?
public init(resolver: Resolver? = SimpleArrayResolver.shared) {
self.resolver = resolver
}
public var wrappedValue: T? {
mutating get { //Step 1
if target == nil {
target = resolver?.resolve(name: T.name) as? T
}
return target
}
mutating set { target = newValue }
}
}

Now let’s implement a simple provider and a resolver. We’ll simplify a few things here as an example:

    

final class DisplayManagerProvider: Provider {

typealias T = DisplayManager

func provide() -> T? {
DisplayManager.shared //Step 3
}
}

final class SimpleDisplayManager: DisplayManager, Injectable {
static var shared = DisplayManager()
}

final class SimpleArrayResolver: Resolver {

static let shared = SimpleArrayResolver()
private var injectables = [Injectable]()

///In this simple example. This list is hardcoded, but we can and should dynamically loaded when the module is load
private let providers = [DisplayManagerProvider()]

init() {
setup()
}
func setup() {
injectables = providers.map { $0.provide() }.compactMap { $0 }
}

func resolve(name: String?) -> Injectable? {
injectables.first(where: { type(of: $0).name == name }) //Step 2
}
}

So how does all this work? Let’s walk it through with an example:

//MARK: Injected properties
@Injected var displayManager: DisplayManager?

  1. The property wrapper @Injected has a mutating getter so that any time this variable is accessed, it checks if this property nil, if so go ahead and ask the default resolver implementation for value based on a name value
  2. The resolver takes the name and looks to the providers to see if any entity matching the same name has been provided, this entity may also be cached within the resolver, depending on how the resolver is implemented.
  3. The provider provides an entity, which the resolver retrieves and hands it to the property that requested it. 
  4. Profit! The class requested has no idea how the property appeared and it doesn’t really need to. All it knows is that it’s ready to go!

But wait, there is more! (Testing)

Testing is also cleaner and easier. The provider/resolver interactions give developers an opportunity to mock singletons once instead of passing in mocks from the class initializer over and over. For example, one can mock all the singletons at once by replacing the providers with mock providers that provide entities sharing the same name 

note:

static var name = "\(DisplayerManager.self)"

///This can be done in the NSPrincipalClass class or can be per test suite in the setup() method
SimpleArrayResolver.shared.providers = [MockDisplayManagerProvider()]

final class MockDisplayManagerProvider: Provider {

typealias T = DisplayManager

func provide() -> T? {
MockDisplayManager.shared
}
}

final class MockDisplayManager: DisplayManager {
static var name = "\(DisplayerManager.self)"
static var shared = MockDisplayManager()
}

Final thoughts

Although our solution is rather simple, in reality, things can be a bit more complicated when we start to deal with data that may not be readily available. Luckily this solution removes the burden to make asynchronous tasks off the caller and into its own specialized provider classes, so we would still call that a win!

That’s all folks, this is far from a complete solution but hopefully will set the groundwork to support complicated dependency requirements! We are excited to see where this leads us next!

Major thanks for the article at Swift 5.1 Takes Dependency Injection to the Next Level by Michael Long for inspirations and usage on property wrappers.

 

 

Share