In its simplest form a better abstraction would be to use a simple func()
type for the handlers, and if parameters are required, callers should pass a closure that captures the required arguments. So you don't have to care about them at all, and you don't have to resort to using reflection.
Allowing these functions to report success or failure is something reasonable and desirable, so use a function type func() error
.
A possible implementation:
type Task struct {
handler func() error
}
func (t *Task) execute() {
if err := t.handler(); err != nil {
fmt.Println("Failed to execute task:", err)
}
}
Now let's create example jobs:
func simpleJob() error {
fmt.Println("simple")
return nil
}
func complexJob(s string) {
fmt.Println("complex", s)
}
This is how we can use them in Task
s:
t := &Task{handler: simpleJob}
t.execute()
t = &Task{handler: func() error {
complexJob("data")
return nil
}}
t.execute()
Which outputs (try it on the Go Playground):
simple
complex data
We were "lucky" with simpleJob()
: its signature matched what we needed for the handlers, so we could use it as-is. Not like complexJob()
which has a completely different signature: it has a string
param and does not return an error
, but with an anonymous function (a closure) we could still use it for Task.handler
.
If more control or reporting capabilities are required from the jobs, then a proper interface should be created for them, and values of such interfaces should be submitted for execution, not just simple functions.
For example:
type Job interface{
Execute() error
Progress() int
Result() interface{}
}
type Task struct {
job Job
}
func (t *Task) execute() {
if err := t.job.Execute(); err != nil {
fmt.Println("Failed to execute task:", err)
}
}