65.9K
CodeProject 正在变化。 阅读更多。
Home

模块化不仅仅是代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (5投票s)

2017年5月25日

CPOL

9分钟阅读

viewsIcon

12644

让我们看看在模块化系统时需要考虑的各种因素。

点击此处免费下载 IncrediBuild,加速您的 C++ 开发.

将大型项目分解为更易于维护的模块,不仅仅是跨文件分割代码。它涉及组件化、分布式、开发者考量、调试的便捷性、测试的有效性、性能关注点、脚本、工具等等。然后,在所有这些之后,您可以考虑代码本身的结构。让我们看看在模块化系统时需要考虑的各种因素。

类 != 文件

让我们从大型软件开发中的一个重要观点开始:良好的面向对象设计并不等同于理想的代码模块化。简而言之,这意味着可以将单个 C++ 类分解为多个源文件。您甚至可以将类分解到不同的目录(或其他语言中的包)中。事实上,这是 C++ 相对于 Java 的一个优势;它允许您在文件系统方面拥有不同的逻辑和物理类表示。让我们看看实现这一目标的一些策略。

文件长度考量

将代码分解到多个文件中,可以简单地基于文件长度。这里有不同的理念:代码不超过一两屏,到定义一个离散的类或模式所需的行数。最好的答案是介于两者之间的指导方针。其他模块化方法基于系统的逻辑和物理分解,例如组件化,我们将在后面讨论。

考虑组件和子系统

组件化涉及将系统分解为执行特定功能的逻辑单元,这些单元可以包含多个源文件。例如,您可以使用接口(通过 IDL 或抽象基类)和具体类来分离契约与实现,或者使用构成单个视觉组件的用户界面小部件,但这些小部件本身由多个代码模块组成。这两个示例在模块化方面都可以实现一对多的关系(参见图 1)。

图 1 - 单个组件(即类或 UI 组件)在文件方面可以拥有一对多的关系。

拥抱包/命名空间/层级结构

包与组件不同,更不用说源文件了。包介于两者之间。然而,开发人员通常对它们考虑很少,因为他们仅仅将包等同于目录。但这正是关键所在。通过抽象进行组织是代码的核心,而包应该成为您策略的重要组成部分。

对于直接支持包的语言,首先从包定义开始,而不是源文件。对于不明确支持包的语言,例如 C++,可以通过命名空间、目录或某种层级结构(如继承或组合)来强制执行。例如,列表 1 中的代码使用命名空间来帮助模块化类 foo 和 bar。

namespace my_module { 
    class foo { };
    class bar { };
}
列表 1 - 使用 C++ 命名空间通过强制层级结构来模块化代码。

在此基础上,您可以将模块化的类放在与命名空间名称匹配的目录中。进一步来说,这种模式有助于将遗留代码与新的、增强的代码区分开来,如列表 2 所示。

namespace MyServer {
    void doThis();
    void doThat();
}

namespace NewServer {
    MyServer oldServer;

    void doSomethingNew();
    void doSsomethingElseNew();
}
列表 2 - C++ 命名空间有助于将遗留代码与新代码进行模块化。

同样,使用与命名空间匹配的模块化目录结构有助于进一步区分新的 C++ 代码和旧的 C++ 代码。同样的目录方法也可以应用于 C++ 继承树。

JavaScript 不是 HTML

避免仅仅因为方便就将代码放在某个位置。虽然我们在这里关注 C++ 代码,但让我们看一个 JavaScript 示例来举例说明——例如,将 JavaScript 嵌入到使用它的网页中是一种基于方便的常见做法,但这也会限制代码的可重用性和可查找性,并可能阻碍协作。

将代码分解为合适的模块,可以让开发团队分别处理它们,增加了重用的可能性,甚至可能带来新的收入来源,因为相关代码可能具有超越您正在构建的系统的价值。这对于采用服务化架构的公司以及 API 设计来说是常见做法。

代码不仅仅是代码

在模块化方面,不要忘记其他系统组件,如构建脚本(例如 make, ant, maven, gradle)、自动化测试、部署脚本、文档和代码生成工具(例如 UML 和 IDL)等等。

声明和定义

其他效果良好的模块化指南包括:

  • 可以分离时就分离:您不必在声明类定义的同一模块、组件或文件中包含该类定义代码。幸运的是,C++ 在这方面有所帮助,因为通常有单独的头文件和源文件,但也有一些情况,较小的类被就地定义,或声明为内联。不要为了微小的性能提升而牺牲模块的正确性和可读性。
  • 合理时就合并:另一方面,如果合理,可以将一组较小的相关类定义在单个头文件中,或者将它们的公共方法放在单个源文件中。这里的通用指南是:如果您不违反面向对象设计、文件长度考量和前面描述的组件化规则,请随时合并。
  • 避免编译时耦合:一般来说,对具体类的任何修改,如果需要客户端代码(即使用该类的代码)重新编译,则表示紧密耦合。使用某种形式的动态类加载,例如使用抽象基类(参见上图 1)和工厂类,在模块化方面非常有帮助。这也有助于在由于系统增长需要进一步模块化时进行未来的重构。

