> With my current implementation, I want to allow a single object to implement
> anywhere between none or all of the methods of a given interface,
> "EventHandlerType". That interface contains several methods such as
> UserDidJoin(channel, username string) and UserDidQuit(username string). But
> since Go doesn't have "optional methods" in interfaces, I had to work around
> that by implementing this type with another type "DefaultEventHandler",
> where each of its methods are no-ops. Then, I just embed DefaultEventHandler
> into my own handler, and override whatever methods I want to implement. This
> way, I get to keep compile-time checking, but it's quite ugly and feels very
> un-idiomatic, almost like faking inheritance.
I think that's what I do given the constraints you want as I understand
them. You seem to want your objects to all inherit a set of default
methods, so using inheritance seems like the right approach.
Ian
you might want to think about avoiding event handling callbacks
entirely and use channels of events. that is - when an event occurs,
you encapsulate the event in some
data structure and send it down a channel.
the "event handler" is then a goroutine with a simple loop that reads from
the channel and processes events as they arrive.
there's no need for an interface{} parameter because all the
context is local to the handler.
whether you have several channels each for a different
kind of event or one channel for all events is a matter of taste.
this architecture has the advantage that several mutually incognizant
event types can integrate easily together, and code for dealing with
events can be written in a very straightforward style (active rather
than passive).
Your first idea sounds like it might be a reasonable way to write a
multiple-event handler; effectively your interface's functions become a
table of function pointers passed to whatever expects it, with no-op
functions provided by the embedded "default" type for "no entry", and
the associated value storing any state used by those handlers.
That said, I think it'd be worth considering a variation on your second
example in which instead of single method interfaces each with a
custom interface type, with the calling code using an implementing type
for each, you just use function values for each member variable, making
it in essence an explicit form of the first idea with empty entries
being nil instead of no-op functions.
It's simpler- eliminates a type per hook, plus a type per hook in every
package adding one, with the only weakness being that if you want to
have multiple instances of those types generated and passed in separate
AddEventHandlers calls, with different state, they have to be closures
closing over that state, so if you want to do that most of the time it
might not be any prettier.
Since you requested any thoughts, my two cents based on your second
example: I wonder whether the constraint you're imposing of a single
multiple-event handler is actually necessary. Any reason why you can't
hook each event separately at whatever point you'd be calling
"AddEventHandlers", passing a function each time, e.g.
bot.HookJoin(joinHandler), bot.HookQuit(quitHandler), where joinHandler
and quitHandler are functions (possibly closures).
I've used that model for hooking event in a Go program and it's been
both simple (no additional event handler types at all), type safe, and
efficient; calling hooks for an event is simply iterating over a slice
of function pointers appended to each time a hook is added and calling
each one, all of which are actual hooks.
If you chose to use channels instead of callback functions (as someone
else suggested) then you could pass a channel to such hook functions
instead of a callback function easily.
If there's a reason you definitely need a multiple event handler, then
disregard this. :P
i believe that this is the way that Go is intended to be used.
channels can make the control of concurrency much more
straightforward.
it is not my experience that this kind of idiom makes the
problem worse as the program gets bigger - as long as
some discipline is observed when connecting components,
it can result in very clean and modular code.
i generally try to live by the golden rule "avoid cycles whenever
possible". as long as you restrict yourself to acyclic topologies,
concurrency issues are minimal.
type Handler interface {
Handle(Event) Event
}
type HandlerFunc func(Event) Event
func (l ListenerFunc) Handle(e Event) Event {
return l(e)
}