Search code examples
swiftdesign-patternscocoa-design-patterns

What design pattern do I need for a single object which can receive data from different sources?


I'm trying to create some code in Swift that will enable the following functionality. There is a single class (call it the Datastore) that is the main interface into the code - this is what most users of the API will deal with most of the time.

This Datastore class is flexible. You could use it with data from a text file, or from Bluetooth data, or from data coming in over WiFi, etc. I call these providers of data. So, for example, you could have a BTProvider class which will take bluetooth data and send it to the Datastore when it arrives. One Datastore will never need to get data from multiple providers of data.

What design patterns or language tools help me to achieve this?

I thought about using protocols, but it feels backwards - a protocol defines which methods an object can respond to - but in this case, that will be the Datastore object - of which there is only one. In my head I feel like I want a reverse-protocol - something where I can guarantee "this object /calls/ these methods on another object". Then all the providers could implement this, and the Datastore could have a method "setProvider: ProviderOfData" where provider of data is the name of the reverse-protocol.

This would be a lot easier if I could poll the providers from the Datastore (then they could implement a protocol that defines methods like 'getMostRecentData', but due to the nature of it (asynchronous data receiving from WiFi, Bluetooth, etc.) this isn't possible and doesn't feel elegant - though if you have ideas I'm open to them!

This doesn't seem like this is the first time this would've been done, so I'm interested in how it's commonly done so I don't have to reinvent the wheel.


Solution

  • something where I can guarantee "this object /calls/ these methods on another object".

    Seems like what you need is the Delegate-Pattern

    You can have a DataStore (Swift is camel case) and this class can implement several delegate protocols. Example:

    class DataStore {
    
        // logic of the DataStore
    
    }
    

    You said that your app is mostly one class (the DataStore), so I am guessing you someone initialise your providers from it. I would suggest:

    // Provider Factory
    extension DataStore {
        func makeBluetoothProvider() {
            let btProvider = BTProvider()
            btProvider.delegate = self
        }
        // creation of other providers, or you can create them all at once.
    }
    

    Not the important part, the DataStore is the delegate of your providers, that way, when they retrieve data, they can call the DataStore. I would have a protocol like this:

    protocol ProviderDelegate: class {
        func provider(_ provider: Provider, didFinishReceiving data: Data)
    }
    
    extension DataStore: ProviderDelegate {
        func provider(_ provider: Provider, didFinishReceiving data: Data) {
             // read data and do something with it...display it, save it, etc.
        }
    }
    

    Provider would be a general class for all the providers, probably with network requests or similar basic data. One example would be:

    class Provider {
        var delegate: ProviderDelegate
        var data: Data
    }
    
    class BTProvider: Provider {
        // logic only for bluetooth provider
    }
    

    Depending on how differently your providers behave, you can have a delegate protocol for each and an extension of DataStore implementing each of this protocols. That only if the behaviours are too different from each other, which I don't think.

    UPDATE ADDRESSING COMMENT: Protocol can provide code

    A protocol can provide code, let me show you an example:

    protocol Provider {
        weak var delegate: ProviderDelegate { get set }
        func fetchData(with url: URL)
        func processData(data: Data)
    }
    
    extension Provider {
        func processData(data: Data) {
            // do some processing that all providers have to do equally
            // probably also call delegate to tell DataStore it is ready
        }
    }
    

    Your provider class would implement the method and can choose to implement a new processData or just use the default one. If it implements it, there is no need to call for override, you would just no longer have access to the protocol method. You provider can look like this:

    class BTProvider: Provider {
    
        weak var delegate: Provider?
    
        func fetchData(with url: URL) {
            // do some logic to fetch data for this provider
            processData(data: whateverWasFetched)
        }
    }