到目前为止,我们已经研究了用于模块化的代码和整体软件设计因素。让我们退后一步,接下来看看其他因素。

敏捷性考量

敏捷开发方法论,凭借其基于冲刺的周期、反馈循环和高度协作,已在开发社区中盛行。这种开发方法会对您的整体系统模块化产生影响。

快速迭代

敏捷开发要求更快速、更迭代的开发和部署方法。您系统的结构会影响您的组织将变得多么灵活和敏捷。从长远来看,通过子系统部署的效率和支持来构建您的代码将会有所帮助。例如,确保您的代码分解尽可能鼓励并行和基于团队的努力。

当在源、组件、子系统甚至服务器级别出现争用时,就会产生瓶颈,开发人员会因此受阻。将与软件设计相同的迭代方法应用于模块化——在迭代过程中,根据经验和反馈进行小但持续的更改和优化。

持续开发

使用敏捷和相关的 DevOps 通常受益于持续开发系统。这意味着您可能需要更好地模块化和构建您的代码,以便与现代构建工具和环境协同工作。其好处包括高效、可靠和自动化的构建与部署。

协作与结对编程

适当的模块化有助于通过额外的协作、远程程序员在家工作或程序员结对工作来促进敏捷。一些公司将其推向极致,通过程序员轮班和地理资源分布来实现 24 小时开发周期。所有这些都要求适当的设计、模块化和工具。

为测试而构建

编写代码只是软件开发生命周期的一部分。您需要设计和模块化您的系统,以便能够对其进行适当的测试。以下是一些在这样做时需要考虑的要点:

  • 注意您的依赖关系:构建相互直接依赖的组件会导致耦合。在测试方面,这会扩大工作量,因为一个组件的更改将意味着对其他组件进行回归测试。通过关注点分离和其他设计模式,您可以进行模块化以避免这种情况。
  • 支持黑盒测试:通过服务和 API 驱动的设计进行的组件化有助于您单独测试离散组件,并在集成测试之前证明它们是稳定的。
  • 考虑调试的便捷性:您的系统模块化是阻碍还是有助于调试工具的使用?您能否轻松地跟踪和描述代码执行流程?当错误发生时,它如何帮助您轻松识别和限制错误?通过正确的结构,您可以实现所有这些,并帮助新开发人员更快地学习您的系统。

总的来说,避免那些使调试和测试整个系统变得困难的组件化模式。这包括过度使用 C++ 宏、特定于平台的组件模型、依赖注入等等。同样,在整体设计与模块化之间存在需要考虑的权衡。

性能考量

到目前为止,我们已经研究了分解系统的原因和方法,以及如何进行有意义的模块化。然而,在您做出的每个决定中,都不要忘记考虑性能。例如,良好的软件设计利用抽象来正确分解复杂性,但由此产生的一些技术和模块化也会影响性能。这些包括:

  • 过多的数据复制:过多的模块化和组件化的过度使用可能导致过度的分布式和数据编组(复制)。这会严重影响性能,特别是当涉及到转换和安全问题时。
  • 过多的缓存:与前一点相关,当模块化和抽象被推向极致时,可能会产生额外的数据缓存层。不利影响包括内存使用量大、对适用语言的垃圾回收产生负面影响等等。
  • 集成托管代码的能力:超越 C++,其他流行的托管代码平台(例如 Java、Ruby、Python 等)只有在您的 C++ 系统代码以正确的方式进行模块化后,才能轻松高效地集成。

为未来增长构建

到目前为止,我们已经研究了在模块化系统时需要考虑的代码、开发方法、测试和性能因素。影响您模块化策略的另一个重要因素是未来增长的考量。最重要的因素包括:

  • 模块化以实现重用:对新系统或不断增长的系统相对较小的代码库施加结构可能看起来是过度设计,但其好处很快就会得到回报。
  • 单体与库:使用“构建块”方法通过开发内部库来构建大型软件系统很少被使用,但它会导致一个非常易读且易于维护的系统。
  • 未来分布:即使您系统的所有部分今天都一起运行,也要计划它们将来可能如何分布到服务器甚至数据中心。
  • 平台隔离:考虑未来的操作系统移植,甚至语言移植(例如 C++ -> Java,或 Java -> C#)。现代系统很少用单一语言或平台构建。在您的模块化工作中考虑全栈开发。
  • 云和移动:您的系统(或其部分)可能有多容易迁移到云端?您能否让代码作为移动应用程序的一部分运行?考虑现在可以做什么来实现这两者,同时使自己免受未来技术变革的完全颠覆。

总而言之,考虑模块化或重构工作永远不会太早,也不会太晚。始终考虑结构和模块化,因为它涉及许多因素,包括协作、性能、测试、系统组件的未来重构,以及将组件分发到云端和移动设备的能力。

© . All rights reserved.