Elegant way to implement template method pattern in Golang
Asked Answered
P

3

7

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.

Pioneer answered 4/6, 2016 at 22:48 Comment(0)
H
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.

Hydrous answered 4/6, 2016 at 23:35 Comment(1)
Yes, now it works. But it still looks kind of ugly.. Thanks, anywayPioneer
P
9

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()
}
Primitivism answered 6/6, 2016 at 13:32 Comment(2)
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.Pioneer
@Pioneer Not necessarily, if you need a Logger with state then just inject in a function object, or interface instead of a pure function: play.golang.org/p/akMfRq8D5cYPrimitivism
H
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.

Hydrous answered 4/6, 2016 at 23:35 Comment(1)
Yes, now it works. But it still looks kind of ugly.. Thanks, anywayPioneer
P
4

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.

Pixie answered 16/10, 2016 at 16:52 Comment(1)
Regarding this affair in 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!Cumulus

© 2022 - 2024 — McMap. All rights reserved.