纯粹的实用建筑。 集思广益

这个想法是否浮现在您的脑海,以从头开始重写大胆的企业应用程序? 如果从头开始,那就太好了。 至少少两倍的代码,对不对? 但是几年会过去,并且还会增长,成为遗产...没有太多的时间和金钱可以完美地重写。

冷静下来,当局仍然不允许重写任何东西。 它仍然需要重构。 花费少量资源的最佳方法是什么? 如何重构在哪里清洁?

本文的标题包括对Bob叔叔“ Clean Architecture”的引用,它是基于Victor Rentea( twitter网站 )在JPoint上的精彩报道(在他的主持下,他将以第一人称发言,但现在阅读介绍性内容)撰写的。 阅读智能书籍,本文不会取代本文,但是对于这样的简短描述非常合适。

这个想法是,诸如“清洁架构”之类​​的流行事物确实很有用。 惊喜 如果您需要解决一个非常具体的问题,那么简单而优雅的代码就不需要额外的精力和过度的设计。 纯体系结构要求您保护域模型不受外部影响,并确切告诉您如何实现。 一种不断发展的方法来增加微服务的数量。 使重构不那么令人恐惧的测试。 你已经知道这一切了吗? 或者您知道,但是您甚至害怕考虑它,因为这太恐怖了,您接下来要做什么?

谁想要获得一种神奇的抗避孕药,该药将帮助您摆脱晃动并开始重构-欢迎观看视频报道或在猫的照看下。





我叫维克多,我来自罗马尼亚。 我正式是罗马尼亚IBM的顾问,技术专家和首席架构师。 但是,如果要求我自己定义活动的定义,那么我就是纯代码的推广者。 我喜欢创建漂亮,干净,受支持的代码-通常,我会在报告中谈到这一点。 更令我受启发的是:在Java EE,Spring,Dojo,测试驱动开发,Java Performance以及上述传福音领域中对开发人员进行培训-干净的代码模式及其开发原理。

我的理论所基于的经验主要是为罗马尼亚最大的IBM客户-银行部门开发企业应用程序。

本文的计划如下:

  • 数据建模:数据结构不应成为我们的敌人;
  • 逻辑组织:“分解过多代码”的原则;
  • “洋葱”是最纯粹的事务脚本哲学体系结构;
  • 测试是解决开发人员恐惧的一种方法。


但是首先,让我们回顾作为开发人员必须记住的主要原则。

唯一责任原则





换句话说,数量与质量。 通常,您的类包含的功能越多,从质的角度讲,效果越差。 开发大型类时,程序员开始感到困惑,在构建依赖项时犯了错误,并且大型代码尤其难以调试。 最好将此类分成几个较小的类,每个类负责一些子任务。 最好有几个紧密耦合的模块而不是一个-大而慢。 模块化还允许逻辑的重用。

模块绑定弱





绑定程度是衡量模块之间交互关系的程度。 它显示了您在系统中任何一点进行的更改所产生的影响能够传播到多大范围。 绑定越高,修改就越困难:您在一个模块中更改某些内容,效果会扩展得很远,而并非总是以预期的方式扩展。 因此,绑定指示器应尽可能低-这将对正在进行修改的系统提供更多控制。

不要重复





您自己的实现今天可能很好,但明天却不太好。 不要让自己复制自己的最佳做法,从而将其分发到代码库中。 您可以从StackOverflow,书籍中复制-可以(肯定知道)提供理想(或接近)理想实现的任何权威来源。 改进自己的实现不止一次,但是在整个代码库中却成倍增加,这可能会很累人。

简洁明了





我认为,这是工程和软件开发中必须遵守的主要原则。 “过早的封装是邪恶的根源,”亚当·比恩说。 换句话说,邪恶的根源在于“再造”。 引文的作者亚当·比恩(Adam Bien)曾经从事旧版应用程序的开发,并完全重写了他们的代码,收到的代码库比原始代码小2-3倍。 如此多的额外代码从何而来? 毕竟,它是有原因的。 他的恐惧引起了我们。 在我们看来,通过堆积大量模式,生成间接性和抽象性,我们为我们的代码提供了保护-保护免受明天的未知数和明天的要求的侵害。 毕竟,事实上,今天我们不需要任何这些,我们仅出于某些“未来需求”而发明了所有这些。 这些数据结构随后可能会相互干扰。 老实说,当我的一些开发人员走近我并说他提出了可以添加到生产代码中的有趣内容时,我总是以相同的方式回答:“男孩,这对您没有用。”

