软件模块依赖关系管理与优化

注:英文引文,机翻未校。


Understanding Dependencies between Software Modules

理解软件模块之间的依赖关系

Miguel Pinilla
Feb 24, 2023

Introduction

引言
One of the most important decisions in the design of a system is to define what subsystems or modules depend on what other subsystems or modules. Identifying these dependencies is critical to ensure that a system is scalable (from a software engineering point of view) and maintainable.
在系统设计中,最重要的决策之一是定义哪些子系统或模块依赖于其他子系统或模块。识别这些依赖关系对于确保系统的可扩展性(从软件工程的角度来看)和可维护性至关重要。

Key Concepts of Dependency

依赖关系的关键概念

Some key ideas that come out of the concept of dependency:
依赖关系的概念产生了一些关键思想:

Client/Server Relationship

客户端 / 服务器关系
Client/Server: Two software modules are considered to be Client/Server of each other if the Client DEPENDS on the Server
客户端 / 服务器:如果客户端依赖于服务器,则认为两个软件模块是彼此的客户端 / 服务器。

Afferent and Efferent Dependencies

入口依赖与出口依赖

Afferent or Client Dependencies: Of a software module are all the other modules in the system that are Clients of the module in question. This may be an open set, and in many cases, it is even desirable for it to be open (e.g. a service that is available to any other module in the system, like a file system or a map API).
入口依赖或客户端依赖:一个软件模块的入口依赖是系统中所有作为该模块客户端的其他模块。这可能是一个开放的集合,在许多情况下,甚至希望它是开放的(例如,像文件系统或地图 API 这样的服务,可供系统中的任何其他模块使用)。

Efferent or Server Dependencies: Of a software module are all other modules that are Servers to this module. This is usually a closed set, as it is determined by the implementation of the module under consideration. There are some cases that seemingly violate this principle like in “Inversion of Control” or “Dependency Injection” patterns, but this is usually done by abstracting the dependency to a well defined interface and have the Server Dependencies expressed as “Every module that implements the interface”
出口依赖或服务器依赖:一个软件模块的出口依赖是所有对该模块提供服务的其他模块。这通常是一个封闭的集合,因为它是由所考虑模块的实现决定的。有一些情况似乎违反了这一原则,例如在 “控制反转” 或 “依赖注入” 模式中,但通常这是通过将依赖关系抽象到一个明确定义的接口,并将服务器依赖关系表达为 “实现该接口的每个模块” 来实现的。

Acyclic Dependencies

无环依赖的重要性

Acyclic dependencies: The Client/Server relationship defines a directed graph with the modules of the system as nodes and the existence of a Client/Server dependency as edges. If this graph is acyclic, the system (set of modules) forms a partially ordered set. This property of a system is extraordinarily important in the design of a system. Violations of this property result in difficulties in testing, diagnostic of error, deployment and system start up and a million other headaches. In some cases, a strict partial order is not possible to achieve. In this case system design should try to make those cycles as small as possible in the sense of graph theory (minimum number of nodes involved and shortest edge path/graph diameter). This allows the cyclic dependency to be confined to a subsystem and can be isolated for special treatment.
无环依赖:客户端 / 服务器关系定义了一个有向图,系统中的模块作为节点,客户端 / 服务器依赖关系的存在作为边。如果这个图是无环的,那么系统(模块集合)就形成了一个部分有序集。这一系统属性在系统设计中极为重要。违反这一属性会导致测试、错误诊断、部署和系统启动等方面的困难,以及无数其他问题。在某些情况下,无法实现严格的偏序。在这种情况下,系统设计应尽量使这些循环在图论意义上尽可能小(涉及的节点数量最少,边路径 / 图直径最短)。这使得循环依赖可以被限制在一个子系统中,并可以进行特殊处理。

Understanding the Dependency Relationship

理解依赖关系

All these concepts routinely used in system analysis and design, rely on a crisp understanding of the “Dependency” relationship between two software modules.
所有这些在系统分析和设计中经常使用的概念,都依赖于对两个软件模块之间 “依赖关系” 的清晰理解。

Definition of Dependency

依赖关系的定义

The definition that I find most useful for this relationship is:
我发现对这种关系最有用的定义是:

A module (“A”) depends on another module (“B”) if the behavior of “A” relies on specific behaviors of B and requires knowledge of the functionality and protocol of “B” at design, build or deployment time.
如果模块 “A” 的行为依赖于模块 “B” 的特定行为,并且在设计、构建或部署时需要了解模块 “B” 的功能和协议,那么模块 “A” 就依赖于模块 “B”。

