1

I am dealing with multiple vendor APIs which allow creating Device records but as expected they represent devices differently. Basic example (focusing on difference of ID types among vendors) -

Vendor1#Device uses integer IDs { ID: <int>, ...vendor1 specific details }

Vendor2#Device uses UUIDs { UUID: <string>, ...vendor2 specific details }

Since, the structures vary among vendors, I am planning to save these (device records) in a MongoDB collection so I have created the following interface to use from application code -

type Device struct {
  Checksum string
  RemoteID ?? # vendor1 uses int and vendor2 uses uuid
}

type DataAccessor interface {
    FindDeviceByChecksum(string) (Device, error)
    InsertDevice(Device) (bool, error)
}

This will be used from a orchestration/service object, like -

type Adapter interface {
    AssignGroupToDevice(GroupID, DeviceRemoteID ??) (bool, error)
}

type Orchestrator struct {
    da             DataAccessor
    vendorAPI      Adapter
}

// Inside orchestrator#Assign method
device, _ := o.da.FindDeviceByChecksum("checksum from arg")
.
.
o.vendorAPI.AssignGroupToDevice("groupID from arg", device.RemoteID ??)
// The above method calls out vendor's HTTP API and passes the json payload built from args
//

As you can see, I can't put a type for RemoteID or DeviceRemoteID. What are my options to handle this pragmatically? An empty interface would have me writing type switches in the interface implementation? Generics? or something else? I am confused.

5
  • @blackgreen Added extended example, thanks! Commented Apr 14, 2022 at 11:39
  • Or store both int and uuid as fields in Device and use whichever based on the context. Commented Apr 14, 2022 at 12:14
  • @blackgreen AssignGroupToDevice is calling vendor 1 or 2's HTTP API using the json payload built from args. Commented Apr 14, 2022 at 12:18
  • @colm.anseo Wouldn't that create a mess coz of zero valued members of the struct. Also, for someone reading the code, it would be confusing, right? Commented Apr 14, 2022 at 12:46
  • 1
    It's a trade off: extra storage vs extra type checking code. You can create Device methods to hide these implementation details in any case. Commented Apr 14, 2022 at 12:55

1 Answer 1

1

Your application code should not care at all about the actual vendors and their APIs.

Try to define some core entity package that you will use in your domain, in your application. This can be anything you decide and shouldn't be dependent on external dependencies.

The service will define the interface it needs in order to do the appropiate BL (Find, Insert, Assign group id)

For example:

Entity package can be device

package device

type Device struct {
  Checksum string
  RemoteID string
}

Note that you can define the RemoteID as a string. For each vendor, you will have an adapter that has knowledge of both the application entities and the external vendor API. Each adapter will need to implement the interface the service requires.

type DeviceRepository interface {
    FindDeviceByChecksum(string) (device.Device, error)
    InsertDevice(device.Device) (bool, error)
}

type VendorAdapter interface {
    AssignGroupToDevice(GroupID, DeviceRemoteID string) (bool, error)
}

type Orchestrator struct {
    deviceRepo    DeviceRepository
    vendorAdapter VendorAdapter
}

// Inside orchestrator#Assign method
device, err := o.deviceRepo.FindDeviceByChecksum("checksum from arg")
if err != nil {...}
.
.
o.vendorAdapter.AssignGroupToDevice("groupID from arg", device.RemoteID)
//

You can note here a few things:

  • Defined the interfaces in the service package. (Define interfaces where you are using/requiring them)
  • DeviceRepository: This is the data layer, responsible for persisting your entity (into mongo) (repo is just a convention I'm used to, it doesn't have to be called repo :)
  • VendorAdapter: adapter to the actual vendor. The service has no knowledge about the implementation of this vendor adapter. It doesn't care what it does with the remote-id. The vendor API that uses int will just convert the string to int.

Of course naming is optional. You can use DeviceProvider instead of VendoreAdapter for example, anything that will make sense to you and your team.

This is the whole point of the adapters, they are converting from/to entity to the external. From application language into the specific external language and vice versa. In some way, a repository is just an adapter to the database.

enter image description here

Edit: For example, the vendor adapter with the int remote-id will just convert the string to int (Unless I'm totally missing the point lol:) :

package vendor2

type adapter struct {
  ...
}

func (a *adapter) AssignGroupToDevice(groupID, deviceRemoteID string) error {
    vendor2RemoteID, err := strconv.Atoi(deviceRemoteID)
    if err != nil {...}

    // send to vendor api2, using the vendor2RemoteID which is int
}

Edit2: If there is another field that is different between these vendors and is not primitive, you can still use string. The vendor's adapter will just need to marshal the string to the specific vendor's custom payload.

Another way is of course do as @blackgreen said and save it as interface{}

You need to make a few decisions:

  • How you will serialize it to the database
  • How you will serialize it in the vendor's adapters
  • Are you using it in the application? Meaning is the application has knowledge or is agnostic to the value. (if so, you probably won't want to save it as a string. maybe :)

The repo - will save it as JSON to the DB

The vendor adapter - will convert this interface{} to the actual payload the API needs

But there are many many other options to deal with dynamic types, so sorry I didn't quite answer your question. The core of the solution depends on whether the application uses it, or it is only data for the vendor adapter to use.

Sign up to request clarification or add additional context in comments.

5 Comments

I agree with the structure you have described but the heart of the problem is handling that RemoteID. Say, it might not be always possible to represent it as a string, what should the VendorAdapter interface look like then?
@Tom maybe I'm missing the point, but the vendorAdapter will just convert from string to int. Added an example
Imagine RemoteID is a struct or some custom type. Then what? Type assertions or something else? Also, that class diagram looks cool, what did you use to generate that? Thanks!
@Tom Oh, now I see what I've missed. Sorry, added an edit, and tried to elaborate (The chart was done using lucid.app :)
Appreciate all the help, thanks :)

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.