弱耦合提高软件可靠性设计

本文讨论了弱耦合设计在软件开发中的重要性,包括增强模块化、提高可测试性、简化维护和提升可扩展性。通过接口隔离、依赖注入、事件驱动架构以及中间件和消息队列的使用,可以实现弱耦合,从而打造更可靠和健壮的软件系统。

63d7f927aab5dd8ca444b5af43579886.gif

【编者按】这篇文章详细讨论了弱耦合设计原则在软件开发中的重要性。弱耦合是一种设计原则,使得软件组件、模块或系统之间的关系保持在一个较低的知识定义层面。这样,每个组件对其他组件的内部工作几乎一无所知,它们通过简单、明确定义的接口进行交互。文章还讨论了实现弱耦合的方法,包括接口隔离、依赖注入、事件驱动架构以及中间件和代理的使用。这些方法都有助于提高软件的模块化、可测试性、可维护性和可扩展性,从而提高软件的可靠性和健壮性。

原文链接:https://www.codereliant.io/keep-it-flexible-how-loose-coupling-boosts-software-reliability/

未经允许,禁止转载!

作者 | CodeReliant 社区       译者 | 明明如月

责编 | 夏萌

出品 | 优快云(ID:优快云news)

随着软件日益复杂化,软件组件间的交互方式变得越发重要。弱耦合是应对这种复杂性的关键策略之一。本文是我们可靠软件设计原则(链接见文底)系列的第三篇,将探讨弱耦合的概念以及它如何促进可靠和健壮的软件设计。

105f5bda7a32b453d4322b12f594b3da.png

什么是弱耦合?

弱耦合是一种关于软件组件、模块或系统间关系的设计原则。在弱耦合的设计中,每个组件对其他独立组件的内部情况的了解被降至最低。它们通过简单且明确定义的接口进行交互,而无需了解其他组件的内部工作机制。

5b320959df3fbd5b45c6f9407d447c52.png

弱耦合的好处是什么?

1. 增强了模块化

当组件弱耦合时,它们可以独立地进行开发、测试、修改,甚至替换,从而实现更高的模块化。这种模块化简化了开发过程,特别是对于大型团队而言,不同的模块可以由不同的团队或开发人员同时进行开发。

2. 增强了可测试性

弱耦合提高了通过单元测试进行测试的能力。由于组件之间的关系不相互交织,所以可以对它们进行单独的测试,确保它们能正确地执行所定义的功能。

3. 更易维护

维护工作变得更加简单,因为修改一个组件并不会对其他组件产生连锁反应。如果需要更新或替换一个组件,最小化对其他组件的破坏风险。

4. 提高了可扩展性

弱耦合的系统更具可扩展性。它们可以轻松地分布在多台服务器上,甚至位于不同的地理位置。这是微服务架构的关键原则,旨在设计可扩展且灵活的系统。

e6a44d3f658795c061ad77a2691e69bc.png

如何实现弱耦合?

1. 接口分离

在面向对象编程领域,接口分离原则(ISP)指出,我们不应该强迫客户端依赖那些它并未使用的接口。通过将较大的接口分割为较小、更具体的接口,我们可以防止系统的某一部分变化对其他部分造成影响。

例如,对于一个电商应用,我们可能有一个User接口。最初,它可能如下所示:

type User interface {
    Register(email string, password string)
    Login(email string, password string)
    AddToCart(itemID string)
    Checkout(cartID string)
}

这个接口包含了很多功能,从用户的注册和登录到购物车操作的处理。这违反了接口分离原则。更好的方式是将该接口拆分为更小、更具体的接口,每个接口处理特定的任务:

type Account interface {
    Register(email string, password string)
    Login(email string, password string)
}


type Cart interface {
    AddToCart(itemID string)
    Checkout(cartID string)
}

通过将最初的User接口拆分为Account和Cart两个独立的接口,分别用于处理用户账户和购物车操作。这种做法更符合接口分离原则。

2.依赖注入

依赖注入(DI)是一种技术,它让对象通过外部获得其依赖,而不是自行创建。这种做法减少了类之间严格的依赖关系,有助于实现低耦合。

让我们考虑一个Store结构体,它依赖于ProductRepository来检索产品。