This is not an operational definition, as it just pushes the core of the meaning into the word “knowledge”. So a more operational definition is:
这不是一个操作性定义,因为它只是将含义的核心推到了 “知识” 这个词上。因此,一个更具操作性的定义是:

A module (“A”) depends on another module (“B”) if “A” executes Operations on “B” and “A” expects to receive Notifications from “B”
如果模块 “A” 在模块 “B” 上执行操作,并且模块 “A” 期望从模块 “B” 接收通知,那么模块 “A” 就依赖于模块 “B”。

Operations and Notifications

操作与通知

Where:
其中:

Operation: is a request to perform a well-defined action or set of actions by a module and a corresponding response by that module. The actions and response are defined in the Interface of the module that supports the operation and they are described in terms and concepts that are part of the module’s contract of behavior (e.g. it may express an action as sending an e-mail, but typically not “invoking the SendGrid API”, the first relies on “shared” concepts between modules, the second is an implementation detail of the module).
操作:是一个模块请求执行一个明确定义的动作或一组动作以及该模块的相应响应。动作和响应是在支持该操作的模块的接口中定义的,它们是用模块行为契约中的术语和概念描述的(例如,它可以将一个动作表达为发送电子邮件,但通常不是 “调用 SendGrid API”,前者依赖于模块之间的 “共享” 概念,后者是模块的实现细节)。

Typically, operations are expressed by request/response pairs either synchronously executed (where the caller blocks until the response arrives) or asynchronously (the caller is unblocked as soon as the request is sent, but it is expected to be able to receive the response).
通常,操作通过请求 / 响应对来表达,可以同步执行(调用者在响应到达之前被阻塞)或异步执行(请求发送后调用者立即解除阻塞,但预计能够接收响应)。

Notification: is a message sent from a module to a destination endpoint without any expectations of response or associated behaviors by the receiver. Typically Notifications are asynchronous (although not always, which can lead to other problems) and do not have a “response” value to be provided by the receiver. Notifications can be implemented using a variety of protocols. Two of them are worth highlighting:
通知:是一个模块向目标端点发送的消息,不期望接收者做出响应或相关行为。通常,通知是异步的(尽管并非总是如此,这可能会导致其他问题),并且接收者不需要提供 “响应” 值。通知可以使用多种协议来实现。其中两种值得关注:

Point-to-Point or “Observer Pattern” notifications: The endpoint to which notifications are to be sent is provided to the module at runtime via a separate operation. The operation that provides the endpoint can also accept additional arguments to define the behavior of the module regarding the notifications to emit, their frequency, format, etc… The underlying protocol may provide a “delivery response” to the module (e.g. Webhooks using a POST operation), but this does not break the requirement that the application layer of the receiving endpoint has any obligation to process or acknowledge the notification to the sender and the behavior of the server is independent of what the client does.
点对点或 “观察者模式” 通知:在运行时,通过一个单独的操作将通知要发送到的端点提供给模块。提供端点的操作还可以接受额外的参数,以定义模块关于要发出的通知的行为、频率、格式等…… 底层协议可能会向模块提供一个 “投递响应”(例如,使用 POST 操作的 Webhooks),但这并不违反接收端点的应用层对发送者没有任何处理或确认通知的义务,服务器的行为与客户端的行为无关。

Topic or Channel notifications: The module publishes a topic/endpoint in advance (design time) and Client modules can listen to that endpoint to receive the notifications that the module will be emitting (publishing). This mechanism usually does not allow for filtering of notifications at the Server at runtime, although the underlying communication mechanisms may provide some form of filtering and message transformation.
主题或通道通知:模块提前(设计时)发布一个主题 / 端点,客户端模块可以监听该端点以接收模块将发出(发布)的通知。这种机制通常不允许在服务器运行时过滤通知,尽管底层通信机制可能会提供某种形式的过滤和消息转换。

Expressing Module Contracts

模块契约的表达

Neither programming languages nor API definition languages provide for a direct expression of the Operation/Notification composition of a module definition of its external contract. The specifics of how to express this duality depends on the specific technology used, but in general it needs to be defined in two parts:
编程语言和 API 定义语言都没有直接表达模块定义的外部契约的操作 / 通知组合。如何表达这种二元性的具体细节取决于所使用的特定技术,但通常需要分两部分来定义:

Operations are expressed as a straight Request/Response API that the module actually implements. Examples in different technologies:
操作被表达为模块实际实现的直接请求 / 响应 API。不同技术中的示例:

  • gRPC Services
    gRPC 服务

  • OAS3 endpoints
    OAS3 端点

Programming Language Interfaces

编程语言接口

