I have to use a built-in API that works like this
class MyAPIWrapper {
func callAPI() {
apiObj.delegate = self
apiObj.doWork() // returns immediately, eventually calls self.delegate.workDone with the result on another thread
}
func workDone(result) {
// do something with result
}
}
You interact with the API by calling various methods on various API objects. Each method returns immediately and reports its success/failure by calling a method on the object's delegate once it is done.
The problem with this API is the delegate. Imagine if someone else calls doWork
for that same object right after you do. They "steal" the delegate, and they will get both workDone
callbacks (yours and theirs). Or even aside from that, it makes it very hard to call a sequence of methods on these API objects, because you have to implement manual synchronization mechanisms to make sure that you get a workDone
call and the result was what you wanted before moving on to the next call.
Since I can't change this API, I want to encapsulate it to eliminate these problems. My idea was to essentially make the doWork
call blocking. Then there's no delegate stealing and calling a sequence of methods is very easy. Something like this
class WrappedAPIObject
init(apiObject) {
self.apiObject = apiObject
self.delegateMutex = Mutex()
self.workCV = ConditionVariable()
}
func doWork() -> Result {
delegateMutex.lock()
apiObj.delegate = self
apiObj.doWork()
workCV.wait()
retVal = self.result
delegateMutex.unlock()
return retVal
}
func workDone(result) {
self.result = result
workCV.notify()
}
}
Then, as long as you create wrapped objects for each API object and always use those instead of the unwrapped objects, you don't have to worry about the delegates or asynchronous nature. If you want to call it asynchronously you can still spawn a thread like this
class APIWrapper
func doLotsOfWork(wrappedObj) {
Thread.new {
result1 = wrappedObj.doWork()
result2 = wrappedObj.doWork()
}
}
}
I've already tested this and it works great. But it makes me a little uneasy:
- The mutex/condition variable combo seems clunky. I feel like there's a more elegant pattern I'm missing here for simplifying this API.
- By forcing an async API to be synchronous, I force the consumer to spawn multiple threads to make it async again. In our case we have a lot of methods like
doLotsOfWork
that batch together API calls. If the user of our APIWrapper calls a whole bunch of these on the same wrapped object, it would spawn tons of threads which could only execute in serial, wasting system resources.
2 is my main concern. I feel like there should be a design where you can make multiple async calls to the same wrapped object and they all get queued up on one thread for that object.
Am I fretting over nothing? Or is there a way to fix the threading problem? Or is there a better way to avoid the delegate issue which I'm not seeing?
* If you care, the actual language is Swift, the lousy API is Apple's CoreBluetooth framework, the API objects are CBPeripherals, the "mutex" is a DispatchQueue and the "condition variable" is a DispatchGroup.
apiObj
at a time, or does all of your code have to share the one instance?