个性化阅读
专注于IT技术分析

创建无依赖性的真正模块化代码

本文概述

开发软件很棒, 但是……我想我们都可以同意, 这可能有点令人激动。一开始, 一切都很棒。你可以在几天甚至几天内一次又一次地添加新功能。你一切顺利!

快进几个月, 你的开发速度会下降。是因为你没有像以前那样努力吗?并不是的。让我们再快几个月, 你的开发速度会进一步下降。在这个项目上工作变得不再有趣, 并且变得拖累了。

模块化代码设计的抽象描述

情况变得更糟。你开始发现应用程序中的多个错误。通常, 解决一个错误会创建两个新错误。此时, 你可以开始唱歌:

代码中的99个小错误。 99个小虫子。记下来, 修补一下, …在代码中有127个小错误。

你现在对这个项目的工作感觉如何?如果你像我一样, 你可能会开始失去动力。开发此应用程序很痛苦, 因为对现有代码的每次更改都可能带来不可预测的后果。

这种经验在软件界很常见, 可以解释为什么这么多的程序员想要扔掉他们的源代码并重写所有内容。

软件开发随着时间推移而变慢的原因

那么, 这个问题的原因是什么呢?

主要原因是复杂性增加。根据我的经验, 整体复杂性的最大贡献者是这样一个事实, 即在绝大多数软件项目中, 所有事物都是相互联系的。由于每个班级都具有依赖性, 因此, 如果你更改了该班级中发送电子邮件的代码, 则你的用户突然无法注册。这是为什么?因为你的注册代码取决于发送电子邮件的代码。现在, 如果不引入错误, 你将无法进行任何更改。根本不可能跟踪所有依赖项。

所以你有它;问题的真正原因是由于代码所具有的所有依赖关系而增加了复杂性。

大泥球及其减少方法

有趣的是, 这个问题已经有很多年了。这是一种常见的反模式, 称为”大泥巴”。多年来, 我在多个不同公司从事的几乎所有项目中都看到过这种架构。

那么, 这种反模式到底是什么呢?简而言之, 当每个元素与其他元素有依赖关系时, 你将陷入困境。在下面, 你可以看到来自著名的开源项目Apache Hadoop的依赖关系图。为了可视化大泥球(或更确切地说, 大纱球), 你绘制了一个圆并将项目中的类均匀地放置在其上。只需在彼此依赖的每对类之间画一条线。现在你可以看到问题的根源。

Apache Hadoop的"泥潭"的可视化

Apache Hadoop的”大泥巴”

模块化代码的解决方案

所以我问自己一个问题:是否可以降低复杂性, 并且仍然像项目开始时一样开心?说实话, 你无法消除所有复杂性。如果要添加新功能, 则始终必须提高代码复杂性。但是, 复杂性可以移动和分离。

其他行业如何解决此问题

考虑一下机械行业。当一些小型机械制造厂制造机器时, 他们购买一组标准元素, 创建一些自定义元素, 然后将它们组合在一起。他们可以完全独立地制造这些组件, 并在最后组装所有组件, 而仅需进行一些调整。这怎么可能?他们知道如何通过固定的行业标准(例如螺栓尺寸)以及预先决定(例如安装孔的尺寸和它们之间的距离)将每个元素组合在一起。

机制的技术图及其各部分的装配方式

上面组装中的每个元素都可以由一个单独的公司提供, 该公司完全不了解最终产品或其其他零件。只要每个模块化元素都是根据规格制造的, 你就可以按计划创建最终设备。

我们可以在软件行业复制吗?

我们当然可以!通过使用接口和反转控制原理;最好的部分是, 这种方法可以在任何面向对象的语言中使用:Java, C#, Swift, TypeScript, JavaScript, PHP-清单不胜枚举。你不需要任何精美的框架即可应用此方法。你只需要遵守一些简单的规则并保持纪律。

控制反转是你的朋友

当我第一次听说控制反转时, 我立即意识到找到了解决方案。这是采用现有依赖关系并通过使用接口将其反转的概念。接口是方法的简单声明。他们没有提供任何具体的实施方式。结果, 它们可以用作两个元素之间关于如何连接它们的协议。如果愿意, 它们可以用作模块化连接器。只要一个元素提供接口, 另一个元素提供接口的实现, 它们就可以一起工作, 而彼此之间一无所知。这个棒极了。

让我们看一个简单的示例, 我们如何解耦系统以创建模块化代码。下图已实现为简单的Java应用程序。你可以在此GitHub存储库中找到它们。