type ProductRepository interface {
    GetProductByID(id string) (Product, error)
}


type Store struct {
    productRepo ProductRepository
}


func NewStore() *Store {
    return &Store{
        productRepo: NewProductRepository(),
    }
}

在这个例子中,Store直接在内部创建了依赖于ProductRepository。这是一种紧耦合的设计。通过使用依赖注入,我们可以把这个依赖作为参数传入Store的构造函数,如下:

func NewStore(productRepo ProductRepository) *Store {
    return &Store{
        productRepo: productRepo,
    }
}

这样,ProductRepository就通过构造函数被注入到Store了。这样做将ProductRepository的创建与Store解耦。

许多库都可以帮助实现依赖注入,使得在软件设计中应用起来更加简洁。例如,在 Go 语言中,流行的选择包括  wire 和dig。Wire 是由 Google 创建的库,采用编译时依赖注入的方式,提供了类型安全的方法,确保您的应用程序在构建时满足其依赖项。另一方面,Dig 是由 Uber 开发的库,它在运行时进行依赖注入,为那些依赖项在静态时期无法确定的情况提供了更大的灵活性。这两个库都提供了强大且方便的方式来实现依赖注入,进一步推动了低耦合,提高了软件的整体可靠性。

3. 事件驱动架构

在事件驱动架构中,各个组件是通过事件进行通信的。这种模式使得各个组件在无需了解其他组件的情况下,可以独立地发送或消费事件,从而达到了弱耦合的目标。

让我们考虑一个名为ProductAddedToCart的事件。我们可以定义如下:

type ProductAddedToCart struct {
    ProductID string
    CartID    string
}


type Event interface {
    Handle()
}


type ProductAddedToCartHandler struct {
    event ProductAddedToCart
}


func (handler *ProductAddedToCartHandler) Handle() {
    // 处理事件
    fmt.Println("Product added to cart: ", handler.event.ProductID)
}


func NewProductAddedToCartHandler(event ProductAddedToCart) *ProductAddedToCartHandler {
    return &ProductAddedToCartHandler{
        event: event,
    }
}

在上述代码中,ProductAddedToCartHandler是响应ProductAddedToCart事件的组件,它的Handle()函数可以执行多种操作,如更新数据库、发送通知等,从而实现了组件间的弱耦合交互。

4. 中间件和消息队列的使用

中间件和代理可以充当组件或服务之间的中介。它们可以防止组件之间直接依赖对方,从而降低了耦合。代理在消息驱动的架构中非常常见,而中间件在 Web 和 API 框架中也是一个常见的概念。

b079d29c856fdd836af569934c7d4981.png

在下图中,客户端的请求会经过两个中间件层后到达服务器,每个中间件层都会在将请求传递给下一层之前进行处理。同样,服务器的响应在发送到客户端之前,也会经过每个中间件层进行处理。

中间件

中间件可以用于处理多种任务,例如授权、错误处理、日志记录、速率限制和缓存等。它们可用于处理应用程序各层间的公共关注问题。例如,授权中间件用于检查用户是否经过认证并具有访问特定资源的相应权限,从而为应用程序提供一致的安全层。同样地,错误处理中间件可以一致地捕获和处理异常,确保用户收到有意义且一致的错误消息。日志记录中间件用于记录与请求和响应相关的关键信息,有助于调试和 性能监控。速率限制中间件可以通过在特定时间段内限制客户端的请求次数,防止 API 被滥用。最后,缓存中间件可以通过存储查询代价较大的操作结果,并在相同请求再次发生时重用这些结果,提高应用程序的性能。

下面是一个使用基本内存缓存的简单电商产品目录缓存中间件示例:

package main


import (
    "encoding/json"
    "net/http"
    "time"
)


// 一个简单的内存缓存
var cache = make(map[string][]byte)
var cacheDuration = 5 * time.Minute


