Design Pattern Real-Life — Builder

Explorez l'implémentation du design pattern Builder en Go pour structurer la création de files d'attente RabbitMQ dans un environnement Kubernetes.

20 novembre 2023

Published

Hugo Mufraggi

Author

4 min read
Design Pattern Real-Life — Builder

Design Pattern Real Life — Builder

In this new article series, I will share how to implement design patterns to improve your code base.

For the first design pattern, I chose the builder pattern.

When to Use It

Use Case

Let’s consider a scenario where we use Go and RabbitMQ. Recently, I developed software for distributing tasks. I implemented this using Go RabbitMQ, and my services run in a Kubernetes cluster. Each listeners runs in a Go function.

The builder design pattern is a perfect fit for this software problem. We want to define N listeners, and I want to make it as structured as possible for future development.

Practice

The first question is what you want to factorize and for what. In my case, I want to normalize the definition of the queue and make the queue listener definition mandatory.

Here are the steps for my use case:

  • Define the channel
  • Add the queue definition
  • Add the listener
  • Build

I will create a struct applying this interface.

type QueueBuilder struct {
    consume <-chan amqp.Delivery
    queue   *amqp.Queue
    ch      *amqp.Channel
    errors  []error
}
type IQueueBuilder interface {
    InitChannel(conn *amqp.Connection) QueueBuilder
    AddQueueDef(name string, durable, autoDelete, exclusive, noWait bool, args amqp.Table) QueueBuilder
    AddListener() QueueBuilder
    Build() (<-chan amqp.Delivery, *amqp.Channel, []error)
}

My interface defines each function in my struct. And my struct has the following fields:

  • the RabbitMQ channel ch
  • the RabbitMQ queue definition queue
  • consume, a channel for receiving amqp.Delivery messages
  • errors, a list of error for error handling logic.

InitChannel

Nothing too complex here. I take the pointer to the RabbitMQ connection, and I obtain the channel using conn.Channel(). If conn.Channel() throws an error, I append the new error to my errors list, and I set b.ch to nil.

func (b QueueBuilder) InitChannel(conn *amqp.Connection) QueueBuilder {
    ch, err := conn.Channel()
    if err != nil {
        b.errors = append(b.errors, err)
        b.ch = nil
        return b
    }
    b.ch = ch
    return b
}

AddQueueDef

For the queue definition, I want to give my teams the choice to set up the configuration manually.

The rest of the code is straightforward. I check if ch is nil; if yes, I add a new error to errors, and I define my queue.

func (b QueueBuilder) AddQueueDef(
    name string,
    durable bool,
    autoDelete bool,
    exclusive bool,
    noWait bool,
    args amqp.Table) QueueBuilder {
    if b.ch == nil {
        b.errors = append(b.errors, errors.New("channel not initialized"))
        return b
    }
    queue, err := b.ch.QueueDeclare(
        name,
        durable,
        autoDelete,
        exclusive,
        noWait,
        args,
    )
    if err != nil {
        b.errors = append(b.errors, err)
    }
    b.queue = &queue
    return b
}

AddListener

For example, I want to lock the listener definition for the rest of my teams.

Like the previous function, I check if I don’t have an error and define my consumer.

func (b QueueBuilder) AddListener() QueueBuilder {
    if len(b.errors) != 0 {
        b.errors = append(b.errors, errors.New("error occurred in the previous step"))
        return b
    }
    msgs, err := b.ch.Consume(
        b.queue.Name,
        "",
        true,  // Auto-acknowledge
        false, // Exclusive
        false, // No local
        false, // No wait time
        nil,
    )
    if err != nil {
        b.errors = append(b.errors, err)
    }
    b.consume = msgs
    return b
}

Build

The build is the last step of your logic, and it can return what you want to build. In my case, I want to return the channel of my consumer, the RabbitMQ channel, and the errors.

func (b QueueBuilder) Build() (<-chan amqp.Delivery, *amqp.Channel, []error) {
    if len(b.errors) != 0 {
        return nil, nil, b.errors
    }
    return b.consume, b.ch, nil
}

Now, we can define our queues very easily.

conn, err := amqp.Dial(amqpURI)
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
var addQueue QueueBuilder
consume, ch, errs := addQueue.
    InitChannel(conn).
    AddQueueDef("add", false, false, false, false, nil).
    AddListener().
    Build()
if len(errs) != 0 {
    for _, err := range errs {
        log.Fatalf("%s", err)
    }
    panic("error for the 'add' queue")
}
var multQueue QueueBuilder
consumeMulti, chMulti, errs := multQueue.
    InitChannel(conn).
    AddQueueDef("mult", false, false, false, false, nil).
    AddListener().
    Build()
if len(errs) != 0 {
    for _, err := range errs {
        log.Fatalf("%s", err)
    }
    panic("error for the 'mult' queue")
}
var divQueue QueueBuilder
consumeDiv, chDiv, errs := divQueue.
    InitChannel(conn).
    AddQueueDef("div", false, false, false, false, nil).
    AddListener().
    Build()
if len(errs) != 0 {
    for _, err := range errs {
        log.Fatalf("%s", err)
    }
    panic("error for the 'div' queue")
}
// Start goroutines for each type of operation
go add(consume, ch)
go mult(consumeMulti, chMulti)
go div(consumeDiv, chDiv)
select {}

I define three queues and then provide my consumer and channel as arguments for each specific queue.

The developer needs to code the add function.

func add(consume <-chan amqp.Delivery, ch *amqp.Channel) {
    for d := range consume {
        var operation Operation
        err := json.Unmarshal(d.Body, &operation)
        failOnError(err, "Failed to unmarshal add message")
        result := operation.A + operation.B
        fmt.Printf(" [x] Addition Result: %d\\\\n", result)
        fmt.Printf("result: %v\\\\n\\\\n\\\\n", result)
        sendToQueue(ch, "mult", operation)
    }
}

Conclusion

The builder design pattern can be used in the front end as well. You can imagine a builder to normalize and centralize the card definition for alerting on your website.

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X)****, LinkedIn*, and* YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.