应该没有太多的代码,而应该是简单的代码-正常使用它的唯一方法。 这是您的开发人员所关心的。 您必须记住,它们是系统的关键指标。 尝试减少他们的能源消耗,以减少他们必须工作的风险。 这并不意味着您必须创建自己的框架,而且,我不建议您这样做:框架中总会有bug,每个人都需要研究,等等。 最好使用现有的资产,而今天的资产数量很大。 这些应该是简单的解决方案。 写下全局错误处理程序,应用方面技术,代码生成器,Spring或CDI扩展,配置请求/线程作用域,使用字节码操作和即时生成等。所有这些都将为您真正重要的事情做出贡献-开发人员的舒适度。

特别是,我想向您演示“请求/线程”区域的应用。 我已经反复观察过这件事如何极大地简化了企业应用程序。 最重要的是,它作为登录用户为您提供了保存RequestContext数据的机会。 因此,RequestContext将以紧凑形式存储用户数据。



如您所见,该实现仅需要几行代码。 将请求写入所需的批注中(如果使用Spring或CDI并不难做到),您将无需将用户登录名传递给方法以及其他任何东西:存储在上下文中的请求元数据将透明地导航应用程序。 作用域代理将使您可以随时访问当前请求的元数据。

回归测试





开发人员害怕更新的要求,因为他们害怕重构过程(代码修改)。 帮助他们的最简单方法是为回归测试创建可靠的测试套件。 有了它,开发人员将有机会随时测试他们的运行时间-确保它不会破坏系统。

开发人员不应该害怕破坏任何东西。 您必须做所有事情,以便将重构视为一件好事。
重构是开发的关键方面。 请记住,恰恰在您的开发人员害怕重构的那一刻,该应用程序可以视为已成为旧版。

在哪里实现业务逻辑?





开始执行任何系统(或系统组件)的过程时,我们都会问自己一个问题:在哪里实现域逻辑(即应用程序的功能方面)更好? 有两种相反的方法。
第一个基于事务脚本原理。 在这里,逻辑是在与贫乏实体 (即数据结构)一起使用的过程中实现的。 这样的方法是好的,因为在其实施过程中可以依靠制定的业务任务。 在为银行业开发应用程序时,我反复观察到业务流程向软件的转移。 我可以说,将方案与软件相关联是非常自然的。

另一种方法是使用域驱动设计原则。 在这里,您需要将规范和要求与面向对象的方法相关联。 仔细考虑对象并确保良好的业务参与非常重要。 以这种方式设计的系统的优点是,将来可以轻松维护它们。 但是,以我的经验,掌握这种方法非常困难:学习六个月后,您或多或少会感到勇敢。

对于我的发展,我始终选择第一种方法。 我可以向您保证,就我而言,它运行良好。

资料建模



实体



我们如何对数据建模? 一旦应用程序采用或多或少的体面大小,则必然会出现持久数据 。 这种数据需要比其他数据存储更长的时间-它是系统的域实体 。 将它们存储在哪里-无论是在数据库中,在文件中还是直接管理内存-都无关紧要。 重要的是如何存储它们-在哪种数据结构中。



这种选择是作为开发人员提供给您的,它仅取决于您将来在实现功能需求时这些数据结构对您有用还是不利于您。 为了使一切都变得更好,您必须通过在实体中放置重用的域逻辑来实现实体。 具体如何? 我将通过一个示例演示几种方法。



让我们看看我提供给客户实体的内容。 首先,我实现了一个综合的 getFullName() getter ,它将返回firstName和lastName的串联。 我还实现了activate()方法-监视实体的状态,从而对其进行封装。 在这种方法中,我首先进行了验证操作 ,其次, 将值分配给 status和ActivateBy 字段 ,因此不需要为它们设置setter。 我还向客户实体添加了isActive()canPlaceOrders()方法,它们在自己内部实现了lambda验证。 这称为谓词封装。 如果使用Java 8过滤器,这些谓词会派上用场:您可以将它们作为参数传递给过滤器。 我建议您使用这些帮助器。

