作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
康拉德·加兹诺夫斯基的头像

康拉德Gadzinowski

Konrad专注于创建易于扩展的模块化、全栈web应用程序. 他的主要专长是Java和JavaScript.

专业知识

以前在

TomTom
分享

开发软件很棒, 但是,我想我们都同意这有点像情绪过山车. 一开始,一切都很好. 你可以在几天甚至几个小时内添加一个又一个新功能. 你运气不错!

快进几个月,你的开发速度就会下降. 是因为你不像以前那样努力了吗? 不是真正的. 让我们再快进几个月,你的开发速度进一步下降. 做这个项目不再有趣了,已经变成了累赘.

情况会变得更糟. 您开始发现应用程序中的多个错误. 通常,解决一个bug会产生两个新bug. 在这一点上,你可以开始唱歌:

代码中的99个小bug. 99只小虫子. 取下一个,重新修补,

代码中的127个小错误.

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

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

软件开发慢下来的原因

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

主要原因是复杂性的增加. 从我的经验来看,导致整体复杂性的最大因素是, 在绝大多数软件项目中, 一切都是相连的. 因为每个类都有依赖关系, 如果你改变了发送电子邮件类中的一些代码, 您的用户突然无法注册. 这是为什么?? 因为你的注册码取决于发送邮件的代码. 现在你不能在不引入bug的情况下改变任何东西. 跟踪所有的依赖关系是不可能的.

So there you have it; the real cause of our problems is raising complexity coming from 所有 the dependencies that our code has.

大泥球和如何减少它

有趣的是,这个问题多年来一直为人所知. 这是一种常见的反模式,称为“大泥球”.“多年来,我在多家不同的公司参与的几乎所有项目中都看到了这种类型的架构.

那么这个反模式到底是什么呢? 简单地说,当每个元素都与其他元素有依赖关系时,您会得到一个大泥球. 下面,您可以看到来自知名开源项目Apache Hadoop的依赖关系图. 为了使大泥球形象化(或者更确切地说, 大纱线球), 您画一个圆,并将项目中的类均匀地放在圆上. 只要在每一对相互依赖的类之间画一条线. 现在你可以看到问题的根源了.

A visualization of Apache Hadoop's "big b所有 of mud," with a few dozen nodes 和 hundreds of lines connecting them to each other.

Apache Hadoop的“大泥球”

模块化代码的解决方案

所以我问了自己一个问题:是否有可能在降低复杂性的同时保持项目开始时的乐趣? 说实话,你无法消除 所有 复杂性. 如果想要添加新特性,就必须提高代码的复杂性. 然而,复杂性是可以移动和分离的.

其他行业如何解决这个问题

想想机械行业. 当一些小的机械车间制造机器时, 他们买了一套标准元件, 创建一些自定义的, 把它们放在一起. 他们可以完全独立地制造这些组件,并在最后组装所有组件, 只是做了一些调整. 这怎么可能?? 他们知道每个元素如何通过设定的行业标准(如螺栓尺寸)组合在一起, 预先决定安装孔的大小和它们之间的距离.

一个物理机制的技术图表,以及它的各个部分是如何组合在一起的. 这些零件按要接接的顺序编号, 但是从左到右的顺序是5, 3, 4, 1, 2.

上述组件中的每个元素都可以由一家独立的公司提供,而该公司对最终产品或其他部件一无所知. 只要每个模块元件都是按照规格制造的, 您将能够按照计划创建最终设备.

我们能在软件行业复制这种模式吗?

当然可以! By using interfaces 和 in版本 of control principle; the best part is the fact that 这 应用程序roach can be used in any object-oriented language: Java, C#, 斯威夫特, 打印稿, JavaScript, php——这个列表还在继续. 您不需要任何花哨的框架来应用此方法. 你只需要坚持一些简单的规则并保持自律.

控制反转是你的朋友

当我第一次听说控制反转时,我立刻意识到我已经找到了解决方案. 它的概念是获取现有的依赖关系,并通过使用接口来反转它们. 接口是方法的简单声明. 他们没有提供任何具体的实现. 因此,它们可以用作两个元素之间关于如何连接它们的协议. 如果您愿意,它们可以用作模块化连接器. 只要一个元素提供接口,另一个元素提供接口的实现, 他们可以在不了解对方的情况下一起工作. 它是聪明的.

让我们看一个简单的例子,我们如何解耦我们的系统来创建模块化的代码. 下面的图表已经被实现为简单的Java应用程序. 你可以在这里找到他们 GitHub库.

问题

