6

Is there an elegant canonical way to implement template method pattern in Go? In C++ this looks like this:

#include <iostream>
#include <memory>

class Runner {
public:
    void Start() {
        // some prepare stuff...
        Run();
    }
private:
    virtual void Run() = 0;
};

class Logger : public Runner {
private:
    virtual void Run() override {
        std::cout << "Running..." << std::endl;
    }
};

int main() {
    std::unique_ptr<Runner> l = std::make_unique<Logger>();
    l->Start();
    return 0;
}

In golang i wrote something like this:

package main

import (
    "fmt"
    "time"
)

type Runner struct {
    doRun func()
    needStop bool
}

func (r *Runner) Start() {
    go r.doRun()
}

func NewRunner(f func()) *Runner {
    return &Runner{f, false}
}

type Logger struct {
    *Runner
    i int
}

func NewLogger() *Logger {
    l := &Logger{}
    l.doRun = l.doRunImpl
    return l
}

func (l *Logger) doRunImpl() {
    time.Sleep(1 * time.Second)
    fmt.Println("Running")
}

func main() {
    l := NewLogger()
    l.Start()
    fmt.Println("Hello, playground")
}

But this code fails with runtime null pointer error. Basic idea is to mix in some functionality from derived classes (go structs) to the base class routine in a way that base class state is available from this mix-in derived routine.

Pallav Jha
  • 3,409
  • 3
  • 29
  • 52
user6256186
  • 103
  • 1
  • 5

3 Answers3

8

The essence of the template method pattern is it allows you to inject in an implementation of a particular function or functions into the skeleton of an algorithm.

You can achieve this in Go by injecting in a function or an interface into your Runner. To achieve the basic template method pattern you don't really need your Logger struct at all:

package main

import (
    "fmt"
)

type Runner struct {
    run func()
}

func (r *Runner) Start() {
    // some prepare stuff...
    r.run()
}

func runLog() {
    fmt.Println("Running")
}

func NewLogger() *Runner {
    return &Runner{runLog}
}

func main() {
    l := NewLogger()
    l.Start()
}
Chris Drew
  • 14,926
  • 3
  • 34
  • 54
  • In more complex cases we need access to the state of the Logger && Runner instance. In your code you could achive this only by passing Logger object to the runLog() function as an argument. I personally think that it is even more uglier than my example. – user6256186 Jun 06 '16 at 17:07
  • @user6256186 Not necessarily, if you need a `Logger` with state then just inject in a function object, or interface instead of a pure function: https://play.golang.org/p/akMfRq8D5cY – Chris Drew Oct 10 '18 at 16:59
5

Logger embeds a pointer which will be nil when you allocate the struct. That's because embedding does not put everything inside the struct, it actually creates a field (named Runner of type *Runner in your case) and the language gives you some syntactic sugar to access what's inside it. In your case it means that you can access Runner fields in two ways:

l := Logger{}
l.needStop = false
//or
l.Runner.needStop = false

To fix the error you need to allocate Runner field inside the Logger like so:

l := Logger{Runner:&Runner{}}

Or embed by value instead of pointer.

creker
  • 9,400
  • 1
  • 30
  • 47
3

The key to have the Template Method Design Pattern work in Golang is to properly use the embedding feature and the function assignment.

Below, a code snippet which works as expected.

package main

import (
    "fmt"
)

type Runner struct {
    run func()  // 1. this has to get assigned the actual implementation
}

func NewRunner(i func()) *Runner {
    return &Runner{i}
}

func (r *Runner) Start() {
    r.run()
}

type Logger struct {
    Runner
}

func NewLogger() *Logger {
    l := Logger{}
    l.run = l.loggerRun  // 2. the actual version is assigned
    return &l
}

func (l *Logger) loggerRun() {
    fmt.Println("Logger is running...")
}

func main() {
    l := NewLogger()  // 3. constructor should be used, to get the assignment working
    l.Start()
}

The type Runner defines a func() attribute which is supposed to receive the actual implementation, according to the specific subtype. Start() wraps call to run(), and once invoked on the right receiver (the base one) it is be able to run the right version of run(): this happens iff in the constructor (i.e. NewLogger()) the actual version of the method run() is assigned to the attribute run of the embedded type.

And, output is:

Logger is running...

Program exited.

Here the code can be run, and modified to test any other variant of this design pattern.

Paolo Maresca
  • 7,396
  • 4
  • 34
  • 30
  • Regarding this affair in https://groups.google.com/d/msg/golang-nuts/dzpJ_riiRZ4/WGnghewhMYUJ a person suggest to use another approach (define the template method in a function with an interface as parameter with the implemention to be invoked in the abstract steps of the template). For me, without further context, both works, Anyway your example is clever and show an use of having a function type inside a struct, thanks for sharing! – Victor Aug 03 '18 at 13:10