也许您使用的是Hibernate之类的ORM。 假设您有两个双向通讯的实体。 必须在双方都进行初始化,否则,如您所知,将来访问此数据时会遇到问题。 但是开发人员经常忘记初始化其中一方的对象。 开发这些实体时,可以提供特殊的方法来保证双向初始化。 看一下addAddress()



如您所见,这是一个非常普通的实体。 但是内部逻辑是域逻辑。 这样的实体不应是微不足道的和肤浅的,但不应被逻辑所淹没。 逻辑上的溢出更经常发生:如果您决定在域中实现所有逻辑,那么对于每种用例,都倾向于实现某些特定方法。 通常,有很多用例。 您不会收到一个实体,但是会收到一大堆各种各样的逻辑。 尝试在此处观察该措施:只有重用的逻辑才会放置在域中,并且数量很少。

价值对象



除了实体,您很可能还需要对象值。 这不过是对域数据进行分组的一种方法,以便以后可以将其一起在系统中移动。

值对象必须是:

  • 。 货币变量无float ! 选择数据类型时要小心。 您的对象越紧凑,新开发人员就越容易弄清楚。 这是舒适生活的基础。
  • 不可更改 。 如果对象确实是不可变的,那么开发人员可能会冷静,因为您的对象不会更改其值,并且在创建后不会损坏。 这为平静,自信的工作奠定了基础。




而且,如果您向构造函数添加validate()方法调用,则开发人员将能够使所创建实体的有效性趋于平静(当传递例如不存在的货币或负数的货币时,构造函数将无法工作)。

实体和值对象之间的区别



值对象与实体的不同之处在于它们没有固定的ID。 实体将始终具有与某个表(或其他存储)的外键关联的字段。 值对象没有此类字段。 问题出现了:检查两个值对象和两个实体的相等性的程序是否不同? 由于值对象没有ID字段,为了得出两个这样的对象相等的结论,您必须成对比较所有它们的字段的值(即检查所有内容)。 比较实体时,只需按字段ID进行一次比较就足够了。 在比较过程中,实体和值对象之间的主要区别在于。

数据传输对象(DTO)





与用户界面(UI)的交互是什么? 您必须将数据显示给他 。 您真的需要其他结构吗? 就是这样 所有这些都是因为用户界面根本不是您的朋友。 他有自己的要求:他需要根据数据的显示方式存储数据。 这太好了-有时有时用户界面及其开发人员才需要我们。 然后,他们需要获取五行数据。 然后他们想到为该对象创建一个isDeletable布尔值字段(该对象原则上可以有这样的字段吗),以便了解Delete按钮是否处于活动状态。 但是,没有什么可愤慨的。 用户界面仅具有不同的要求。

问题是,可以将我们的实体委托给他们使用吗? 它们很可能会以我们最不希望的方式更改它们。 因此,我们将为他们提供其他东西- 数据传输对象 (DTO)。 它们将特别适合外部需求和与我们不同的逻辑。 DTO结构的一些示例是:表单/请求(来自UI),视图/响应(发送至UI),SearchCriteria / SearchResult等。从某种意义上讲,您可以将其称为API模型。

第一个重要原则:DTO应该包含最少的逻辑。
这是CustomerDto的示例实现。



内容: 私有领域, 公共获取者和设置者。 一切似乎都很棒。 OOP的所有荣耀。 但是有一件事是不好的:以吸气剂和吸气剂的形式,我实现了太多的方法。 在DTO中,逻辑应尽可能少。 那我的出路是什么? 我将这些字段公开! 您可能会说,这与Java 8中的方法引用一起使用时效果不佳,会有限制等。但是不管您是否相信,我都使用此类DTO完成了所有项目(10-11件)。 哥哥还活着。 现在,由于我的字段是公共字段,因此只需放置等号就可以轻松地将值设置为dto.fullName 。 还有什么更美丽,更简单?

逻辑组织



制图



