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

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 receivingamqp.Deliverymessageserrors, a list oferrorfor 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.