Attempting to use Dependency injection

Hi,

I have a class called Box that I use in production.

The initializer takes in no arguments.

However for some SwiftUI previews / test cases I would like that class to be initialized with some values.

I don't want that initializer (with values) to be available in production, it would be only used in SwiftUI previews and test cases.

My attempt:

I used #if DEBUG it solves my problem with minimal code change but not sure if I was missing a better way.

Given below is a simplified example.

@Observable
class Box {
    private(set) var chocolates = [String]()
    
    @ObservationIgnored
    private var isProcessed = false

    #if DEBUG
    init(chocolates: [String]) {
        self.chocolates = chocolates
    }
    #endif
    
    // contains more complex logic using chocolates
}

Questions

  1. Is the above a reasonable approach using #if DEBUG?
  2. Or is there a more elegant way to do it?

Problems I faced

  1. Trying to use a convenience initializer but that exposed a designated initializer to production defeating the purpose
  2. This code contains some macros so was a bit concerned using it in protocols

Thanks

Can you say a bit more about the kind of logic you want using chocolates? Where do they come from and how are they transformed? Because from the example per se, it's not really clear to me if Dependency Injection (DI) is what is needed.

The standard DI pattern would be: initiliazing Box takes a type that conforms to a ChocolateStorage protocol (or something). And then you construct one like:

// production
let box = Box(chocolateStorage: RealChocolateStorage())

// preview
let box = Box(chocolateStorage: MockupChocolateStorage())

But if this really helps, depends a lot on what kind of behavior you want in the // contains more complex logic using chocolates and/or where the chocolates come from.

Honestly, I'd not take the init(initialChocolates: [String] = [] off the table immediately unless the complex logic has real risks with a setup like that.

1 Like

Thanks a lot @maartene

Chocolates represent available purchases that would be fetched from an API. May be dependency injection is not the best approach.

Your approach is a good one, the only place where I got stuck with your solution was passing a mock storage where chocolates would need to mutable inside the box but shouldn't be modified outside the box.

In order to make mutable I couldn't avoid making RealChocolateStorage expose (internal) chocolates which could be modified from the outside.

I don't want to introduce a loop hole in production where accidentally I leave an option to modify the chocolates outside the Box.

If chocolates are a value type (like String in your example), the private(set) already makes them impossible to mutate outside of the Box. Even if you pass them in the init. For example:

var chocolates = ["Pure", "Milk", "White"]
let box = Box(chocolates: chocolates)

// mutate the chocolates array
chocolates.append("Caramel-Seasalt")

// Box of chocolates is not changed, because it has its own chocolate storage
print(box.chocolates) // ["Pure", "Milk", "White"]

// Direct modification of the chocolates in the box are not allowed
box.chocolates.append("Pecan") // ❌ does not compile

Would that cover your needs?

Thanks @maartene, yes chocolates is a value type.

May be I will explain where I am stuck.

Based on your inputs I have built something, see the code below:

Problem

  • RealChocolateStorage.chocolate is modifiable and can be modified outside of the Box
  • I don't want RealChocolateStorage.chocolate to be modifiable outside the Box class.

Code

protocol ChocolateStorage: AnyObject {
    // would need to be settable so that it can be used inside the box
    var chocolates: [String] { get set }
}

class RealChocolateStorage: ChocolateStorage {
    var chocolates: [String] = []
}

class MockupChocolateStorage: ChocolateStorage {
    var chocolates: [String]
    
    init(chocolates: [String]) {
        self.chocolates = chocolates
    }
}

@Observable
class Box {
    private(set) var chocolateStorage: ChocolateStorage
    
    @ObservationIgnored
    private var isProcessed = false
    
    init(chocolateStorage: ChocolateStorage) {
        self.chocolateStorage = chocolateStorage
    }
    
    func fetchCholates() {
        chocolateStorage.chocolates = ["aaa", "bbb"]
    }
}

// production
let realStorage = RealChocolateStorage()
// I am still able to modify here,
// so potentially in production it could get modified
// outside the box
realStorage.chocolates = ["A1", "A2"]
let box1 = Box(chocolateStorage: realStorage)

// preview
let box2 = Box(chocolateStorage: MockupChocolateStorage(chocolates: ["test1", "test2"]))

ah ok. my point would be: do you still need the ChocolateStorage protocol?

Or is this enough?

@Observable
class Box {
    private(set) var chocolates: [String]
    
    @ObservationIgnored
    private var isProcessed = false
    
    init(initialChocolates: [String] = []) {
        self.chocolates = initialChocolates
    }
    
    func fetchChocolates() async {
        // API call here
    }
}

// production
let box1 = Box()
await box1.fetchChocolates() // retrieve ["aaa", "bbb"] from API

// preview
let box2 = Box(initialChocolates: ["test1", "test2"])

Note: if you want to practice DI, then you are on the right track. The trick would be to make the chocolateStorage.chocolates array immutable from the outside and provide operations to interact with it:

protocol ChocolateStorage: AnyObject {
    var chocolates: [String] { get }
    func store(_ chocolate: String) 
    func remove(_ chocolate: String)
}
1 Like

Thanks a lot @maartene

Yeah your last solution of initializing with chocolates is what I have settled for.

And I have just wrapped it in a #if DEBUG to protect it from being used in production.

Even if we add store and remove functions still would be exposed as anyone conforming could still modify it outside box.

Not sure if I am overthinking it :)

Thanks a lot @maartene, your guidance helped me refactor my code to extract the common bits into a separately.

That way as you had suggested I could pass it as a mock / real object and works beautifully, thanks to you!!

1 Like

You’re welcome!