因此,我们有一个任务:我们需要将实体转换为DTO。 我们执行以下转换:



如您所见,通过声明DTO,我们继续进行映射(值分配)操作。 我是否需要成为高级开发人员才能以这种数量编写常规作业? 对于某些人来说,这是非常不寻常的,以至于他们开始在旅途中换鞋:例如,使用某种使用反射的映射框架复制数据。 但是他们错过了主要的事情-UI迟早会与DTO交互,因此实体和DTO的含义有所不同。

可以说,将映射操作放入构造函数中。 但这对于任何映射都是不可能的。 特别是,设计者无法访问数据库。

因此,我们被迫将映射操作留在业务逻辑中。 而且,如果它们的外观紧凑,则无需担心。 如果映射不占用几行,而是多走几行,那么最好将其放置在所谓的mapper中 。 映射器是专门设计用于复制数据的类。 总的来说,这是古老的事物和样板。 但是,在它们后面,您可以隐藏我们的许多任务-使代码更简洁。

切记: 太大代码必须移到单独的结构中 。 在我们的例子中,映射操作确实很多,因此我们将它们移到了一个单独的类-映射器。

映射器是否应该允许访问数据库? 您可以默认启用它-这样做通常是出于简单性和实用性的原因。 但这会使您面临某些风险。

我将举例说明。 基于现有的DTO,我们创建Customer实体。



进行映射时,我们需要从数据库获得指向客户组的链接。 所以我运行了getReference()方法,它返回了我一些实体。 该请求很可能会发送到数据库(在某些情况下不会发生,并且存根函数会起作用)。

但是麻烦不是在这里等待着我们,而是在执行逆向操作的方法中-将实体转换为DTO。



使用循环,我们遍历与现有客户相关的所有地址,并将其转换为DTO地址。 如果使用ORM,则可能在调用getAddresses()方法时将执行延迟加载。 如果您不使用ORM,那么这将是对此父级的所有子级的公开请求。 在这里,您有陷入“ N + 1问题”的风险。 怎么了



您有一组父母,每个父母都有孩子。 为此,您需要在DTO内创建自己的类似物。 您将需要执行一个SELECT查询来遍历N个父实体,然后执行N个SELECT查询来遍历每个子实体。 总计N +1个请求。 对于1000个父级Customer实体,此操作将花费5到10秒,这当然会花费很长时间。

但是,假设在循环内调用了CustomerDto()方法,将Customer对象的列表转换为CustomerDto列表。



N + 1个查询的问题具有简单的标准解决方案:在JPQL中 ,可以使用customer.addresses的FETCH来检索子代,然后使用JOIN进行连接,而在SQL中,可以使用IN旁路和WHERE

但是我会做不同的事情。 您可以找出子列表的最大长度是多少(例如,可以基于分页搜索来完成)。 如果列表仅包含15个实体,则我们仅需要16个查询。 而不是5毫秒,我们将花费所有时间,例如15毫秒-用户将不会注意到差异。

关于优化



我不建议您在开发初期回顾一下系统性能。 正如Donald Knud所说:“过早的优化是邪恶的根源。” 您不能从一开始就进行优化。 这正是以后需要保留的内容。 尤其重要的是: 没有假设-只有测量和测量评估!

您确定自己有能力成为真正的专家吗? 在评估自己时要谦虚。 在至少阅读了几本有关JIT编译的书之前,请不要以为您了解JVM。 碰巧,我们团队中最好的程序员来找我,说他们认为自己找到了更有效的实现。 事实证明,他们再次发明了一些只会使代码复杂化的东西。 所以我一遍又一遍地回答:YAGNI。 我们不需要它。

通常,对于企业应用程序,根本不需要优化算法。 通常,它们的瓶颈不是编译,就处理器而言,不是所有的输入输出操作。 例如,从数据库读取一百万行,大量写入文件,与套接字交互。

随着时间的流逝,您开始了解系统包含的瓶颈,并且通过测量来增强所有功能,您将开始逐步优化。 现在,保持代码尽可能干净。 您会发现,这样的代码更容易进一步优化。

优先考虑组成而不是继承



回到我们的DTO。 假设我们这样定义一个DTO:



