Builder Pattern เป็นรูปแบบที่ใช้งานเมื่อการสร้างอ๊อบเจ๊คมีพารามิเตอร์หลายตัวหรือมีขั้นตอนซับซ้อน การแยกการตั้งค่าออกเป็นเมทอดแบบ chain จะช่วยให้โค้ดอ่านง่าย ลดโค้ดซ้ำ และจัดการค่าของ struct ได้เป็นระบบ ใน Go แนวทางนี้ทำให้โค้ดการสร้างตัวแปรที่ซับซ้อนดูสวยงามและสะอาดขึ้น

ตัวอย่างนี้จะใช้ไลบรารี github.com/ibm-messaging/mq-golang/v5/ibmmq เพื่อสาธิตการสร้างการเชื่อมต่อและการประกาศข้อความด้วย Builder


สมมติว่าเราต้องการเชื่อมต่อกับ Queue Manager โดยมีค่าที่ต้องกำหนดอย่าง QueueManager, ConnectionName, Channel, UserID และ Password หากเขียนโค้ดตรง ๆ เราต้องเตรียม MQCNO, MQCD และ MQCSP ด้วยตัวเอง ซึ่งทำให้โค้ดยาวและอ่านยาก

package main

import (
    "fmt"
    "log"

    ibmmq "github.com/ibm-messaging/mq-golang/v5/ibmmq"
)

func buildConnectionDirectly() (*ibmmq.MQQueueManager, error) {
    cno := ibmmq.NewMQCNO()
    cno.Options = ibmmq.MQCNO_CLIENT_BINDING

    cd := ibmmq.NewMQCD()
    cd.ChannelName = "DEV.APP.SVRCONN"
    cd.ConnectionName = "localhost(1414)"
    cno.ClientConn = cd

    csp := ibmmq.NewMQCSP()
    csp.UserId = "app"
    csp.Password = "password"
    cno.SecurityParms = csp

    qm, err := ibmmq.Connx("QM1", cno)
    if err != nil {
        return nil, err
    }
    return qm, nil
}

func main() {
    qm, err := buildConnectionDirectly()
    if err != nil {
        log.Fatalf("failed to connect to MQ: %v", err)
    }
    defer qm.Disc()

    fmt.Println("connected to MQ queue manager", qm.Name)
}

โค้ดตัวอย่างนี้ทำงานได้ดี แต่รายละเอียดการสร้าง connection ถูกกระจายอยู่หลายจุด เมื่อเราต้องการเพิ่มตัวเลือก เช่น SSLConfig หรือ Timeout โค้ดก็จะยิ่งเดินหน้าลำบาก


Builder Pattern จะย้ายรายละเอียดการประกอบค่าไปไว้ใน Builder แล้วเปิด API ให้ฝั่งเรียกใช้งานอ่านง่ายขึ้น

package main

import (
    "fmt"
    "log"

    ibmmq "github.com/ibm-messaging/mq-golang/v5/ibmmq"
)

type MQConnectionBuilder struct {
    qMgrName      string
    connectionName string
    channelName   string
    userID        string
    password      string
}

func NewMQConnectionBuilder() *MQConnectionBuilder {
    return &MQConnectionBuilder{}
}

func (b *MQConnectionBuilder) WithQueueManager(name string) *MQConnectionBuilder {
    b.qMgrName = name
    return b
}

func (b *MQConnectionBuilder) WithConnectionName(conn string) *MQConnectionBuilder {
    b.connectionName = conn
    return b
}

func (b *MQConnectionBuilder) WithChannel(channel string) *MQConnectionBuilder {
    b.channelName = channel
    return b
}

func (b *MQConnectionBuilder) WithCredentials(userID, password string) *MQConnectionBuilder {
    b.userID = userID
    b.password = password
    return b
}

func (b *MQConnectionBuilder) Build() (*ibmmq.MQQueueManager, error) {
    cno := ibmmq.NewMQCNO()
    cno.Options = ibmmq.MQCNO_CLIENT_BINDING

    cd := ibmmq.NewMQCD()
    cd.ChannelName = b.channelName
    cd.ConnectionName = b.connectionName
    cno.ClientConn = cd

    if b.userID != "" || b.password != "" {
        csp := ibmmq.NewMQCSP()
        csp.UserId = b.userID
        csp.Password = b.password
        cno.SecurityParms = csp
    }

    return ibmmq.Connx(b.qMgrName, cno)
}

func main() {
    builder := NewMQConnectionBuilder().
        WithQueueManager("QM1").
        WithConnectionName("localhost(1414)").
        WithChannel("DEV.APP.SVRCONN").
        WithCredentials("app", "password")

    qm, err := builder.Build()
    if err != nil {
        log.Fatalf("failed to connect to MQ: %v", err)
    }
    defer qm.Disc()

    fmt.Println("connected to MQ queue manager", qm.Name)
}

ข้อดีของ Builder คือโค้ดฝั่งใช้งานย่อมอ่านง่ายขึ้น และรายละเอียดการสร้าง connection ถูกซ่อนอยู่ใน Builder ทำให้เราสามารถเพิ่มตัวเลือกใหม่ได้โดยไม่ต้องแก้โค้ดใน main


