I face this a lot, and I think you have one very valid approach to it that I often employ. Especially if you have some centralized point to make a list of structs, or walk a tree to collect them.
I think another other route is to provide lots of tooling to inspect the object graph -- include a pretty print function in the interface or something to that effect.
There's equivalency between the two, the struct with pointers to implementation functions can implement an interface. Likewise, the implementation functions may call other implementations, so you haven't exactly solved the proxy problem, the state controlling which implementations becomes function local instead of on a struct. Unless you start to provide a struct with all of the context, at which point you've actually re-invented interfaces. I've grown wary of having too much of the details as function local like this because it hinders making more advanced tooling by hiding intermediate state from reflection.
The interface provides the means to give other representations, and allows for embedding one within another, and gives runtime tooling to inspect underlying types. It gives more elegance and flexibility to the problem at language and standard library scale.
One of the nice things is that it gives a sort of bidirectional refactoring path. If someone chose struct-of-pointers and it's at it's breaking point, those structs can stay as is and start to implement the interface with minimal effort. Likewise, if I need to boil down a hodgepodge of implementations into a really simple control flow, struct methods capture the struct and can be passed around as ordinary function pointers, so I can built a struct out of existing interface implementations.
There's no real way around the complexity -- leaky abstractions are a thing. I think the author got it right: use interface upgrades and proxies sparingly. If they are the right tool for the job, then it's probably a good idea to go the extra mile to provide good documentation and instrumentation with extra attention to caller/callee expectations.
The main problem the method pointers solves for me is answering the question "can this object do X". The mere existence of a method you can type assert isn't enough.
Returning a NotImplemented error isn't a great solution either as you might have to do quite a bit of work to get ready to call the method only to find it isn't implemented.
Some bit of reflection magic which allowed objects to remove methods from their method sets at runtime would be perfect. You'd just use interface upgrades and you'd be sure that it would work.
Also an available flag for each method would work too and might be neater than the function pointers.
> Some bit of reflection magic which allowed objects to remove methods from their method sets at runtime would be perfect. You'd just use interface upgrades and you'd be sure that it would work.
Given that methods sets are...sets. I'd be interested at a language level, what it would look like to add some notion of set difference or expressing disjointedness in some way, e.g.,
type ReadOnly retraction {
Write([]byte) (int, error)
}
type ReadOnlyFile struct {
ReadOnly
*File
}
where `ReadOnlyFile` would have all of `*File`'s methods minus the methods defined in `ReadOnly`
restriction probably isn't a good term, I could see it easily being misinterpreted, but I haven't all day to ponder it :)
(edit: after publishing I realized `retract` might be more clear)
I don't know if it would ultimately simplify the problem or not, but I agree that having some way to easily mask or hide a method subset could be quite nice.
Edit:
I want to add that the idiomatic thing to do now would be
type ReadOnlyFile File
/ *proceed to implement every method you want accessible */
maybe something like the following could be possible
type ReadOnlyFile retracts *File {
Write([]byte) (int, error)
}
A similarity to your struct of fileops, would be Go to allow implementing an interface will nil methods. Like:
func (f *file) Rename() = nil
Then later test for nilness with: if f.Rename == nil,
or with better readability:
if v, ok := f.(interface {Rename()}); ok && v == nil // -> if true, then nil method
We could define a Type Assertion to return "nil, ok" when all methods
of the interface it is being passed to are implemented nil methods.
type NotOftenImplemented interface {
RareMethod()
SuperRareMethod()
}
if v, ok := data.(NotOftenImplemented); ok && v == nil {
// RareMethod() and SuperRareMethod() are nil
}
switch data.(type) {
case NotOftenImplemented: // would be supposed to match ONLY if AT LEAST >= 1 of the methods of this interface is non-nil
}
Of course it would create a phenomenon where all callers of any interface methods would now fear of calling any method without first testing for its nillness.
On another hand, there is also sometimes methods implemented with a single-line of panic("Not Yet Implemented") so...
I think another other route is to provide lots of tooling to inspect the object graph -- include a pretty print function in the interface or something to that effect.
There's equivalency between the two, the struct with pointers to implementation functions can implement an interface. Likewise, the implementation functions may call other implementations, so you haven't exactly solved the proxy problem, the state controlling which implementations becomes function local instead of on a struct. Unless you start to provide a struct with all of the context, at which point you've actually re-invented interfaces. I've grown wary of having too much of the details as function local like this because it hinders making more advanced tooling by hiding intermediate state from reflection.
The interface provides the means to give other representations, and allows for embedding one within another, and gives runtime tooling to inspect underlying types. It gives more elegance and flexibility to the problem at language and standard library scale.
One of the nice things is that it gives a sort of bidirectional refactoring path. If someone chose struct-of-pointers and it's at it's breaking point, those structs can stay as is and start to implement the interface with minimal effort. Likewise, if I need to boil down a hodgepodge of implementations into a really simple control flow, struct methods capture the struct and can be passed around as ordinary function pointers, so I can built a struct out of existing interface implementations.
There's no real way around the complexity -- leaky abstractions are a thing. I think the author got it right: use interface upgrades and proxies sparingly. If they are the right tool for the job, then it's probably a good idea to go the extra mile to provide good documentation and instrumentation with extra attention to caller/callee expectations.