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.
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
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)
}