type Product struct {
    ID    string  `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}


func productHandler(w http.ResponseWriter, r *http.Request) {
    // 假设这个函数从数据库中查询出产品
    products := []Product{
        {ID: "1", Name: "Product 1", Price: 100.0},
        {ID: "2", Name: "Product 2", Price: 200.0},
    }


    data, err := json.Marshal(products)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }


    w.Header().Set("Content-Type", "application/json")
    w.Write(data)
}


func cachingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if data, found := cache[r.RequestURI]; found {
            w.Header().Set("Content-Type", "application/json")
            w.Write(data)
            return
        }


        rec := &responseRecorder{ResponseWriter: w, status: http.StatusOK}
        next.ServeHTTP(rec, r)


        // 只有当状态码为 200 时才缓存响应
        if rec.status == http.StatusOK {
            cache[r.RequestURI] = rec.body.Bytes()
            // 在 cacheDuration 后删除缓存的项目
            time.AfterFunc(cacheDuration, func() {
                delete(cache, r.RequestURI)
            })
        }
    })
}


type responseRecorder struct {
    http.ResponseWriter
    body   bytes.Buffer
    status int
}


func (rec *responseRecorder) Write(b []byte) (int, error) {
    rec.body.Write(b)
    return rec.ResponseWriter.Write(b)
}


func (rec *responseRecorder) WriteHeader(statusCode int) {
    rec.status = statusCode
    rec.ResponseWriter.WriteHeader(statusCode)
}


func main() {
    http.Handle("/products", cachingMiddleware(http.HandlerFunc(productHandler)))
    http.ListenAndServe(":8080", nil)
}

在调用下一个处理器之前,这个缓存中间件会先检查请求的 URI 是否在缓存中。如果数据在缓存中,它直接将数据返回给客户端。如果不在,它会调用下一个处理器,并在状态码为 200(OK)时将响应存储在缓存中。缓存的数据会在cacheDuration之后自动删除。

请注意,这只是一个基本的缓存和简单的例子。在实际生产环境中,您应该使用更健壮的缓存解决方案,例如Redis  或 Memcached 。

消息队列

在软件架构的上下文中,消息队列也是一种中间件,可以将一个系统的请求转发到另一个系统,实现不同系统之间的无缝通信。这种方法常用于事件驱动和面向服务的架构中,以解耦的方式共享数据。

举个例子,设想有一个产生数据的系统(发布者或生产者)和另一个需要消费该数据的系统(订阅者或消费者),这两个系统无需直接通信。生产者将数据发送给消息代理,而消费者从代理获取数据。通过这种机制,生产者和消费者可以独立演化,无需协调彼此的变化,从而实现弱耦合。

常见的消息代理服务包括 RabbitMQ、Apache Kafka 和 AWS SQS(Simple Queue Service)。这些服务提供了强大的消息处理功能,如持久化消息以确保数据的耐久性、基于模式的消息路由,以及在网络中断或消费者下线时确保消息传递。

3f2876f8f431618684821214fd3f0e9c.png

在上图中,生产者将消息发送到 Kafka 代理,代理将这些消息存储在一个 Topic 中。然后,消费者从该 Topic 获取消息,由应用程序进行处理。

18325590ef016382b77d465d4f25188a.png

结论

如果能够合理运用弱耦合,就可以显著提高软件系统的可靠性和健壮性。通过在软件设计中考虑弱耦合,可以使系统具有灵活性、可扩展性、易维护性和可测试性,这些特性对于可靠的软件设计至关重要。在不断发展的软件开发领域,理解和应用像弱耦合这样的原则对于每位软件工程师都是非常重要。

你是否还知道其他降低耦合的方法?欢迎在评论区分享讨论。

参考链接:

1、可靠软件设计原则:https://www.codereliant.io/principles-of-reliable-software-design-part-1/

2、模块化:https://www.codereliant.io/modularity-a-pillar-of-reliable-software-design/

3、wire:https://github.com/google/wire?ref=codereliant.io

4、dig:https://github.com/uber-go/dig?ref=codereliant.io

5、Redis:https://redis.io/?ref=codereliant.io

6、Memcached:https://memcached.org/?ref=codereliant.io

推荐阅读:

曾与 HarmonyOS 一较高下的 Fuchsia,再失利!

亚马逊云科技重磅推出七项生成式 AI 新功能

姜宁两度当选 Apache 软件基金会董事 | 开源英雄

粉丝福利:

5724a2ce3d38b04b82544ea948f948d6.png

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

优快云资讯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值