Notifications are expressed as a separate API consisting exclusively of primitives (technology dependent) that DO NOT return a value. At most, they can return an “acknowledgement” of sending completion. This API is not to be implemented by the module, but it is an specification to be implemented by any other module that intends to depend on the module that “publishes” the API. Similarly, they can be expressed using technology specific constructs
通知被表达为一个独立的 API,该 API 仅由不返回值的原语(技术依赖)组成。最多,它们可以返回一个 “发送完成” 的 “确认”。这个 API 不应由模块实现,而是由任何打算依赖于 “发布” 该 API 的模块的其他模块实现。同样,它们可以使用特定于技术的构造来表达:

  • gRPC Services
    gRPC 服务

  • OAS3 endpoints that do not specify the servers in the schema. In particular, OAS3 in its version 3.10, it explicitly models webhooks which allow to keep the operations and notifications specification together.
    OAS3 端点在模式中不指定服务器。特别是,OAS3 在其 3.10 版本中明确建模了 Webhooks,允许将操作和通知规范保持在一起。

  • Unimplemented programming language interfaces.
    未实现的编程语言接口。

In addition to the published API in the appropriate technology, the Server module can also publish one or several “SDK” libraries for different languages to facilitate interaction with the underlying communication protocol (e.g. service stubs, marshalling and unmarshalling of types from/to the wire format, etc…)
除了在适当技术中发布的 API 外,服务器模块还可以发布一种或多种针对不同语言的 “SDK” 库,以便于与底层通信协议进行交互(例如,服务桩、类型从 / 到线格式的序列化和反序列化等……)

Practical Application in Software Engineering

在软件工程中的实际应用
In order to use this concept of dependency in an effective way in a large Software Engineering project, it will be necessary to develop conventions and templates of use, together with tools that understand them to facilitate both the definition and the use of services expressed in this approach.
为了在大型软件工程中有效使用这种依赖概念,将有必要开发使用规范和模板,并结合理解它们的工具,以便于以这种方式表达的服务的定义和使用。


软件系统设计中的关键策略

一、客户端 / 服务器关系

客户端 / 服务器关系是软件系统中常见的交互模式,用于描述两个模块之间的依赖关系。

  • 客户端(Client):请求服务的一方,依赖服务器提供的功能来完成操作。客户端类型包括桌面客户端、移动客户端、Web 客户端等。
  • 服务器(Server):提供服务的一方,为客户端提供所需功能或资源。
  • 特点
    • 单向依赖:客户端依赖服务器,但服务器不依赖客户端。
    • 通信协议:通过 HTTP、TCP/IP 等协议交互。
    • 解耦:客户端和服务器通常解耦,可在不同机器上运行。
  • 架构模式
    • 三层架构:将应用分为表示层、业务逻辑层和数据访问层。
    • RESTful API:使用 HTTP 方法进行资源操作的接口设计风格。

二、减少模块间的出口依赖

减少模块间的出口依赖有助于降低系统耦合度,提高可维护性和可扩展性。

(一)引入中间层(服务层)

  • 定义:通过中间层解耦模块间的直接依赖。
  • 操作
    • 提取公共功能至中间层。
    • 定义通用接口供模块调用。
  • 示例:以订单服务和支付服务为例,订单服务不直接依赖支付服务,而是依赖一个支付接口。中间层负责根据配置选择具体的支付实现(例如支付宝或微信支付)。

(二)使用依赖注入

  • 定义:通过依赖注入框架(如 Spring)将依赖关系从模块内部转移到外部。
  • 操作
    • 在配置文件或注解中定义依赖关系。
    • 在运行时动态注入依赖。
  • 优点:增强了代码的可测试性和可配置性。

(三)设计通用接口

  • 定义:设计通用接口,避免模块直接调用具体实现。
  • 操作
    • 使用抽象类或接口定义交互。
    • 分离接口实现与定义。
  • 优点:允许在不修改客户端代码的情况下替换不同的实现。

(四)使用观察者模式

  • 定义:一个对象(主题)维护一系列依赖于它的对象(观察者),当主题状态改变时,会通知所有观察者。
  • 操作:定义主题接口和观察者接口,主题维护一个观察者列表,并在状态改变时通知所有观察者。
  • 优点:解耦了主题和观察者,主题不需要知道具体观察者的类型。

(五)使用发布/订阅模式 (Pub/Sub)

  • 定义:消息发布者 (Publisher) 将消息发布到消息队列,消息订阅者 (Subscriber) 从消息队列订阅感兴趣的消息。
  • 操作:使用消息中间件(例如 RabbitMQ, Kafka)作为消息队列。
  • 优点:进一步解耦了发布者和订阅者,发布者不需要知道有哪些订阅者,订阅者也不需要知道消息来自哪个发布者。

三、避免循环依赖

循环依赖会导致系统难以理解和维护,甚至无法正常运行。以下是一些避免循环依赖的方法:

