Golang Factory Method Pattern

Using the Factory Method Creational Design Pattern in Golang

The Factory Method Pattern is a creational design pattern that facilitates the use of a single product interface with different concrete implementations of said product. It does this by handling the creation of said product implementations.

This makes it possible to add additional concrete implementations of the target interface without modying client code, minimizing the amount of code changes needed to introduce or change functionality. This, in turn, minimizes the chances of introducing bugs into the application. If you are unfamiliar with interfaces in golang, see this post before proceeding.

Implementation Example

Let’s say we have an application that performs some business logic with a shape. So far, support for the concrete implementations circle and square have been added.

package main

import (
    "fmt"
    "math"
)

// product
type shape interface {
    area() float64
    draw()
}

// product implemented as circle
type circle struct {
    radius float64
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c circle) draw() {
    // logic for drawing a circle goes here
}

// product implemented as square
type square struct {
    length float64
}

func (s square) area() float64 {
    return s.length * s.length
}

func (s square) draw() {
    // logic for drawing a square goes here
}

// client code
func businessLogicArea(sh shape) {
    fmt.Printf("\nUsing shape area (%f) for business purposes\n", sh.area())
    fmt.Printf("Shape type is %T\n\n", sh)
}

// client code
func businessLogicDraw(sh shape) {
    fmt.Println("Using shape draw for business purposes")
    fmt.Printf("Shape type is %T\n\n", sh)
    sh.draw()
}

// client code
func main() {
    var inputShape string
    fmt.Println("Please input a type of shape:")
    fmt.Scanln(&inputShape)
    if inputShape == "circle" {
        var inputRadius float64
        fmt.Println("Please input a radius (e.g. 5.0): ")
        fmt.Scanln(&inputRadius)
        ci := circle{radius: inputRadius}
        businessLogicArea(ci)
        businessLogicDraw(ci)
    } else if inputShape == "square" {
        var inputLength float64
        fmt.Println("Please input a length (e.g. 5.0): ")
        fmt.Scanln(&inputLength)
        sq := square{length: inputLength}
        businessLogicArea(sq)
        businessLogicDraw(sq)
    } else {
        fmt.Printf("\nShape type not supported\n")
    }
}
Please input a type of shape:
circle
Please input a radius (e.g. 5.0): 
2.0

Using shape area (12.566371) for business purposes
Shape type is main.circle

Using shape draw for business purposes
Shape type is main.circle

If we wanted to add support for a new shape, the business logic functions already accept the shape interface so they don’t need to be changed, great! However, the if/else conditionals in our main() client code need to be updated anytime a new shape is added. It is pretty simple to add a new shape in this very basic example, but not so simple if we have a large codebase that has conditionals everywhere.

In the end, the best way to avoid bugs when adding new functionality is to make as little changes as possible. The less parts of the code you have to touch, the less the chances of introducing unwanted behavior. Our goal today will be the ideal: add a new shape implementation without touching any conditional logic at all, only cleanly adding new code by registering the implementation.

Factory Method Components

Our first step is to define and identify the three components of the factory method: the product, the client, and the creator. The product is the interface that is being implemented in varying ways. In this case, it’s the shape that is being implemented as a circle and square. The client is the code that is using the product and its implementations. In this case, that is main() and the business logic functions. Lastly, the creator is a class or function that returns a concrete implementation of the product (e.g. a circle or square). This is where the conditional if/else logic that determines what object is needed should live. Our code has the creator logic in the client code, so we’ll need to tackle that next.

Simple Factories

Let’s create simple factories (a.k.a. factory functions or factory constructors) that create our circle and square objects so we can handle the user input of shape dimensions there. This is an important step because it ensures that every instance of a circle or square are created the same way every time. That’s not hard to mess up in this simple example, but it’s not unheard of for structs to have tens if not hundreds of fields! Using factories allows you to ensure that all fields are set and that sane defaults are set for fields that are optional.

func getDimension(dimensionName string) float64 {
    var inputDimension float64
    fmt.Printf("Please input a %s (e.g. 5.0): ", dimensionName)
    fmt.Scanln(&inputDimension)
    return inputDimension
}

// usual go convention is for a factory function to return a pointer to the output instance
func newCircle() *circle {
    return &circle{
        radius: getDimension("radius"),
    }
}

// usual go convention is for a factory function to return a pointer to the output instance
func newSquare() *square {
    return &square{
        length: getDimension("length"),
    }
}

Creator

Next, let’s move the conditional if/else logic that handles what kind of shape to create to its own factory.

// creator
func newShape(shapeType string) (shape, error) {
    if shapeType == "circle" {
        return newCircle(), nil
    } else if shapeType == "square" {
        return newSquare(), nil
    } else {
        return nil, errors.New("Shape type not supported")
    }
}

Now our main() client code is much cleaner and easier to read

func main() {
    var inputShape string
    fmt.Println("Please input a type of shape:")
    fmt.Scanln(&inputShape)

    sh, err := newShape(inputShape)
    if err != nil {
        fmt.Printf("\n%s\n", err)
        return
    }

    businessLogicArea(sh)
    businessLogicDraw(sh)
}

Progress! Now we can add a new shape implementation (e.g. a triangle) without having to modify client code; all of the changes are on the creator side. However, we still need to modify the if/else conditionals in the creator shape factory function to add triangle support, so we still need a bit more work to be at the ideal place where we simply register new implementations.

Interface Factories

The next step is to slightly change the circle and square factories so they return a shape interface instead of pointers to structs. This changes them from factory functions to interface factories. Now we can introduce a shapeFactory type that is a function that takes in no inputs and returns a shape - which exactly describes our circle and square factories. This allows our factory functions to be considered the new shapeFactory type.

