Factory Method Design Pattern in Go

Factory Method Design Pattern in Go

So, what is it about?

The Factory Method design pattern solves the problem of creating objects without specifying their concrete classes. It provides an interface to create them in a parent class but allows child classes to change the type of objects that will be created.

Interesting... How does it work?

Factory Method defines a method that should be used instead of the new operator. Child classes can override this method to change the class of the objects that will be created.

Here's a diagram to better understand it:

DBClientFactory-Page-2.png

Amazing! But how do I do that in Go?

So, as you can see it is almost impossible to implement the classic Factory Method pattern in Go, mostly because we don't have some basic OOP features, like classes and inheritance. But, we can still implement a basic version of the pattern, called the Simple Factory. To do this we can make use of composition in Go since the end goal of it is the same as that of inheritance, so instead of inheriting features from a parent class, larger objects are composed of other objects and thereby use their functionality.

Here's how it looks like in Go:

DBClientFactory-Factory Method (Go).png

Show me some code!

So we're not going to deep dive into a "real world" example right now, since the idea here is to better understand the concept behind the pattern, so what we're going to do is to translate the diagram above into real code, so you can get a grasp on how it would look like (I'll give you some tips below on when it'll be a good idea to use the Factory Method design pattern).

product_interface.go

package factory

type ProductInterface interface {
    MakeSomething(string) string
}

product.go

package factory

type Product struct {
    name string
}

func (p *Product) MakeSomething(feature string) string {
    return feature + " " + p.name
}

producta.go

package factory

type ProductA struct {
    Product
}

func NewProductA() ProductInterface {
    return &ProductA{
        Product: Product{
            name: "Product A",
        },
    }
}

productb.go

package factory

type ProductB struct {
    Product
}

func NewProductB() ProductInterface {
    return &ProductB{
        Product: Product{
            name: "Product B",
        },
    }
}

creator.go

package factory

type ProductType int

const (
    ProductAType ProductType = 1 << iota
    ProductBType
)

func NewProduct(t ProductType) ProductInterface {
    switch t {
    case ProductAType:
        return NewProductA()
    case ProductBType:
        return NewProductB()
    default:
        return NewProductA()
    }
}

main.go

package main

import (
    "fmt"
    "github.com/arielcr/factory-method-go/factory"
)

func main() {

    productA := factory.NewProduct(factory.ProductAType)
    fmt.Println(productA.MakeSomething("Awesome"))

    productB := factory.NewProduct(factory.ProductBType)
    fmt.Println(productB.MakeSomething("Nice"))

}

Here is the complete code repo.

When to use it?

  • When you don't know the exact types of objects your code should work with: since there is a separation from the code that creates the product from the one that actually uses it, it's easier to extend the construction code from the rest of the code. So if you want to add a new product type you just need to create the appropriate implementation.

  • When you want to provide a way to extend your code when it's used by other users: it's easier for other developers to add additional functionality to your code since all that is needed is to add new classes (structs).

Final Thoughts

  • By using the Factory Method Design Pattern you avoid tight coupling between the creator and the products
  • You are complying with the Single Responsibility Principle since you can move the product creation code into one place, making the code easier to support.
  • You are complying with the Open/Closed Principle because you can add new types of products into the program without breaking existing code.
  • One "bad" thing that I can think of is that your code becomes more complicated since you need to introduce new files and code to implement the pattern.