我们可能在许多工作流程中都需要它。 但是这些流程是不同的,并且很可能每个用例都将采用不同程度的字段填充。 例如,显然,我们需要在拥有完整的用户信息时更早地创建DTO。 您可以暂时将这些字段留空。 但是,您忽略的字段越多,就越需要为该用例创建一个新的更严格的DTO。

或者,您可以创建一个过大的DTO副本(以可用用例的数量为单位),然后为每个副本从中删除多余的字段。 但是对于许多程序员而言,由于他们的才智和素养,按Ctrl + V确实很痛。 公理说复制粘贴是不好的。

您可以诉诸OOP理论中已知的继承原理:只需定义一个基本DTO并为每个用例创建一个继承人。



一个众所周知的原则是:“优先考虑组成而不是继承。” 阅读其内容: “扩展” 。 看来我们应该“扩展”源类。 但是,如果您考虑一下,那么我们所做的根本就不是“扩展”。 这是真正的“重复”-相同的复制粘贴侧视图。 因此,我们将不使用继承。

但是那应该是什么呢? 如何去作文? 让我们这样做:在CustomerView中编写一个字段,该字段指向基础DTO的对象。



因此,我们的基础结构将嵌套在内部。 这就是真正的构图出来的方式。

无论我们使用继承还是通过组合解决问题-这些都是细节,在我们实施过程中深深地产生了微妙之处。 他们非常脆弱 。 脆弱是什么意思? 仔细看一下这段代码:



我向其展示过的大多数开发人员都立即脱口而出,重复数字“ 2”,因此需要将其作为常量取出。 他们没有注意到,在所有这三种情况下,演习都具有完全不同的含义(或“商业价值”),并且重复演说不过是巧合。 将二取为常数是一个合理的决定,但非常脆弱。 尽量不要让脆弱的逻辑进入领域。 切勿使用外部数据结构,尤其是DTO。

那么,为什么消除继承和引入合成的工作是没有用的呢? 正是因为我们不是为自己而是为外部客户创建DTO。 以及客户端应用程序将如何解析从您那里收到的DTO-您只能猜测。 但是显然,这与您的实现无关。 另一方面,开发人员可能无法区分您已经仔细考虑过的基本DTO和非基本DTO。 他们可能使用继承,也可能愚蠢地复制粘贴。

外墙





让我们回到应用程序的整体情况。 我建议您通过Facade模式实现域逻辑,并根据需要使用域服务扩展Facade。 当外观中累积了太多的逻辑时,就会创建域服务,将其放在单独的类中会更方便。
域服务必须使用域模型的语言(它的实体和值对象)。 它们绝不能与DTO一起使用,因为您记得,DTO是在客户端不断变化的结构,对于域而言太脆弱了。



外墙的目的是什么?

  1. 数据转换。 如果我们从一端有实体,从另一端有DTO,则有必要进行从一端到另一端的转换。 这是立面的第一件事。 如果转换过程的数量增加了,请使用映射器类。
  2. 执行逻辑。 在外观中,您将开始编写应用程序的主要逻辑。 一旦变得很多-参加域服务。
  3. 数据验证。 请记住,从定义上讲,从用户那里收到的任何数据都是错误的(包含错误)。 外墙具有验证数据的能力。 这些程序在超出容量时通常会带给验证器
  4. 方面 您可以走得更远,并使每个用例都通过其外观。 然后会发现将诸如事务,日志记录,全局异常处理程序之类的内容添加到Facade方法中,我注意到在任何应用程序中捕获能够被其他处理程序捕获的所有错误的全局异常处理程序非常重要。 它们将极大地帮助您的程序员-他们将使他们放心,行动自由。


分解很多代码





关于这个原则再说几句话。 如果班上的人数对我来说不方便(例如200行),那么我应该尝试将其分成几部分。 但是,将一个新的类与现有的类隔离开并非总是容易的。 我们需要提出一些通用的方法。 这些方法之一是搜索名称:您正在尝试为类的方法的子集查找名称。 一旦找到名称,请随时创建一个新类。 但这不是那么简单。 如您所知,在编程中,只有两件复杂的事情:这使高速缓存无效和创建名称。 在这种情况下,发明名称涉及识别子任务-隐藏并且因此以前未被任何人识别。