问题

假设我们有一个非常简单的应用程序, 仅由Main类, 三个服务和一个Util类组成。这些元素以多种方式相互依赖。在下面, 你可以看到使用”大泥巴”方法的实现。类之间相互简单地调用。它们紧密耦合, 你不能简单地去除一个元素而不碰其他元素。使用此样式创建的应用程序使你可以快速成长。我相信这种风格适用于概念验证项目, 因为你可以轻松地玩转事物。不过, 它不适合用于生产环境的解决方案, 因为即使维护也很危险, 而且任何单个更改都可能产生无法预测的错误。下图显示了这个泥浆建筑的大球。

"大泥巴"风格架构的简单示意图

为什么依赖注入完全错误

为了寻求更好的方法, 我们可以使用一种称为依赖注入的技术。此方法假定应通过接口使用所有组件。我读过一些声称它使元素分离的说法, 但是确实如此吗?不, 请看下面的图表。

将依赖注入添加到泥泞大球中的示意图

当前情况和一个大麻烦之间的唯一区别是, 现在, 我们通过它们的接口而不是直接调用类, 而不是直接调用类。它稍微改善了彼此分离的元素。例如, 如果你想在另一个项目中重用服务A, 则可以通过取出服务A本身以及接口A, 接口B和接口实用程序来实现。如你所见, 服务A仍然取决于其他元素。结果, 我们仍然遇到在一个地方更改代码而在另一个地方弄乱行为的问题。它仍然会产生一个问题, 即如果你修改服务B和接口B, 则需要更改所有依赖于它的元素。这种方法无法解决任何问题;在我看来, 它只是在元素之上添加了一层接口。你永远不应注入任何依赖关系, 而应该彻底摆脱它们。为独立而欢呼!

模块化代码的解决方案

我相信解决所有主要依赖问题的方法是完全不使用依赖关系。你创建一个组件及其侦听器。侦听器是一个简单的界面。每当需要从当前元素外部调用方法时, 只需将方法添加到侦听器并改为调用它即可。该元素仅允许使用文件, 其包内的调用方法以及使用主框架或其他使用的库提供的类。在下面, 你可以看到将应用程序修改为使用元素架构的图表。

修改为使用元素架构的应用程序图

请注意, 在这种体系结构中, 只有Main类具有多个依赖关系。它将所有元素连接在一起, 并封装了应用程序的业务逻辑。

另一方面, 服务是完全独立的元素。现在, 你可以从该应用程序中取出每个服务, 然后在其他地方重用它们。他们不依赖其他任何东西。但是, 等待, 它会变得更好:只要你不更改服务的行为, 就无需再次修改这些服务。只要这些服务按其应有的方式工作, 它们就可以一直使用到时间结束。可以由专业的软件工程师来创建它们, 也可以是第一次编写的编码器, 它折衷到任何有混合goto语句的人都煮过的最糟糕的意大利面条代码。这没关系, 因为它们的逻辑是封装的。尽管可能如此可怕, 但它永远不会扩散到其他阶层。这也使你能够在多个开发人员之间拆分项目中的工作, 每个开发人员都可以独立地在自己的组件上工作, 而无需打扰另一个开发人员甚至不知道其他开发人员的存在。

最后, 你可以再开始编写一次独立代码, 就像在上一个项目开始时一样。

元素图案

让我们定义结构元素模式, 以便我们能够以可重复的方式创建它。

元素的最简单版本由两部分组成:主元素类和侦听器。如果要使用元素, 则需要实现侦听器并调用主类。这是最简单的配置图:

应用程序内单个元素及其侦听器的图

显然, 你最终需要为元素增加更多的复杂性, 但是你可以轻松地做到这一点。只要确保你的逻辑类都不依赖项目中的其他文件即可。他们只能在此元素中使用主框架, 导入的库和其他文件。对于资产文件, 例如图像, 视图, 声音等, 也应将其封装在元素中, 以便将来易于重用。你只需将整个文件夹复制到另一个项目中就可以了!

在下面, 你可以看到显示更高级元素的示例图。请注意, 它包含一个正在使用的视图, 并且不依赖于任何其他应用程序文件。如果你想知道一种检查依赖关系的简单方法, 只需查看导入部分。当前元素之外是否有文件?如果是这样, 则需要通过将这些依赖项移到元素中或向侦听器添加适当的调用来删除这些依赖项。

