当你第一步进入软件世界时,几乎不可能不遇到“不要重复自己”(DRY)这个术语。这是因为我们在项目中使用函数、抽象等,以避免重复。然而,在每种情况下,这个想法总是有效的吗?
例如,考虑系统的两个不同部分。它们具有不同的业务逻辑,但在不同的上下文中使用相同的函数来解决各种问题。这种情况下,我们应该在这些不同的部分中使用共享库以防止代码重复吗?如果你的答案是“是”,那么在阅读本文时,你需要重新思考一下你的答案。
在这两个部分中使用相同的库会增加依赖和耦合度。这是因为它们具有不同的业务逻辑,可以根据业务环境的需要独立发展。棒极了!你减少了代码行数,但增加了耦合度和维护成本。
耦合是衡量两个组件彼此了解和相互依赖程度的一种方式。耦合越高,依赖性越强。松耦合是指不同组件尽可能地互相了解得少,而没有耦合则是指它们完全不知道彼此的存在。[1]
由于耦合和依赖问题,在分布式架构中,著名的原则是“写一次或写两次”(WET)。“WET”的关键点是减少共享代码的数量。然而,完全防止代码重用是不可能的。因此,在分布式系统中使用一些技术来管理代码重用。
今天,我将根据《软件架构的难点:面向分布式架构的现代权衡分析》一书,介绍其中的一种技术:共享库。
共享库尽管共享库帮助我们管理代码重用,但它也存在权衡。当我们使用共享库时,需要考虑依赖管理和更改控制。
我们将研究两个用例,以了解依赖管理和更改控制之间的权衡。
在第一种情况下,你有五个服务使用同一个共享库。
图像:一个自定义的共享库
- 当共享库更改发生时,每个服务最终都必须采用更改,因为库的版本随着时间的推移而被弃用。在这种情况下,我们必须在每次更改时重新测试和重新部署应用程序。我们需要确保是否有任何已弃用的功能。应用程序可以使用不同的版本;然而,当这个版本被弃用时,我们的服务将不再工作。
- 另一个缺点是,当库因服务需求而更改时,我们失去了库的通用性,最终包含了其他服务的混乱和无关紧要的代码块。这不是我们想要的。
在第二种情况下,我们有五个服务和四个不同的共享库。
图像:多个自定义的共享库
这些不同的服务使用不同的库。看起来很混乱,对吧?如你所料,依赖管理在这种情况下变得更加困难,因为与前面的例子相比,我们的服务依赖于多个库:共享库越多,依赖性就越多。
另一方面,更改控制和可维护性变得更容易。
然而,不要忘记,共享总是增加我们决定共享的耦合度:在两种情况下,耦合和依赖都会增加。
尽管有关共享库的所有批评,有时它对技术目的可能是有益的。例如,如果我们过多地使用RabbitMQ SDK,我们可以编写一个包装器,并将其功能提取到库中,然后我们可以将此库用于我们的服务。
让我们看看如何使用Golang进行此实现。
创建库我们需要创建一个适当版本的标签来共享此代码。
package rabbitmqsdk
import (
"github.com/labstack/gommon/log"
amqp "github.com/rabbitmq/amqp091-go"
)
type RabbitMQ struct {
connection *amqp.Connection
channel *amqp.Channel
connURL string
errCh <-chan *amqp.Error
messageChan <-chan amqp.Delivery
retryAttempt int
}
type RabbitMQOptions struct {
URL string
RetryAttempt int
}
func NewRabbitMQ(options RabbitMQOptions) (*RabbitMQ, error) {
rabbitMQ := &RabbitMQ{
connURL: options.URL,
retryAttempt: options.RetryAttempt,
}
if err := rabbitMQ.connect(); err != nil {
return nil, err
}
return rabbitMQ, nil
}
func (rmq *RabbitMQ) connect() error {
var err error
rmq.connection, err = amqp.Dial(rmq.connURL)
if err != nil {
log.Error("Error when creating connection", err.Error())
return err
}
rmq.channel, err = rmq.connection.Channel()
if err != nil {
log.Error("Error when creating channel", err.Error())
return err
}
rmq.errCh = rmq.connection.NotifyClose(make(chan *amqp.Error))
return nil
}
func (rmq *RabbitMQ) reconnect() {
attempt := rmq.retryAttempt
for attempt != 0 {
log.Info("Attempting rabbitmq reconnection")
if err := rmq.connect(); err != nil {
attempt--
log.Error("Rabbitmq retry connection error", err.Error())
continue
}
return
}
if attempt == 0 {
log.Fatal("Rabbitmq retry connection is failed")
}
}
func (rmq *RabbitMQ) Close() {
rmq.channel.Close()
rmq.connection.Close()
}
func (rmq *RabbitMQ) ConsumeMessageChannel() (jsonBytes []byte, err error) {
select {
case err := <-rmq.errCh:
log.Warn("Rabbitmq comes error from notifyCloseChan", err.Error())
rmq.reconnect()
case msg := <-rmq.messageChan:
return msg.Body, nil
}
return nil, nil
}
func (rmq *RabbitMQ) CreateQueue(name string, durable bool, autoDelete bool, exclusive bool, noWait bool, args map[string]interface{}) (amqp.Queue, error) {
queue, err := rmq.channel.QueueDeclare(name, durable, autoDelete, exclusive, noWait, args)
if err != nil {
log.Error("Error when creating queue", err.Error())
return amqp.Queue{}, err
}
return queue, nil
}
func (rmq *RabbitMQ) CreateExchange(name string, kind string, durable bool, autoDelete bool, internal bool, noWait bool, args map[string]interface{}) error {
if err := rmq.channel.ExchangeDeclare(name, kind, durable, autoDelete, internal, noWait, args); err != nil {
log.Error("Error when creating exchange", err.Error())
return err
}
return nil
}
func (rmq *RabbitMQ) BindQueueWithExchange(queueName string, key string, exchangeName string, noWait bool, args map[string]interface{}) error {
if err := rmq.channel.QueueBind(queueName, key, exchangeName, noWait, args); err != nil {
log.Error("Error when queue binding", err.Error())
return err
}
return nil
}
func (rmq *RabbitMQ) CreateMessageChannel(queue string, consumer string, autoAck bool, exclusive bool, noLocal bool, noWait bool, args map[string]interface{}) error {
var err error
rmq.messageChan, err = rmq.channel.Consume(queue, consumer, autoAck, exclusive, noLocal, noWait, args)
if err != nil {
log.Error("Error when consuming message", err.Error())
return err
}
return nil
}
创建版本和标签
要创建版本,我们需要遵守行业标准。一种流行的版本控制标准是语义化版本控制。
图:语义化版本控制
你可以查看文档以了解主要版本、次要版本和修补版本以及它们如何使用。
在实现阶段,我按照下面的方式执行:
图:语义化版本控制的实现
使用库package main
import (
"fmt"
rabbitmq_sdk "github.com/dilaragorum/rabbitmq-sdk"
)
func main() {
fmt.Println("Selam")
options := rabbitmq_sdk.RabbitMQOptions{
URL: "amqp://guest:guest@localhost:5672/",
RetryAttempt: 5,
}
rabbitMQ, err := rabbitmq_sdk.NewRabbitMQ(options)
if err != nil {
return
}
queue, err := rabbitMQ.CreateQueue("queue2", true, true, false, false, nil)
if err != nil {
fmt.Println(err.Error())
return
}
err = rabbitMQ.CreateExchange("exchange2", "fanout", true, true, false, false, nil)
if err != nil {
fmt.Println(err.Error())
return
}
err = rabbitMQ.BindQueueWithExchange(queue.Name, "", "exchange2", false, nil)
if err != nil {
fmt.Println(err.Error())
return
}
err = rabbitMQ.CreateMessageChannel(queue.Name, "", true, false, false, false, nil)
if err != nil {
return
}
for {
message, err := rabbitMQ.ConsumeMessageChannel()
if err != nil {
return
}
fmt.Println(string(message))
}
}
go get
replace
replace github.com/username/repo => /path/to/local/fork
奖励:在Github Action中添加lint
如果你在GitHub中查看我们的库,你会看到一个只有一个名为“lint”的步骤的流水线。要在GitHub的CI中实现lint,你可以查看文档。
源代码感谢阅读。