-
Notifications
You must be signed in to change notification settings - Fork 23
Message interfaces #122
Message interfaces #122
Conversation
@@ -34,23 +34,27 @@ func (m *MockMessageLog) EXPECT() *MockMessageLogMockRecorder { | |||
} | |||
|
|||
// Append mocks base method | |||
func (m *MockMessageLog) Append(arg0 *messages.Message) { | |||
func (m *MockMessageLog) Append(arg0 *protobuf.Message) { | |||
m.ctrl.T.Helper() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this .Helper()
line relevant to current change? I know this and other changes on mock files are automatically generated by make generate
, but if so why not included all changes generated by make generate
? If you have specific reason, please leave some comment on it. Or committing all make generate
changes is OK, you can do it first at separate patch, then update message interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I just never looked at changes to auto-generated files. But this commit touches too many files, so the changes should be better minimal. I will update the old generated mock files in a separate commit.
messages/api.go
Outdated
// | ||
// type IsSealed struct{} | ||
// | ||
// func (IsSealed) isSealed() {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Sealed interface" concept is new to me, but it sounds to strictly control visibility of the type, which should be good for code robustness. Anyway this patch only provides interfaces and I can't say anything about how each interface has each method for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This concept is admittedly tricky, but seems to be the most reliable way to enforce explicit implementation of interfaces in Go (see golang/go#34996).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking maybe combine those type IsFoo struct{}
and their methods at the end of the file.
Anyway this patch only provides interfaces and I can't say anything about how each interface has each method for now.
True, I just wanted to share the interfaces before I proceed with implementation. Now I have some implementation (not yet used), so I can share that, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This concept is admittedly tricky, but seems to be the most reliable way to enforce explicit implementation of interfaces in Go (see golang/go#34996).
You're trying to improve Golang itself, wow 😲
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To have some fun at night 😆
|
||
type Message interface { | ||
encoding.BinaryMarshaler | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works for decoupling logic and representation, so I'm looking forward to see the concrete type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me.
d23e28a
to
8245dae
Compare
Summary of changes in #122 (commits):
|
Sorry, I'll fix the DCO issue with the next update... |
Thanks for the update. |
I seem to have finished implementing messages half-way 🙂 My plan was to get your feedback on this while working on some other patches for view change (that will also fix #110). Those changes might affect how the message interfaces will be used in the core protocol logic. |
@@ -34,14 +34,20 @@ func (*impl) NewFromBinary(data []byte) (messages.Message, error) { | |||
return nil, fmt.Errorf("failed to unmarshal message wrapper: %s", err) | |||
} | |||
|
|||
switch msg.Type.(type) { | |||
switch t := msg.Type.(type) { | |||
case *Message_Request: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function switches based on message type extracted from raw message data, but it's seems that the sealed interface message.IsRequest
is not used here.
protobuf.request
embeds messages.IsRequest
, so I guess that you realize the new thing on this type. So if you explain how it's used, the concept will get clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I try to convey in the "explicit interfaces" proposal: The sealed interfaces is a tricky solution to the problem.
Here, msg
is a Message
structure which is defined by auto-generated code. Protobuf's code generator protoc
defines a oneof
field Message.Type
as an unexported interface isMessage_Type
with a single unexported method isMessage_Type()
. The whole purpose of isMessage_Type
interface is to choose which concrete types can be assigned to the field. This is close to the sealed interfaces construction, but within the same Go package.
Thus, this type switch determines the unmarshaled message type relying on protobuf's definitions. This should work as expected, because the switch cases match concrete types (structures, not interfaces) and protoc
guarantees the expected semantic of such type switches/assertions.
However, the returned value of this function is an abstract interface messages.Message
, defined in messages/api.go
. That value's type should embed one of IsXxx
structures defined in messages/api.go
. The value returned by newRequest()
in the following line is of request
type, which embeds messages.IsRequest
. This embedding ensures that request
implements the unexported methods of messages.Request
(the only purpose of messages.IsRequest
). The remaining methods of messages.Request
are implemented by request
itself so that it fulfils messages.Request
interface.
"github.com/hyperledger-labs/minbft/messages" | ||
) | ||
|
||
func TestRequest(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess that some test code using isRequest()
to check the type elegantly will be added later?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is no need to test isRequest()
because impl.NewRequest()
simply cannot return anything not implementing messages.Request
interface, which requires isRequest()
method. It is checked by the compiler. Moreover, isRequest()
is unexported in messages
package, so it cannot be invoked here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, so this method is only used to make some type to fulfill the interface. How useful it is will be
clearer with view-change patch.
8245dae
to
762ae1a
Compare
Summary of changes:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change set looks good to me, so I think it's ready to merge.
Summary of changes:
This makes me review easier (nice practice), thank you.
"Draft" status was removed when I clicked "ready for review" button to update my review status. I'm not sure that this was the right review process, so sorry if it surprised you. I leave this PR open for you to add the final changes, or to merge current PR branch. |
It looks like the official recommendation for resolving ambiguities among similar interfaces, as opposed to using sealed interfaces, is to augment interfaces with so-called qualifying methods. This would look like follows. type ReplicaMessage interface {
Message
ReplicaID() uint32
ImplementsReplicaMessage()
}
// ...
type Reply interface {
ReplicaMessage
// ...
ImplementsReply()
} type reply struct {
// ...
}
func (m *reply) MarshalBinary() ([]byte, error) { /* ... */ }
func (m *reply) ReplicaID() uint32 { /* ... */ }
// ...
func (reply) ImplementsReply() {}
func (reply) ImplementsReplicaMessage() {} What do you think of this alternative? |
Thanks for the info. I'd rather like the official way even both are not perfect. It needs less codes, is easier to read for me, and is in the official FAQ. |
|
I agree that following an official recommendation should make that easier for others to understand it. Though, I'm not sure if it needs less code: Note that
What was desired was to avoid Basically the actual problem I tried to solve is perfectly captured on Wikipedia:
So far, I don't have a strong opinion on which approach to follow. Maybe I adjust the code to follow the "qualifying methods" approach, and we'll see how it looks like. Sleep on it, then decide 🙂 |
It is my understanding that the sealed interface needs an additional type definition ( I don't have a strong option and either is fine. |
I tried to adjust the code for qualifying methods approach. Please have a look to see which approach you prefer. |
Thanks, I prefer the qualifying methods approach. |
OK, so let's go with the "qualifying methods" approach. It's nice to keep the commit about "sealed interface" because the project history has the record and that allows us to revisit easier in the future. I'm OK to merge your branch, but the latest commit has "RFC" in the subject, so you may make the final change. |
@ynamiki @Naoya-Horiguchi Thanks for the feedback! The recommended approach indeed doesn't seem too bad, let's stick to it.
I would still prefer to merge it when I make the interfaces actually used by the core... Please let me know if you need it sooner. |
feb001e
to
e94a066
Compare
Summary of changes:
(More commits will come soon.) |
A few comments:
|
@Naoya-Horiguchi Thanks for reviewing this.
Sorry for making the commit so large, but I couldn't find a better way to do the switch without breaking building/testing.
Before the switch,
Thanks, I think I missed it when resolving rebase conflicts. That's easy to fix 🙂 |
14337be
to
f11add8
Compare
Make sense, thank you very much. So I'm totally fine about proposed patches |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had a question but it is solved.
One (slightly off-topic) question: do we have a nice tool to support refactoring that has been done in the last commit? I guess changing a struct to an interface is a common refactoring method and this cause tedious and extensive code modification. It is hard to do that manually.
I have no idea about specific tools, but maybe some modern editor (VSCode in my mind) helps for the purpose. For example, if you change some code around struct definition, VSCode automatically (in real time manner) checks whole repository and marks all type mismatches as warnings. So you can adjust all affected places with confidence. |
I tried to resolve conflict, but I found that I had to change |
@ynamiki I think |
Maybe we better first finalize and merge #142, then rebase this PR.
I am not sure how good the Web UI is for this kind of job 🙂
No worries, I'll take care of this PR. |
Thanks to both of you for reply. I understand there are not a one-shot method and we need to update code manually referencing compiler errors. |
Signed-off-by: Sergey Fedorov <[email protected]>
Protocol Buffers is only one of possible mechanisms to serialize messages. To prepare for decoupling the core code from a specific implementation of message serialization, move the code into a corresponding sub-package. Signed-off-by: Sergey Fedorov <[email protected]>
The protocol logic should not depend on particular implementation of message representation, e.g. on automatically generated code. Define abstract interface for message types to decouple the usage of messages from their particular representation. Signed-off-by: Sergey Fedorov <[email protected]>
Signed-off-by: Sergey Fedorov <[email protected]>
Signed-off-by: Sergey Fedorov <[email protected]>
Signed-off-by: Sergey Fedorov <[email protected]>
Signed-off-by: Sergey Fedorov <[email protected]>
Signed-off-by: Sergey Fedorov <[email protected]>
The officially recommended way [1] to require types to explicitly declare that they implement an interface is to add a special method with a descriptive name to the interface's method set. Then the type must have an (empty) implementation of that method, in order to implement the interface. This technique is also known as "qualifying methods". [1]: https://golang.org/doc/faq#guarantee_satisfies_interface Use qualifying methods to ensure the desired semantics of type switches/assertions for message interfaces. Signed-off-by: Sergey Fedorov <[email protected]>
Signed-off-by: Sergey Fedorov <[email protected]>
View change will introduce more protocol message types that have to be processed according to the current view number. Generalize viewMessageProcessor to allow for this extension. Signed-off-by: Sergey Fedorov <[email protected]>
Message interfaces will not require each implementation to provide a method for getting embedded messages. Message interfaces will be accompanied with a helper function to get embedded messages. Restructure the test to prepare for this change. Signed-off-by: Sergey Fedorov <[email protected]>
Now message interfaces are defined and implemented, switch the core and client code to use those interfaces as opposed to directly manipulating Protocol Buffers auto-generated code. Signed-off-by: Sergey Fedorov <[email protected]>
f11add8
to
8984c84
Compare
Summary of changes:
|
…repare-timer Resolve conflict with changes from "Message interfaces hyperledger-labs#122". Signed-off-by: Naoya Horiguchi <[email protected]>
This pull request introduces an abstract interface between a particular representation of protocol messages and their usage by the core protocol logic.
This decouples the core protocol code from a particular way the protocol messages are represented, e.g. from automatically generated code of Protocol Buffers. It is advantageous to make this separation before introducing more complex message types as those of the view change operation.