(一)理解依赖关系

  • 定义:在设计系统前,分析模块间的依赖关系,明确职责。
  • 操作
    • 绘制依赖图或使用依赖分析工具。
    • 确保模块职责清晰,避免重叠。

(二)合理划分模块

  • 定义:遵循单一职责原则,合理划分模块。
  • 操作
    • 将复杂模块拆分为多个子模块,使每个模块职责单一。

(三)引入中间层

  • 定义:通过中间层协调模块间的交互,避免直接依赖。
  • 操作
    • 提取公共功能至中间层。
    • 定义通用接口供模块调用。

(四)使用依赖注入

  • 定义:通过依赖注入框架管理模块间的依赖关系。
  • 操作
    • 在配置文件或注解中定义依赖关系。
    • 在运行时动态注入依赖。

(五)重构代码

  • 定义:发现循环依赖时,通过重构代码解决。
  • 操作
    • 提取公共功能至新模块。
    • 调整模块间的调用关系,打破循环依赖。

(六)持续监控和优化

  • 定义:持续监控模块间的依赖关系,及时发现和解决问题。
  • 操作
    • 编写单元测试和集成测试,检查依赖关系是否符合预期。
    • 定期运行依赖分析工具,检测潜在的循环依赖。

(七)应用依赖倒置原则 (DIP)

  • 定义:高层模块不应该依赖低层模块,二者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
  • 操作:抽取公共接口,使模块之间通过接口进行通信,降低模块间的耦合度。

四、优化模块间的接口依赖

优化模块间的接口依赖有助于提高系统的可维护性、可扩展性和可测试性。

(一)明确接口职责

  • 定义:每个接口应只负责一个功能或一组相关功能。
  • 操作
    • 拆分职责不单一的接口,使其功能更加明确。
    • 遵循单一职责原则 (SRP,Single Responsibility Principle )。

(二)使用抽象层

  • 定义:通过抽象接口隐藏具体实现细节。
  • 操作
    • 使用抽象类或接口定义交互,客户端只与抽象接口进行交互。

(三)减少接口的复杂性

  • 定义:尽量减少接口中的方法数量,使接口简洁。
  • 操作
    • 拆分方法过多的接口,避免接口过于臃肿。
    • 使用数据传输对象(DTO)封装输入输出,简化接口参数。

(四)提高接口的通用性

  • 定义:设计通用接口,使其能被多个模块调用。
  • 操作
    • 避免接口依赖于特定模块的实现细节,提高接口的复用性。
    • 使用泛型,使接口能够处理不同类型的数据。

(五)确保接口的稳定性

  • 定义:接口定义完成后,尽量减少变更。
  • 操作
    • 设计接口时充分考虑未来需求,预留扩展空间。
    • 提供版本控制,当接口发生变化时,保证兼容性。

(六)提供错误处理机制

  • 定义:接口应提供统一的错误处理机制。
  • 操作
    • 定义统一的错误码和错误信息,方便客户端识别和处理错误。
    • 定义自定义异常,根据不同的错误类型抛出相应的异常。

(七)支持异步和同步调用

  • 定义:接口应支持同步和异步调用。
  • 操作
    • 提供同步和异步接口,满足不同场景的需求。

(八)安全性

  • 定义:接口应该考虑安全性问题。
  • 操作:进行身份验证 (Authentication) 和授权 (Authorization),确保只有合法的用户或模块才能访问接口。

(九)持续优化和监控

  • 定义:通过性能测试和优化,确保接口的高效性。
  • 操作
    • 使用压力测试工具测试接口性能,找出性能瓶颈并进行优化。
    • 使用监控工具监控接口调用情况,及时发现和解决问题。

(十)可观测性

  • 定义:监控接口的运行状态和性能指标。
  • 操作:使用日志记录 (Logging)、指标收集 (Metrics) 和分布式追踪 (Distributed Tracing) 等手段,对接口进行全面的监控和分析。

五、总结

在软件系统设计中,应用客户端/服务器模式,借助中间层、依赖注入解耦模块,设计通用接口,运用观察者与发布/订阅模式,遵循单一职责、接口隔离、依赖倒置等原则,并持续监控优化,可有效优化模块依赖关系。

这些策略能降低系统复杂度,提升可扩展性、可测试性与可重用性,从容应对业务需求变化。构建高质量软件系统是复杂过程,需综合考量架构、设计、工程实践与持续改进。

具体通过明确接口职责、用抽象层隐藏细节、简化接口、增强通用性、保障稳定性、设错误处理机制、支持异步同步调用等操作,助力打造健壮、灵活、可维护的系统,实现应对业务变化的高效软件系统目标。


via:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值