英文:
Go - Same interface to handle multiple types
问题
我正在处理多个供应商的API,这些API允许创建Device
记录,但如预期的那样,它们以不同的方式表示设备。以下是一个基本示例(关注供应商之间ID类型的差异):
Vendor1#Device使用整数ID { ID: <int>, ...供应商1特定的详细信息 }
Vendor2#Device使用UUID { UUID: <string>, ...供应商2特定的详细信息 }
由于结构在供应商之间有所不同,我计划将这些(设备记录)保存在MongoDB集合中,因此我创建了以下接口供应用程序代码使用:
type Device struct {
Checksum string
RemoteID ?? # 供应商1使用整数,供应商2使用UUID
}
type DataAccessor interface {
FindDeviceByChecksum(string) (Device, error)
InsertDevice(Device) (bool, error)
}
这将从编排/服务对象中使用,例如:
type Adapter interface {
AssignGroupToDevice(GroupID, DeviceRemoteID ??) (bool, error)
}
type Orchestrator struct {
da DataAccessor
vendorAPI Adapter
}
// 在orchestrator#Assign方法内部
device, _ := o.da.FindDeviceByChecksum("参数中的checksum")
.
.
o.vendorAPI.AssignGroupToDevice("参数中的groupID", device.RemoteID ??)
// 上述方法调用供应商的HTTP API,并传递从参数构建的JSON负载
//
如你所见,我无法为RemoteID
或DeviceRemoteID
指定类型。在处理这个问题时,我有哪些选择?一个空接口会让我在接口实现中编写类型切换吗?泛型?还是其他什么?我感到困惑。
英文:
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.
答案1
得分: 1
你的应用程序代码不应该关心实际的供应商和他们的API。
尝试定义一些核心实体包,你将在你的领域和应用程序中使用。这可以是你决定的任何东西,不应该依赖于外部依赖。
服务将定义它需要的接口,以便执行适当的业务逻辑(查找、插入、分配组ID)。
例如:
实体包可以是 device
package device
type Device struct {
Checksum string
RemoteID string
}
注意,你可以将 RemoteID
定义为字符串。对于每个供应商,你将有一个适配器,它了解应用程序实体和外部供应商API。每个适配器都需要实现服务所需的接口。
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
}
// 在 orchestrator#Assign 方法内部
device, err := o.deviceRepo.FindDeviceByChecksum("checksum from arg")
if err != nil {...}
.
.
o.vendorAdapter.AssignGroupToDevice("groupID from arg", device.RemoteID)
//
你可以注意到以下几点:
- 在服务包中定义了接口(在使用/需要它们的地方定义接口)
DeviceRepository
:这是数据层,负责将实体持久化到数据库(repo 只是我习惯的一种约定,它不一定非得叫 repo :))VendorAdapter
:实际供应商的适配器。服务对这个供应商适配器的实现没有任何了解。它不关心它对远程ID做了什么。使用整数的供应商API只会将字符串转换为整数。
当然,命名是可选的。你可以使用 DeviceProvider 而不是 VendorAdapter,例如,任何对你和你的团队有意义的名称。
这就是适配器的全部意义,它们将从实体转换为外部语言,反之亦然。在某种程度上,存储库只是数据库的适配器。
编辑:例如,具有整数远程ID的供应商适配器只会将字符串转换为整数(除非我完全误解了问题哈哈):
package vendor2
type adapter struct {
...
}
func (a *adapter) AssignGroupToDevice(groupID, deviceRemoteID string) error {
vendor2RemoteID, err := strconv.Atoi(deviceRemoteID)
if err != nil {...}
// 使用 vendor2RemoteID(整数)发送到供应商API2
}
编辑2:
如果这些供应商之间还有其他不同的字段,并且不是基本类型,你仍然可以使用字符串。供应商的适配器只需要将字符串编组为特定供应商的自定义有效负载。
另一种方法当然是像 @blackgreen 提到的那样,将其保存为 interface{}
。
你需要做出一些决策:
- 如何将其序列化到数据库中
- 如何在供应商的适配器中将其序列化
- 是否在应用程序中使用它?也就是说,应用程序是否知道该值或对该值不关心(如果是这样,你可能不希望将其保存为字符串,也许是其他类型)
存储库 - 将其保存为 JSON 到数据库中。
供应商适配器 - 将此 interface{}
转换为API所需的实际有效负载。
但是,处理动态类型还有很多其他选项,所以很抱歉我没有完全回答你的问题。解决方案的核心取决于应用程序是否使用它,或者它只是供应商适配器使用的数据。
英文:
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 repoVendorAdapter
: 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.
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论