假设我们有一个非常简单的应用程序,它只包含a Main 类,三个服务,和一个单一 跑龙套 class. 这些元素以多种方式相互依赖. 下面,您可以看到使用“大泥球”方法的实现. 类只是相互调用. 它们是紧密耦合的,你不能简单地去掉一个元素而不触及其他元素. 使用这种风格创建的应用程序允许您最初快速增长. 我相信这种风格适合于概念验证项目,因为您可以轻松地进行操作. 不过, 它不适合生产就绪的解决方案,因为即使是维护也可能是危险的,任何单个更改都可能产生不可预测的错误. 下图展示了这个巨大的泥球建筑.

Main使用业务A、B和C,它们都使用跑龙套. 服务C也使用服务A.

为什么依赖注入会出错

为了寻找更好的方法,我们可以使用一种称为依赖注入的技术. 此方法假定所有组件都应通过接口使用. 我读到过它解耦元素的说法,但它真的是这样吗? No. 看看下面的图表吧.

以前的架构,但有依赖注入. 现在Main使用接口服务A、B和C,它们由它们对应的服务实现. 服务A和C都使用接口服务B和接口跑龙套,由跑龙套实现. 服务C也使用接口服务A. 每个服务及其接口都被认为是一个元素.

目前的情况和一个大泥球之间的唯一区别是,现在, 而不是直接调用类, 我们通过它们的接口调用它们. 它稍微改进了元素之间的分离. 例如,如果您想重用 服务 在不同的项目中,你可以通过去掉 服务 本身,连同 接口,以及…… 接口B接口跑龙套. 如你所见, 服务 还取决于其他因素. 结果是, 我们仍然会遇到在一个地方修改代码而在另一个地方搞砸行为的问题. 它仍然会产生问题,如果你修改 服务B接口B,您将需要更改依赖于它的所有元素. This 应用程序roach doesn’t solve anything; in my opinion, 它只是在元素的顶部添加了一层界面. 你不应该注入任何依赖,而应该一劳永逸地摆脱它们. 为独立万岁!

模块化代码的解决方案

我认为解决依赖关系的所有主要问题的方法是根本不使用依赖关系. 您创建了一个组件及其侦听器. 侦听器是一个简单的接口. 当您需要从当前元素外部调用方法时, 您只需向侦听器添加一个方法并调用它即可. 元素只允许使用文件, 调用其包中的方法, 并使用main框架或其他常用库提供的类. 下面,您可以看到为使用元素体系结构而修改的应用程序关系图.

为使用元素体系结构而修改的应用程序图. Main使用跑龙套和所有三个服务. Main还为每个服务实现了一个侦听器,供该服务使用.  侦听器和服务一起被认为是一个元素.

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

另一方面,服务是完全独立的元素. 现在,您可以从这个应用程序中取出每个服务,并在其他地方重用它们. 它们不依赖于其他任何东西. 但是等等, 更好的是:您再也不需要修改这些服务了, 只要你不改变他们的行为. 只要这些服务做他们应该做的事, 它们可以不被碰触,直到时间的尽头. 它们可以由专业人士制作 软件工程师或者是第一次编程的人遇到了最糟糕的意大利面条代码 转到 混合陈述. 没关系,因为它们的逻辑被封装了. 尽管它可能很可怕,但它永远不会泄露给其他班级. 这也使您能够在多个开发人员之间拆分项目中的工作, 每个开发人员都可以独立地开发自己的组件,而不需要打断其他开发人员,甚至不知道其他开发人员的存在.

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

元素模式

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

元素的最简单版本由两部分组成:主元素类和侦听器. 如果你想使用一个元素, 然后需要实现侦听器并调用主类. 下面是最简单的配置示意图:

应用程序中单个元素及其监听器的关系图. 和前面一样,应用程序使用元素,元素使用由应用程序实现的侦听器.

显然,您最终需要向元素中添加更多的复杂性,但您可以轻松地做到这一点. 只要确保您的逻辑类不依赖于项目中的其他文件即可. 它们只能使用该元素中的主框架、导入的库和其他文件. 当涉及到像图像,视图,声音等资产文件时.,它们也应该被封装在元素中,以便将来可以很容易地重用. 您可以简单地将整个文件夹复制到另一个项目中,然后就可以了!

下面,您可以看到一个显示更高级元素的示例图. 请注意,它由一个正在使用的视图组成,并且不依赖于任何其他应用程序文件. 如果您想知道检查依赖项的简单方法,只需查看import部分. 是否有任何来自当前元素外部的文件? 如果是这样的话, 然后,您需要通过将这些依赖项移动到元素中或添加对侦听器的适当调用来删除它们.

一个更复杂元素的简单图解. 在这里, the larger sense of the word "元素" consists of six parts: View; Logics A, B, 和 C; Element; 和 Element Listener. 后两者与应用程序的关系与之前一样, 但内部元素也使用逻辑A和C. 逻辑C使用逻辑A和逻辑B. 逻辑A使用逻辑B和视图.

让我们看一下用Java创建的简单的“Hello World”示例.