一个例子:



CustomerFacade的原始外观中CustomerFacade某些方法与客户直接相关,而某些方法与客户偏好相关。 基于此,当达到临界尺寸时,我将能够将班级分为两部分。 我得到两个外观: CustomerFacadeCustomerPreferencesFacade 。 唯一的坏处是这两个外观都属于相同的抽象级别。 按抽象级别进行分离意味着有所不同。

另一个例子:



假设我们的系统中有一个OrderService类,在其中我们实现了电子邮件通知机制。 现在,我们正在创建一个DeliveryService并希望在这里使用相同的通知机制。 复制粘贴不包括在内。 让我们这样做:将通知功能提取到新的AlertService类中,并将其编写为DeliveryServiceOrderService的依赖项。 在这里,与前面的示例相反,分离恰好发生在抽象级别。 DeliveryService比更加抽象AlertService,因为它将它用作工作流的一部分

按抽象级别进行分隔始终假定提取的类成为依赖项,并且进行提取以进行重用

提取任务并不总是那么容易。它还可能带来一些困难,并且需要对单元测试进行一些重构。但是,根据我的观察,开发人员在应用程序的庞大整体代码库中搜索任何功能甚至更加困难。

配对编程





许多顾问将讨论结对编程,这是对当今IT开发中任何问题的通用解决方案。在此期间,程序员将发展其技术技能和功能知识。此外,该过程本身很有趣,它将团队团结在一起。

说到不是顾问,而是人类,最重要的是:结对编程提高了“总线系数”。 “总线因素”的本质是,应该让尽可能多的人了解系统的结构。失去这些人意味着失去这些知识的最后线索。

结对编程重构是一门需要经验和培训的艺术。例如,在进行积极的重构,进行黑客马拉松,剪切,编码Dojos等实践时,这很有用。

在需要解决高复杂性问题的情况下,结对编程非常有效。一起工作的过程并不总是那么简单。但这保证了您将避免“重新设计”,相反,您将获得一种可以以最小的复杂性满足既定要求的实现。



组织方便的工作格式是团队的主要职责之一。您必须不断照顾开发人员的工作条件-为他们提供完全的舒适感和创造力的自由,尤其是在需要增加设计架构及其复杂性的情况下。

“我是一名建筑师。根据定义,我永远是对的。”



这种愚蠢行为是定期公开或在幕后发表的。在当今的实践中,越来越多的建筑师被发现。随着敏捷的问世,这个角色逐渐转移给了高级开发人员,因为通常所有的工作都以某种方式围绕着他们进行。实现的大小逐渐增长,因此需要重构,并且正在开发新功能。

洋葱架构



洋葱是最纯粹的事务脚本哲学。构建它时,我们的目标是保护我们认为关键的代码,为此,我们将其移至域模块。



在我们的应用程序中,最重要的是域服务:它们实现了最关键的流程。将它们移到域模块。当然,将所有域对象(实体和值对象)移到此处也是值得的。可以说,我们今天编辑的所有其他内容(DTO,映射器,验证器等)已成为用户的第一道防线。因为用户,las,不是我们的朋友,因此有必要保护系统免受他的侵害。

注意这种依赖性:



应用程序模块将取决于域模块-即不是相反。通过注册这样的连接,我们保证DTO永远不会闯入域模块的圣地:它们在域模块中根本是不可见和不可访问的。事实证明,从某种意义上说,我们围堵了领域领地-我们限制了陌生人对其的访问。

但是,域可能需要与某些外部服务进行交互。用外部手段不友好,因为他配备了DTO。我们有什么选择?

首先:跳过模块内的敌人。



显然,这是一个错误的选择:明天外部服务可能不会升级到2.0版,并且我们将不得不重新绘制域。不要让敌人进入领域!

我提出了一种不同的方法:我们将创建一个用于交互的特殊适配器



适配器将从外部服务接收数据,提取我们域所需的数据,并将其转换为所需的结构类型。在这种情况下,我们在开发过程中所需要做的就是将对外部系统的调用与域的需求相关联。可以将它视为这样的巨大适配器我称这一层为“反腐败”。

