当你第一步进入软件世界时,几乎不可能不遇到“不要重复自己”(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,你可以查看文档。

源代码

感谢阅读。