อีกงานที่ Builder Pattern เหมาะมากคือการประกอบข้อความที่จะส่งใน IBM MQ เรามักต้องกำหนดค่าใน MQMD และ MQPMO หากใช้ Builder โค้ดฝั่งส่งจะดูชัดเจนและโฟกัสที่ค่าที่เปลี่ยนแปลงได้

package main

import (
    "github.com/ibm-messaging/mq-golang/v5/ibmmq"
)

type MQPutMessageBuilder struct {
    md      *ibmmq.MQMD
    pmo     *ibmmq.MQPMO
    message []byte
}

func NewMQPutMessageBuilder() *MQPutMessageBuilder {
    return &MQPutMessageBuilder{
        md:  ibmmq.NewMQMD(),
        pmo: ibmmq.NewMQPMO(),
    }
}

func (b *MQPutMessageBuilder) WithFormat(format string) *MQPutMessageBuilder {
    b.md.Format = format
    return b
}

func (b *MQPutMessageBuilder) WithPersistence(persistence int32) *MQPutMessageBuilder {
    b.md.Persistence = persistence
    return b
}

func (b *MQPutMessageBuilder) WithReplyToQueue(replyTo string) *MQPutMessageBuilder {
    copy(b.md.ReplyToQName[:], []byte(replyTo))
    return b
}

func (b *MQPutMessageBuilder) WithMessage(body []byte) *MQPutMessageBuilder {
    b.message = body
    return b
}

func (b *MQPutMessageBuilder) Build() (*ibmmq.MQMD, *ibmmq.MQPMO, []byte) {
    return b.md, b.pmo, b.message
}

เมื่อใช้งาน Builder ฝั่งส่งข้อความก็จะเหมือนเป็นการประกาศค่าที่อ่านได้ง่าย ไม่ต้องกลับไปจัดการโครงสร้าง MQMD / MQPMO ซ้ำ ๆ

func sendMessage(qm *ibmmq.MQQueueManager, queueName string) error {
    qObject, err := qm.Open(ibmmq.NewMQOD().SetObjectName(queueName), ibmmq.MQOO_OUTPUT)
    if err != nil {
        return err
    }
    defer qObject.Close(0)

    md, pmo, body := NewMQPutMessageBuilder().
        WithFormat("MQSTR").
        WithPersistence(ibmmq.MQPER_PERSISTENT).
        WithReplyToQueue("REPLY.QUEUE").
        WithMessage([]byte("hello from builder pattern")).
        Build()

    if err := qObject.Put(md, pmo, body); err != nil {
        return err
    }
    return nil
}

Builder Pattern เหมาะกับงานที่ต้องสร้างตัวแปรหลายตัวและต้องจัดการกับ API ที่ซับซ้อนอย่าง IBM MQ เพราะมันช่วยแยกส่วนการตั้งค่าจากส่วนที่เรียกใช้งานจริง ทำให้โค้ดฝั่งใช้งานดูสะอาดขึ้น โดยไม่ต้องรู้รายละเอียดภายในของ MQCNO, MQCD, MQMD หรือ MQPMO


Unit testing

Builder Pattern ทำให้การเขียนเทสต์ง่ายขึ้น เพราะเราไม่จำเป็นต้องพึ่งพา MQ server จริงในเทสต์ ตัวอย่างนี้เป็น unit test ของ MQPutMessageBuilder เพื่อยืนยันว่า Build() คืนค่า MQMD และ payload ตามที่ตั้งไว้

package main

import (
    "testing"
    "github.com/ibm-messaging/mq-golang/v5/ibmmq"
)

func TestMQPutMessageBuilder_Build(t *testing.T) {
    b := NewMQPutMessageBuilder().
        WithFormat("MQSTR").
        WithPersistence(ibmmq.MQPER_NOT_PERSISTENT).
        WithReplyToQueue("REPLY.Q").
        WithMessage([]byte("payload"))

    md, _, body := b.Build()

    if md.Format != "MQSTR" {
        t.Fatalf("expected format MQSTR, got %s", md.Format)
    }
    if string(body) != "payload" {
        t.Fatalf("expected body 'payload', got %s", string(body))
    }

    reply := string(md.ReplyToQName[:])
    if len(reply) == 0 || reply[:7] != "REPLY.Q" {
        t.Fatalf("reply-to queue not set correctly: %q", reply)
    }
}

สรุปแล้ว Builder Pattern เป็นเครื่องมือที่ช่วยให้การสร้างอ๊อบเจ๊คที่มีพารามิเตอร์หลายตัวหรือมีขั้นตอนซับซ้อนดูเป็นระเบียบมากขึ้น ใน Go เราสามารถใช้เมทอดแบบ chain เพื่อแยกการตั้งค่าออกจากการทำงานจริง ทำให้โค้ดอ่านง่าย ลดโค้ดซ้ำ และเพิ่มความยืดหยุ่นในการปรับเปลี่ยนหรือเพิ่มตัวเลือกใหม่ในอนาคต