例如,我们可能需要从域执行LDAP查询。为此,我们正在实施“反腐败模块” LDAPUserServiceAdapter



在适配器中,我们可以:

  • 隐藏丑陋的API调用(在我们的示例中,隐藏采用Object数组的方法);
  • 在我们自己的实现中打包异常;
  • 将其他人的数据结构转换为自己的数据结构(转换为我们的域对象);
  • 检查传入数据的有效性。


这是适配器的目的。很好,在与需要与之交互的每个外部系统的接口处,必须安装适配器。


因此,域将不会将呼叫定向到外部服务,而是定向到适配器。为此,必须在域中注册相应的依赖项(从适配器或从它所在的基础结构模块中注册)。但是这种成瘾安全吗?如果您这样安装,则外部服务DTO可以进入我们的域。我们不应该允许这一点。因此,我建议您使用另一种建模依赖关系的方法。

依赖倒置原则





让我们创建一个接口,在其中写入必要方法的签名,并将其放入我们的域中。适配器的任务是实现此接口。事实证明,在导入接口的基础结构模块中,接口位于域内部,而适配器位于外部。因此,我们将依赖的方向转向了相反的方向。在运行时,域系统将通过接口调用任何类。

如您所见,仅通过将接口引入体系结构,我们就能够部署依赖项,从而保护我们的域免受陷入其中的外部结构和API的侵害。这种方法称为依赖反转



通常,依赖关系反转假定您将感兴趣的方法放在高级模块(在域中)的接口中,并从外部(在一个或另一个低级(基础结构)丑陋的模块中)实现此接口。

域模块内部实现的接口必须使用域语言,也就是说,它将在其实体,其参数和返回类型上进行操作。在运行时,域将通过对接口的多态调用来调用任何类。依赖注入框架(如Spring和CDI)在运行时为我们提供了该类的具体实例。

但是最主要的是,在编译期间,域模块将看不到外部模块的内容。那就是我们所需要的。任何外部实体都不应属于该域。

根据Bob叔叔的说法,控制反转的原理(或他所谓的“插件架构”)也许是OOP范式通常提供的最好的原理。



该策略可用于与任何系统集成,同步和异步调用和消息,发送文件等。

灯泡概述





因此,我们决定保护域模块。它的内部是域服务,实体,值对象以及现在用于外部服务的接口,以及用于存储库的接口(用于与数据库进行交互)。

结构如下所示:



应用程序模块,基础结构模块(通过依赖关系反转),存储库模块(我们也将数据库视为外部系统),批处理模块以及可能的其他一些模块声明为该域的依赖关系。这种架构称为“洋葱”它也称为“干净”,“六边形”和“端口和适配器”。

储存库模块



我将简要讨论存储库模块。是否将其带出域是一个问题。存储库的任务是使逻辑更加整洁,从而避免了处理持久性数据的恐怖。老派的选择是使用JDBC与数据库进行交互:



您还可以使用Spring及其JdbcTemplate:



或MyBatis DataMapper:



但它是如此的复杂和丑陋,以至于阻止了人们进一步做任何事情的愿望。因此,我建议使用JPA / Hibernate或Spring Data JPA。它们将使我们有机会发送不是基于数据库架构而是直接基于我们实体模型构建的查询。

JPA / Hibernate的实现:



对于Spring Data JPA:



Spring Data JPA可以在运行时自动生成方法,例如getById(),getByName()。它还允许您在必要时执行JPQL查询-而不是数据库,而是您自己的实体模型。

Hibernate JPA和Spring Data JPA代码看起来确实不错。我们是否需要从域中提取它?我认为这不是必须的。如果您将此片段留在域中,则代码很可能会更加干净。因此,根据情况采取行动。



但是,如果您创建存储库模块,则为了组织依赖关系,最好以相同的方式使用控制反转的原理。为此,请将接口放在域中并在存储库模块中实现。至于存储库逻辑,最好将其传输到域。因为您可以在域中使用Mock对象,所以这使测试方便。它们将使您能够快速重复地测试逻辑。

