Replies: 3 comments 6 replies
-
Fwiw, I'm not sure we can solve this universally and I am not sure we can/should change the existing code. Some of the examples are architected with the pattern in mind and changing that would be hard (and requires some serious digging). But for new greenfield code I would like to avoid it, let me explain what I do not like about the approach. I have a (relatively) strong opinion here and I want to apologize in advance if I step on any toes. Lets take the Uploader interface: type Uploader interface{
func UploadAndRegister() error
} for this we want to also support returning structured data. For an aws cloud it would be AMI name for GCP a URL. Now with the type UploaderResult inteface{
func IsUploadResult()
}
type Uploader interface{
func UploadAndRegister() (UploadResult, error)
}
type awsUploadResult struct {
ami string
}
func (*awsUploadResult) IsUploadResult() {}
type googleCloudUploadResult struct {
url string
}
func (*googleCloudUploadResult) IsUploadResult() {} my concern is that we actually gain very little type safety with this approach and the cost is that we have these empty methods that add (IMHO) very little value and look very out of place in go projects. Why little value? Because func (* awsUploader) UploaderAndRegister() (UploadResult, error) {
...
return &googleCloudResult{URL: "no-compilter-will-complain"}
} is still perfectly valid. We protect against returning the wrong class of datatypes but not the wrong data. And the caller needs to cast to the right type anyway and check that it is correct (just like with an And the call side still needs to do: res, err := awsUploader.UploadAndRegister()
...
awsRes, ok := res.(*awsResult)
if !ok {
return fmt.Errorf("internal error: ....")
} I think if we go down this path, we should simply return an I guess at the end of the day it comes down to a choice like: is the value of (IMHO little) type safety from the
To me the answer is an unequivocally "no - its not worth it" but I see of course that there is a lot of subjectivity in opinions like this. |
Beta Was this translation helpful? Give feedback.
-
I will comment on the Uploader use case because I know nothing about stages and what those I woud start with a idiomatic way of building Go interfaces and split the I do not understand what an inteface u := AWSUploader{
InputData1: "input data",
}
u.Upload(imageReader, progressBar)
println(u.OutputData1, u.OuputData2) Note I intentionally left out a "getter" here, there is no point for that. Now, I understand that some generalization can be useful for example to be able to print the results in a common way, let me assume that this is needed for the CLI binary, then I am assuming those values will be somewhat printed to the console. In that case, I would think about perhaps key-value pairs that can be nicely formatted and shown to the user. In this scenario, creating a getter does make sense because a result can be created either via upload, or via registration or perhaps both. Tho I slightly lean towards returning slice of key-value pairs from both operations and combining them together. Something in the ballpark of: type Result struct {
Name string
Value string
}
type Uploader struct {
Upload(...) ([]Result, error)
}
type Registrator struct {
Register(...) ([]Result, error)
} The caller can now call both methods and collect the results as needed. Since these values end up in a terminal, strings are probably okay but if needed, values can be generalized. A nice example is the built-in log/slog interface from the stdlib and its slog.Value generalization tho this is perhaps stretched way to far. The result type can be more sophisticated and details hidden, what matters is a printing interface that can be different for various environments - key/values as strings perhaps suits CLI, there might be a different "renderer" returning data in other forms. But I would rather keep it simple. I can imagine CLI output like:
|
Beta Was this translation helpful? Give feedback.
-
I think I agree that there's no universal better alternative to this pattern. And I also agree that One area where we use this pattern is when serialisation is involved, and there I don't know if
The unmarshal function for Target is annoying: It's easy to forget to add you target to this switch statement. Say the TargetOptions interface is
And then you'd end up with something like: This just moved the problem however, because this would put the onus on the caller to pass the |
Beta Was this translation helpful? Give feedback.
-
Background: osbuild/images#1213
In Golang projects, we tend to run into situations where we use a common interface with a set of methods, which is then implemented by multiple implementations. A good example is the new
Uploader
interface, which needs to be implemented differently for different clouds. However, in some cases, we need the ability to get implementation-specific data after calling a specific method. For example, to get data to identify the image in the target cloud after it has been uploaded, which will look different for each cloud.So far, we tend to return interface type in such cases, which contains just the
IsXXX()
function, which is, in turn, implemented by every data structure returned by a specific implementation. Sometimes, we do not necessarily return these structures implementing the interface as a function's return value, but we use it to represent diverse data structures. In principle, it is, in my opinion, the same core idea.A few examples:
https://github.com/osbuild/images/blob/ce45a6fb33f6f686e15df5a4f42c78a07058c31d/pkg/osbuild/stage.go#L16-L19
https://github.com/osbuild/images/blob/ce45a6fb33f6f686e15df5a4f42c78a07058c31d/pkg/osbuild/input.go#L4-L6
https://github.com/osbuild/images/blob/ce45a6fb33f6f686e15df5a4f42c78a07058c31d/pkg/osbuild/mount.go#L12-L14
https://github.com/osbuild/osbuild-composer/blob/74638a97338f7681db426eccdc35a9d1be921e19/internal/target/target.go#L47-L49
https://github.com/osbuild/osbuild-composer/blob/74638a97338f7681db426eccdc35a9d1be921e19/internal/target/targetresult.go#L26-L28
https://github.com/osbuild/osbuild-composer/blob/74638a97338f7681db426eccdc35a9d1be921e19/internal/weldr/upload.go#L31-L33
https://github.com/osbuild/osbuild-composer/blob/74638a97338f7681db426eccdc35a9d1be921e19/internal/upload/koji/koji.go#L115-L117
@mvo5 does not like it, and I can't blame him. Although I'm not a proponent of this approach, personally I find it acceptable in warranted cases.
Let's have a short brainstorming discussion to hopefully find a nicer way to approach this problem consistently in all of our projects. Or if not a nicer way, let's at least have a common understanding of which approach we want to use in which case.
Beta Was this translation helpful? Give feedback.
All reactions