3
\$\begingroup\$

Im implementing and designing a workflow that performs both HTTP and BLE requests.

Im having trouble figuring out the best approach to design a service to handle both types of requests, and manage state.

I thought to make a base class Request with an execute method that all types of requests will conform to.

BLE and HTTP are quite different and dont share any attributes, so I made an enum to handle their associated values. It includes a provider which is a client that exposes api to perform actual BLE or HTTP request.

enum RequestCode: UInt {
    case getStatus             = 0x01
    case download              = 0x02
    case confirm               = 0x03
    case cancel                = 0x04
    case getAuthToken          = 0xF5
}


enum RequestType {
    case BLE(code:RequestCode, provider:BLEProvider, characteristic: CBCharacteristic, service: CBService)
    case HTTP(code:RequestCode, provider:NetworkProvider, data: Data, accessToken:String, method:HTTPMethod, params:[String:Any]?)
}


class Request {

    var type: RequestType

    init(type: RequestType) {
        self.type = type
    }

    func execute() {

        switch type {

        case .BLE(let code, let provider, let characteristic, let service):
            provider.writeValue(data: code.rawValue, characteristic: characteristic, service:service)

        case .HTTP(let code, let provider, let data, let accessToken, let method, let params):
            provider.performRequest(data: data, token: accessToken, method: method, params:params)

        }

    }
}

Questions

  1. What is a better way to structure my requests, to handle both BLE and HTTP?

  2. How can I handle transitions from one state/event to the next? Does it make sense to try to use promises, where I can call execute on each request, and chain them all together? If not, should I think of adding each request to a queue, if and how could a state machine be designed around RequestCode which are the events in the sequence?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

You should separate concerns whenever possible. Since BLE & HTTP requests are quite different, you could write the following protocols:

protocol Request {
    associatedtype Provider
    var provider: Provider { get }

    func execute() 
}

protocol BLERequest: Request {
    typealias Provider = BLEProvider

    var code: UInt { get }
    var characteristic: CBCharacteristic { get }
    var service: CBService { get }
}

protocol HTTPRequest: Request {
    typealias Provider = NetworkProvider

    var data: Data { get }
    var accessToken: String { get }
    var method: HTTPMethod { get }
    var params: [String:Any]? { get }
}

Then comes the magic; you can write generic extensions on your Request protocol, for each specific Request type.

extension Request where Self: BLERequest {
    func execute() {
        // provider is available since it is a get-able requirement, 
        // and we know it is a BLEProvider instance
        // same goes for all the other get-able vars
        provider.writeValue(data: code, characteristic: characteristic, service: service)
    }
}

extension Request where Self: HTTPRequest {
    func execute() {
        // Again, same thing, we have all vars we need
        provider.performRequest(data: data, token: accessToken, method: method, params: params)
    }
}

Then, when you are implementing a new Request, all you need to do is implement the requirements for your chosen protocol, and everything else will be handled for you :)

Example:

enum MyBLERequest: BLERequest {
    case getStatus
    case download

    // MARK: BLE Request conformance
    var provider: Provider {
        return BLEProvider(... whatever init is needed)
    }

    var code: UInt {
        switch self {
        case .getStatus: return 0x01
        case .download: return 0x02
        // Any further new case will require you to implement the code here, so you have compile-time safety :)
        }
    }

    var characteristic: CBCharacteristic {
        return CBCharacteristic(.. init)
    }

    var service: CBService {
        return CBService(... more init)
    }
}

Then you can simply do MyBLERequest.getStatus.execute(), and it's all done through the protocol extension code.

And since Swift Enums allow associated values, you can always pass in additional parameters, whenever needed (you could have another case case getAuthToken(byUserID: String), which are then available to you in your implementation)

Now, since you have this all neatly separated, I would go further and pass your Request instances to each specific Provider directly. In effect, you are not the one executing a request, a provider is. That means your requests are agnostic to whatever Provider executes them, which will allow you to set up unit testing easier (because then you can write Mock Providers, and still pass them your requests like usual).

So you should probably extend each provider like such:

extend BLEProvider {
    func execute(request: BLERequest) {
        writeValue(code: request.code, 
                   characteristic: request.characteristic,
                   service: request.service)
    }
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ thanks, exactly the detailed answer i was looking for. \$\endgroup\$
    – J. Doe
    Mar 23, 2020 at 1:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.