复杂元素的简单示意图

让我们看一个用Java创建的简单” Hello World”示例。

public class Main {

  interface ElementListener {
    void printOutput(String message);
  }

  static class Element {

    private ElementListener listener;

    public Element(ElementListener listener) {
      this.listener = listener;
    }

    public void sayHello() {
      String message = "Hello World of Elements!";
      this.listener.printOutput(message);
    }
  }

  static class App {

    public App() {
    }

    public void start() {

      // Build listener
      ElementListener elementListener = message -> System.out.println(message);

      // Assemble element
      Element element = new Element(elementListener);
      element.sayHello();
    }
  }

  public static void main(String[] args) {
    App app = new App();
    app.start();
  }
}

最初, 我们定义ElementListener来指定打印输出的方法。元素本身在下面定义。在元素上调用sayHello时, 它仅使用ElementListener打印一条消息。注意, 该元素完全独立于printOutput方法的实现。可以将其打印到控制台, 物理打印机或精美的UI中。该元素不依赖于该实现。由于这种抽象, 该元素可以轻松地在不同的应用程序中重用。

现在看看主要的App类。它实现了侦听器, 并通过具体的实现将元素组装在一起。现在我们可以开始使用它了。

你也可以在JavaScript中运行此示例

元素架构

让我们看一下在大型应用程序中使用元素模式。在一个小型项目中展示它是一回事, 将其应用于现实世界则是另一回事。

我喜欢使用的全栈Web应用程序的结构如下:

src
├── client
│   ├── app
│   └── elements
│   
└── server
    ├── app
    └── elements

在源代码文件夹中, 我们最初将客户端文件和服务器文件分开。这样做是合理的, 因为它们在两种不同的环境中运行:浏览器和后端服务器。

然后, 我们将每一层中的代码分成名为app和elements的文件夹。元素由具有独立组件的文件夹组成, 而app文件夹将所有元素连接在一起并存储所有业务逻辑。

这样, 可以在不同项目之间重用元素, 而所有特定于应用程序的复杂性都封装在一个文件夹中, 并且通常减少为对元素的简单调用。

动手实例

相信实践总是胜过理论, 让我们看一下用Node.js和TypeScript创建的真实示例。

现实生活中的例子

这是一个非常简单的网络应用程序, 可以用作更高级解决方案的起点。它的确遵循元素体系结构, 并且使用了广泛的结构元素模式。

从高亮显示, 你可以看到主页已被区分为一个元素。此页面包含其自己的视图。因此, 例如, 当你要重用它时, 你可以简单地复制整个文件夹并将其放入另一个项目中。只需将所有内容连接在一起, 就可以设置好了。

这是一个基本示例, 说明你可以立即在自己的应用程序中引入元素。你可以开始区分独立的组件并分离其逻辑。你当前正在处理的代码有多混乱都没关系。

开发更快, 重用更多!

我希望, 通过这套新工具, 你将能够更轻松地开发可维护性更高的代码。在实际使用元素模式之前, 让我们快速回顾一下所有要点:

  • 由于多个组件之间的依赖关系, 导致软件中出现许多问题。

  • 通过在一处进行更改, 你可以在其他地方引入不可预测的行为。

三种常见的体系结构方法是:

  • 大泥球。这对快速发展非常有用, 但对稳定生产而言却不那么理想。

  • 依赖注入。你应该避免这种半熟的解决方案。

  • 元素架构。该解决方案允许你创建独立的组件, 并在其他项目中重复使用它们。它具有可维护性和出色性能, 可以稳定地发布产品。

基本元素模式包括一个具有所有主要方法的主类以及一个侦听器, 该侦听器是一个简单的接口, 允许与外部世界进行通信。

为了实现全栈元素架构, 首先需要将前端代码与后端代码分开。然后在每个应用程序和元素中创建一个文件夹。 elements文件夹由所有独立元素组成, 而app文件夹则将所有内容连接在一起。

现在, 你可以开始创建和共享自己的元素。从长远来看, 它将帮助你创建易于维护的产品。祝你好运, 让我知道你创造了什么!

另外, 如果你发现自己过早地优化代码, 请阅读srcminier Kevin Bloch的同事如何避免过早优化的诅咒。

相关:JS最佳实践:使用TypeScript和依赖注入构建Discord Bot

赞(0)
未经允许不得转载:srcmini » 创建无依赖性的真正模块化代码

评论 抢沙发

评论前必须登录!