传统上,只为域中的存储库创建一个实体。仅当体积太大时,他们才将其切成碎片。请记住,类必须紧凑。

API





您可以创建一个单独的模块,放置从立面提取的接口和依赖该立面的DTO,然后将其打包在JAR中,然后以这种形式将其传输到Java客户端。有了此文件,他们将能够向外观发送请求。

实用灯泡



除了向我们提供功能的“敌人”(即客户)之外,我们还有敌人,另一方面,我们自己依赖的那些模块。我们还需要保护自己免受这些模块的侵害。为此,我为您提供了一个经过稍微修改的“洋葱”-在其中,整个基础架构被组合到一个模块中。


我称这种架构为“实用灯泡”。在这里,组件的分离是根据“我的”和“可集成的”原则进行的:分别与我的领域有关,以及与外部合作者的集成。因此,仅获得两个模块:域和应用程序。这样的架构非常好,但是仅当应用程序模块很小时。否则,您最好返回传统洋葱。

测验



正如我之前说的,如果每个人都害怕您的申请,请考虑它补充了旧版的行列。
但是测试是好的。它们使我们充满信心,使我们能够继续进行重构。但是不幸的是,这种信心很容易被证明是不合理的。我将解释原因。 TDD(通过测试进行开发)假定您既是代码的作者,又是测试用例的作者:您阅读规范,实现功能并立即为其编写测试套件。测试将成功。但是,如果您误解了规范要求怎么办?然后测试将检查不需要什么。因此,您的信心毫无价值。所有这些都是因为您仅编写代码和测试。

但是,请尝试对此视而不见。测试仍然是必要的,无论如何它们都使我们充满信心。当然,最重要的是,我们喜欢功能测试:它们并不意味着任何副作用,没有依赖性-仅输入和输出数据。要测试域,您需要使用模拟对象:它们将使您能够独立测试类。

至于数据库查询,对其进行测试是不愉快的。这些测试非常脆弱,它们要求您首先将测试数据添加到数据库中-只有在此之后,您才能继续测试功能。但是据您了解,即使您使用JPA,这些测试也是必要的。

单元测试





我要说的是,单元测试的力量不是运行它们的可能性,而是在于编写它们的过程所包含的内容。在编写测试时,您需要重新考虑并遍历代码-减少连接性,将其分成几类-总之,执行下一个重构。被测试的代码是纯代码;更简单,其中的连接性降低;通常,它也会被记录(编写良好的单元测试可以完美描述类的工作原理)。编写单元测试很困难,这并不奇怪,尤其是前几部分。



在第一个单元测试的阶段,很多人真的担心他们真的必须测试某些东西的前景。他们为什么这么辛苦?

因为这些测试是您课堂上首要负担。这是对系统的第一击,也许表明它是脆弱且脆弱的。但是您需要了解,这几项测试对您的开发最重要。从本质上讲,他们是您最好的朋友,因为他们会说所有有关您代码质量的事情。如果您担心此阶段,那么您将走不远。您必须为系统运行测试。之后,复杂度将降低,测试将被更快地编写。逐一添加它们,您将为系统创建可靠的回归测试基础这对于开发人员的未来工作极为重要。他们将更容易进行重构;他们将了解可以随时对系统进行回归测试,因此使用代码库是安全的。而且,我向您保证,他们将更加乐于参与重构。



我对您的建议:如果您觉得自己今天拥有很多力量和精力,请专注于编写单元测试。并且确保每个清洁,快速,有自己的重量并且不会重复其他任何一个。

小费



总结一下今天所说的一切,我谨向您提示以下几点:

  • 尽可能长时间保持简单(无论成本如何):避免“重新设计”和过时的优化,不要使应用程序过载;
  • , , ;
  • «» — ;
  • , — : ;
  • «», , — ;
  • 不要害怕测试:给他们机会降低您的系统,感受他们的所有好处-最终,他们是您的朋友,因为他们能够诚实地指出问题。


通过做这些事情,您将为您的团队和您自己提供帮助。然后,当产品交付之日到来时,您将为之做好准备。

读什么







. JPoint — , 19-20 - Joker 2018 — Java-. . .

Source: https://habr.com/ru/post/zh-CN419179/


All Articles