type shapeFactory func() shape

// we accept no inputs and return a shape interface so we are considered a shapeFactory
func newCircle() shape {
    return &circle{
        radius: getDimension("radius"),
    }
}

// we accept no inputs and return a shape interface so we are considered a shapeFactory
func newSquare() shape {
    return &square{
        length: getDimension("length"),
    }
}

Registration

Now we can keep track of the factories we have implemented as well as have a dedicated place where we can register new factories.

var shapeFactories = map[string]shapeFactory{}

// add new factory implementations here
func registerShapes() {
    shapeFactories["circle"] = newCircle
    shapeFactories["square"] = newSquare
}

// no more conditional if/else in our shape factory function
// just a map lookup
// creator
func newShape(shapeType string) (shape, error) {
    if _, ok := shapeFactories[shapeType]; ok {
        return shapeFactories[shapeType](), nil
    }
    return nil, errors.New("shape type not supported")
}

func main() {
    registerShapes()
    var inputShape string
    fmt.Println("Please input a type of shape:")
    fmt.Scanln(&inputShape)

    sh, err := newShape(inputShape)
    if err != nil {
        fmt.Printf("\n%s\n", err)
        return
    }

    businessLogicArea(sh)
    businessLogicDraw(sh)
}

If we want to add a triangle implementation, we add the needed structs and methods then register it with the addition of one line to the registerShapes() function.

// product implemented as triangle
type triangle struct {
    base   float64
    height float64
}

func (t triangle) area() float64 {
    return t.base * t.height / 2
}

func (t triangle) draw() {
    // logic for drawing a square goes here
}

// we accept no inputs and return a shape interface so we are considered a shapeFactory
func newTriangle() shape {
    return &triangle{
        base:   getDimension("base"),
        height: getDimension("height"),
    }
}

// add new factory implementations here
func registerShapes() {
    shapeFactories["circle"] = newCircle
    shapeFactories["square"] = newSquare
    shapeFactories["triangle"] = newTriangle
}

The finalized version of our app should now look like this

package main

import (
    "errors"
    "fmt"
    "math"
)

var shapeFactories = map[string]shapeFactory{}

// product
type shape interface {
    area() float64
    draw()
}

// no more conditional if/else in our shape factory function
// just a map lookup
// creator
func newShape(shapeType string) (shape, error) {
    if _, ok := shapeFactories[shapeType]; ok {
        return shapeFactories[shapeType](), nil
    }
    return nil, errors.New("Shape type not supported")
}

type shapeFactory func() shape

// product implemented as circle
type circle struct {
    radius float64
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c circle) draw() {
    // logic for drawing a circle goes here
}

// we accept no inputs and return a shape interface so we are considered a shapeFactory
func newCircle() shape {
    return &circle{
        radius: getDimension("radius"),
    }
}

// product implemented as a square
type square struct {
    length float64
}

func (s square) area() float64 {
    return s.length * s.length
}

func (s square) draw() {
    // logic for drawing a square goes here
}

// we accept no inputs and return a shape interface so we are considered a shapeFactory
func newSquare() shape {
    return &square{
        length: getDimension("length"),
    }
}

// product implemented as a triangle
type triangle struct {
    base   float64
    height float64
}

func (t triangle) area() float64 {
    return t.base * t.height / 2
}

func (t triangle) draw() {
    // logic for drawing a square goes here
}

// we accept no inputs and return a shape interface so we are considered a shapeFactory
func newTriangle() shape {
    return &triangle{
        base:   getDimension("base"),
        height: getDimension("height"),
    }
}

// add new factory implementations here
func registerShapes() {
    shapeFactories["circle"] = newCircle
    shapeFactories["square"] = newSquare
    shapeFactories["triangle"] = newTriangle
}

func getDimension(dimensionName string) float64 {
    var inputDimension float64
    fmt.Printf("Please input a %s (e.g. 5.0): ", dimensionName)
    fmt.Scanln(&inputDimension)
    return inputDimension
}

// client code
func businessLogicArea(sh shape) {
    fmt.Printf("\nUsing shape area (%f) for business purposes\n", sh.area())
    fmt.Printf("Shape type is %T\n\n", sh)
}

// client code
func businessLogicDraw(sh shape) {
    fmt.Println("Using shape draw for business purposes")
    fmt.Printf("Shape type is %T\n\n", sh)
    sh.draw()
}

// client code
func main() {
    registerShapes()
    var inputShape string
    fmt.Println("Please input a type of shape:")
    fmt.Scanln(&inputShape)

    sh, err := newShape(inputShape)
    if err != nil {
        fmt.Printf("\n%s\n", err)
        return
    }

    businessLogicArea(sh)
    businessLogicDraw(sh)
}
Please input a type of shape:
triangle
Please input a base (e.g. 5.0): 3
Please input a height (e.g. 5.0): 4

Using shape area (6.000000) for business purposes
Shape type is *main.triangle

Using shape draw for business purposes
Shape type is *main.triangle

As this example shows, the factory method creational design pattern can be used with Go to minimize the amount of code needed to add new functionality and thus minimize the introduction of bugs.

Additional Reading

Overview of the Factory method - link

Another use of a shape interface - link

Factory functions and interface factories - link

An example of registering factories written in Python - link

A slightly different factory approach that uses a shapeFactory interface with concrete circleFactory and squareFactory implementations. This setup is necessary if the shapeFactory creator interface defines more than one method. - link

A slightly different approach to product implementation that uses composition - link