公共类Main {

  接口ElementListener
    无效printOutput(字符串消息);
  }

  元素{

    private ElementListener监听器;

    公共元素(ElementListener 侦听器) {
      这.Listener =监听器;
    }

    public void sayHello() {
      字符串消息= "Hello World of Elements .!";
      这.侦听器.printOutput(消息);
    }
  }

  静态类应用程序 {

    public 应用程序() {
    }

    Public void start() {

      //创建监听器
      ElementListener 元素Listener = message -> System.出.println(消息);

      //组装元素
      Element = new Element(元素Listener);
      元素.sayHello ();
    }
  }

  public static void main(String[] args) {
    应用程序 应用程序 = new 应用程序();
    应用程序.开始();
  }
}

首先,我们定义 ElementListener 指定打印输出的方法. 元素本身定义如下. 在调用 sayHello 在元素上,它只是使用 ElementListener. 的实现是完全独立的 printOutput 方法. 它可以打印到控制台、物理打印机或花哨的UI中. 元素不依赖于该实现. 由于这种抽象,可以很容易地在不同的应用程序中重用该元素.

现在看一下主要的 应用程序 class. 它实现侦听器并将元素与具体实现组合在一起. 现在我们可以开始使用它了.

您也可以在这里用JavaScript运行这个示例

元素架构

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

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

src
├──客户
│├──├
││──元素
│   
└──服务器
    ├──应用
    └──元素

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

然后我们将每一层的代码分成应用程序和元素s两个文件夹. 元素由具有独立组件的文件夹组成, 而应用程序文件夹将所有元素连接在一起并存储所有业务逻辑.

这种方式, 元素可以在不同的项目之间重用, 而所有特定于应用程序的复杂性都被封装在一个文件夹中,并且经常被简化为对元素的简单调用.

操作示例

相信实践总是胜过理论, 让我们看一下在Node中创建的实际示例.js和打印稿.

现实生活中的例子

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

通过高亮显示,您可以看到主页已被区分为一个元素. 该页包含自己的视图. 所以,当, 例如, 你想要重用它, 您可以简单地复制整个文件夹并将其放入不同的项目中. 只要把所有的东西连接起来,你就搞定了.

这是一个基本示例,演示了您现在就可以开始在自己的应用程序中引入元素. 您可以开始区分独立的组件并分离它们的逻辑. 不管你当前编写的代码有多混乱.

开发更快,重用更频繁!

我希望, 有了这套新工具, 您将能够更容易地开发出更易于维护的代码. 在开始在实践中使用元素模式之前,让我们快速回顾一下所有的要点:

  • 软件中的许多问题都是由于多个组件之间的依赖关系而发生的.

  • 通过在一个地方进行更改,您可以在其他地方引入不可预测的行为.

三种常见的架构方法是:

  • 那个大泥球. 它对于快速开发非常有用,但是对于稳定的生产目的就不那么有用了.

  • 依赖注入. 这是一个不成熟的解决方案,你应该避免.

  • 元素架构. 此解决方案允许您创建独立的组件并在其他项目中重用它们. 它是可维护的,非常适合稳定的生产版本.

基本元素模式由具有所有主要方法的主类和允许与外部世界通信的简单接口侦听器组成.

以实现全栈元素架构, 首先,将前端代码与后端代码分离. 然后为应用程序和元素创建一个文件夹. 元素s文件夹由所有独立的元素组成, 而应用程序文件夹将所有东西连接在一起.

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

此外,如果您发现自己过早地优化了代码,请阅读 如何避免过早优化的诅咒 是托普塔勒的同事凯文·布洛赫.

了解基本知识

  • 是什么让计划外的代码难以维护并容易出现bug?

    由于多个组件之间的依赖关系,代码可能很难维护. 因此,在一个地方做出改变可能会在其他地方引入不可预测的行为.

  • 什么是模块化架构?

    模块化架构意味着将应用程序划分为独立的元素. 我们认识到,所有项目间的依赖关系都是难以发现和解决问题的原因. 完全的独立性使得这些组件非常容易测试, 维护, 分享, 并在将来重用.

  • 开发人员采用了哪些不同类型的代码架构?

    常见的架构方法有:1)大泥球:非常适合快速开发, 但不太适合稳定生产. 2)依赖注入:一个不成熟的解决方案,你应该避免. 3)元素架构:它是可维护的,并且非常适合稳定的生产版本.

聘请Toptal这方面的专家.
现在雇佣
康拉德·加兹诺夫斯基的头像
康拉德Gadzinowski

位于 Łodź,波兰

成员自 2017年8月10日

作者简介

Konrad专注于创建易于扩展的模块化、全栈web应用程序. 他的主要专长是Java和JavaScript.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

以前在

TomTom

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal开发者

加入总冠军® 社区.