重构字符串型系统

去年,我加入了一个项目,该项目接管了另一个未能满足客户需求的软件公司。 如您所知,在“继承”的项目及其代码库中,有许多事情可以并且应该加以改进。 可悲的是(但并不奇怪)领域模型就是这样一个孤零零,被遗忘已久的领域之一,它大声呼唤帮助。

我们知道我们需要动手,但是您如何在一个陌生的项目中改进领域模型,在该项目中,所有事情都是如此混杂,纠结和长成,并具有偶然的复杂性? 您设置边界(分而治之!),在一个区域中进行小幅改进,然后移到另一个区域,同时了解风景并发现隐藏在那些可怕的显而易见的事物背后的更大问题,这些事物一见钟情。 您可能会感到惊讶,您可以通过进行一些小的改进并选择低挂的水果来取得多少成就,但同时您也会傻傻地认为它们可以解决由于缺少(或没有从项目刚开始就进行的建模工作就足够了。 但是,如果没有这些小的改进,将很难解决大多数主要的领域模型问题。

对我来说,通过引入简单的值对象将更多的表达能力和类型安全性带入代码中,始终是挂在最下面的成果之一。 这是一个总能奏效的技巧,尤其是在处理散布有原始痴迷代码气味的代码库时,所提到的系统是一个字符串类型的系统。 到处都是这样的代码:

public void verifyAccountOwnership(String accountId, String customerId) {...}

虽然我敢打赌,每个人都希望它看起来像这样:

public void verifyAccountOwnership(AccountId accountId, CustomerId customerId) {...}

这不是火箭科学! 我会说这是不费吹灰之力的,这总是让我感到惊讶的是,找到在模糊,无上下文的BigDecimals而不是Amounts,Quantities或Percentages上运行的实现是多么容易。

使用特定于域的值对象而不是无上下文基元的代码是:

  • 更具表现力(您无需将字符串映射到脑海中的客户标识符,也不必担心其中的任何字符串都是空字符串)
  • 更容易掌握(不变式被保护在一个地方,而不是分散在各处的if语句中的代码库中)
  • 越野车少(我是否将所有这些字符串按正确的顺序排列?)
  • 更容易开发(显式定义更明显,不变量在您期望的位置得到保护)
  • 开发速度更快(IDE提供了更多帮助,编译器提供了快速的反馈周期)

而这些只是您几乎免费获得的一些东西(您只需要使用常识^^)即可。

对价值对象的重构听起来简直是小菜一碟(此处未考虑命名),您只需在此处提取类,在此处迁移类型,就没有什么特别的了。 通常就这么简单,尤其是当您要处理的代码位于单个代码存储库中并在单个进程中运行时。 但这一次并不那么琐碎。 并不是说它复杂得多,它只需要更多的思考(这使得描述一件不错的工作^^)。

这是一个分布式系统,其服务边界设置在错误的位置,并且在服务之间共享了过多的代码(包括模型)。 边界设置得如此糟糕,以至于系统中的许多关键操作都需要与多种服务进行多次交互(大多数情况下是同步的)。 在描述的上下文中应用提到的重构存在一个挑战(不是很大),但这种挑战不会最终成为创建不必要的层并在服务边界引入意外复杂性的练习。 在开始重构之前,我必须设置一些规则,或者甚至是一个关键规则:服务(包括后备服务)外部应该看不到任何更改。 简而言之,所有已发布的合同都保持不变,并且在支持服务方面不需要进行任何更改(例如,无需更改数据库架构)。 坦率地说,轻而易举地完成了一些枯燥的工作。

让我们以String accountId ,并演示必要的步骤。 我们要转这样的代码:

public class Account {private String accountId;// rest omitted for brevity
}

到这个:

public class Account {private AccountId accountId;// rest omitted for brevity
}

这可以通过引入AccountId值对象来实现:

@ToString
@EqualsAndHashCode
public class AccountId {private final String accountId;private AccountId(String accountId) {if (accountId == null || accountId.isEmpty()) {throw new IllegalArgumentException("accountId cannot be null nor empty");}// can account ID be 20 characters long?// are special characters allowed?// can I put a new line feed in the account ID?this.accountId = accountId;}public static AccountId of(String accountId) {return new AccountId(accountId);}public String asString() {return accountId;}
}

AccountId只是一个值对象,没有身份,不会随时间变化,因此是不可变的。 它在单个位置执行所有验证,并且由于无法实例化AccountId而导致错误输入快速失败,而不是随后在隐藏在调用堆栈下几层的if语句中失败。 如果需要保护任何不变式,您就知道将其放置在哪里以及在哪里寻找它们。

到目前为止一切顺利,但是如果Account是一个实体怎么办? 好吧,您只需实现一个属性转换器:

public class AccountIdConverter implements AttributeConverter<AccountId, String> {@Overridepublic String convertToDatabaseColumn(AccountId accountId) {return accountId.asString();}@Overridepublic AccountId convertToEntityAttribute(String accountId) {return AccountId.of(accountId);}
}

然后,您可以通过在转换器实现上直接设置的@Converter(autoApply = true)或在实体字段上设置的@Convert(converter = AccountIdConverter.class)启用@Convert(converter = AccountIdConverter.class)

当然,并非所有事物都围绕数据库旋转,幸运的是,在提到的项目中应用的许多不太好的设计决策中,也有很多好的决策。 如此好的决定之一就是标准化用于进程外通信的数据格式。 在提到的情况下,它是JSON,因此我需要使JSON有效负载不受执行的重构的影响。 最简单的方法(如果使用Jackson的话)是在实现中添加几个Jackson注释:

public class AccountId {@JsonCreatorpublic static AccountId of(@JsonProperty("accountId") String accountId) {return new AccountId(accountId);}@JsonValuepublic String asString() {return accountId;}// rest omitted for brevity
}

我从最简单的解决方案开始。 这不是理想的,但已经足够好了,那时我们还有更多重要的问题要处理。 在不到3个小时的时间里,就完成了JSON序列化和数据库类型转换的工作,我将前两个服务从字符串类型的标识符移到了基于值对象的服务中,这些值是系统中最常用的标识符。 花了很长时间有两个原因。

第一个很明显:在此过程中,我必须检查null值是否不可能(以及是否可以明确声明该值)。 没有这个,整个重构将仅仅是代码完善的练习。

第二个是我几乎想念的东西–您还记得从外部看不到更改的要求吗? 在将帐户ID转换为值对象后,草签定义也发生了变化,现在帐户ID不再是字符串而是对象。 这也很容易解决,只需要指定swagger模型替换即可。 对于swagger-maven-plugin,您所需要做的就是将其提供给包含模型替换映射的文件 :

com.example.AccountId: java.lang.String

重构的结果是否有明显的改善? 并非如此,但是您可以通过进行许多小的改进来改善很多。 尽管如此,这并不是一个小小的改进,它使代码更加清晰,并使进一步的改进变得更加容易。 值得付出努力–我肯定会说:是的。 一个很好的指标是其他团队也采用了这种方法。

快速完成一些冲刺,解决了一些更重要的问题,并开始将继承的,纠结不清的混乱变成基于六边形架构的更好的解决方案,现在是时候应对采用最简单方法进行支持的缺点了JSON序列化。 我们需要做的是将AccountId域对象与与该域无关的事物分离。 也就是说,我们必须移出定义如何序列化此值对象并删除耦合到Jackson的域的部分。 为了实现这一点,我们创建了处理AccountId序列化的Jackson模块:

class AccountIdSerializer extends StdSerializer<AccountId> {AccountIdSerializer() {super(AccountId.class);}@Overridepublic void serialize(AccountId accountId, JsonGenerator generator, SerializerProvider provider) throws IOException {generator.writeString(accountId.asString());}
}class AccountIdDeserializer extends StdDeserializer<AccountId> {AccountIdDeserializer() {super(AccountId.class);}@Overridepublic AccountId deserialize(JsonParser json, DeserializationContext cxt) throws IOException {String accountId = json.readValueAs(String.class);return AccountId.of(accountId);}
}class AccountIdSerializationModule extends Module {@Overridepublic void setupModule(SetupContext setupContext) {setupContext.addSerializers(createSerializers());setupContext.addDeserializers(createDeserializers());}private Serializers createSerializers() {SimpleSerializers serializers = new SimpleSerializers();serializers.addSerializer(new AccountIdSerializer());return serializers;}private Deserializers createDeserializers() {SimpleDeserializers deserializers = new SimpleDeserializers();deserializers.addDeserializer(AccountId.class, new AccountIdDeserializer());return deserializers;}// rest omitted for brevity
}

如果您使用的是Spring Boot,则只需配置以下模块即可在应用程序上下文中注册该模块:

@Configuration
class JacksonConfig {@BeanModule accountIdSerializationModule() {return new AccountIdSerializationModule();}
}

实现自定义序列化器也是我们所需要的,因为在所有改进中,我们确定了更多的价值对象,其中一些对象更加复杂-但这是另一篇文章。

翻译自: https://www.javacodegeeks.com/2018/01/refactoring-stringly-typed-systems.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/348237.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OFDM仿真程序,可直接运行,注释详细(没人注释比我还详细了)

OFDM仿真程序 clc clear allIFFT_bin_length128; %IFFT点数128个 carrier_count50; %子信道&#xff08;子载波&#xff09;数目 bits_per_symbol2; %4进制符号 symbols_per_carrier200;%每个子信道或者说子载波有200个符号 SNR0:1:40; for num1:41baseband_out_lengthcarrie…

Delta-Sigma调制(DSM)技术

前言 数字信号处理和通信系统的性能很大程度上受到了模拟信号到数字信号转换接口——ADC的精度和分辨率的限制。而传统的线性脉冲编码调制&#xff08;PCM&#xff09;ADC受到了制造工艺的限制&#xff0c;无法达到很高的分辨率。但基于Delta-Sigma调制技术的ADC可以在现有工艺…

无载波幅度和相位调制(CAP)与QAM调制的详细解析(可见光通信应用场景),以及CAP matlab程序下载链接

文章目录前言一、QAM调制&#xff1f;二、无载波幅度和相位调制&#xff08;CAP)三、CAP调制与QAM调制之间的联系&#xff08;异同点&#xff09;四、CAP调制相比于QAM调制的优缺点4.1、优点4.2、缺点五、无载波幅度和相位调制matlab程序五、Reference前言 目前的通信系统中&a…

jpa 事务嵌套事务_JPA 2 | EntityManagers,事务及其周围的一切

jpa 事务嵌套事务介绍 对我来说&#xff0c;最令人困惑和不清楚的事情之一是&#xff0c;作为Java开发人员&#xff0c;一直是围绕事务管理的谜团&#xff0c;尤其是JPA如何处理事务管理。 事务什么时候开始&#xff0c;什么时候结束&#xff0c;实体的持久化方式&#xff0c;持…

Matlab中装载和存储实验数据的操作

一、装载实验数据 例如&#xff0c;以下程序&#xff0c;其中path是路径&#xff0c;strcat函数将后面的参数组合成一个字符串。load函数将由twoband_CAP4_400MBd_2000MSa_float_字符串与Tx.txt构成的: twoband_CAP4_400MBd_2000MSa_float_Tx.txt文件读取至matlab中。 numSam…

无服务器革命:好,坏和丑

“这是愚蠢的。 比愚蠢还糟&#xff1a;这是一场营销炒作。” ‐ 理查德斯托曼 &#xff08; Richard Stallman&#xff09;对云计算的评论&#xff0c;2008年9月 而且&#xff0c;十年后&#xff0c;当有人提到这个词时&#xff0c;您开始三思而后行&#xff1a;是到天上掉的…

MATLAB中,信号的频谱图该怎么绘制?横坐标如何标注出频率值?

一、什么是频谱&#xff1f; 频谱的全称是频率谱密度。在对时域信号进行认识和研究的过程中非常不便&#xff0c;那我们该如何更直观地认识信号&#xff0c;更清楚地了解信号的特点呢&#xff1f; 利用傅里叶变换将时域信号变换到频域。 我们知道&#xff0c;在通信领域里傅…

Verilog HDL中模块参数传递的方法

文章目录前言一、参数传递二、参数传递方法1.方法一2.方法二3.方法三总结前言 “parameter”是Verilog HDL中的一个关键字&#xff0c;代表着参数型常量&#xff0c;即用parameter来定义一个标识符代表一个常量&#xff0c;这样可以提高程序的可读性与可维护性。 例如&#xf…

仔细看看_仔细看看,您会发现需要改进的地方

仔细看看我建议您做一个练习&#xff1a;明天早上返回工作时&#xff0c;浏览项目的源代码&#xff0c;并尝试寻找重构的机会。 即使老板没有要求也要这样做。 这样做是因为您想要一些激动人心的工作时间。 重构是改变已经可以正常工作的艺术 。 但是要进行重构&#xff0c;您…

Verilog HDL中位运算符、逻辑运算符和缩减运算符的区别

文章目录前言一、单目运算符、双目运算符和三目运算符二、位运算符三、逻辑运算符四、缩减运算符五、总结前言 我们在学习和理解Verilog HDL中的一些运算符的意义时&#xff0c;可能会对一些运算符的使用产生混乱&#xff0c;因此本文整理了Verilog HDL中&与&&、|…

Verilog HDL中容易生成锁存器的两种情况

在Verilog HDL的程序设计中&#xff0c;有两种情况会生成锁存器。 第一种情况 在always块中使用if语句&#xff0c;但是没有else&#xff0c;这会导致当条件不成立时&#xff0c;没有其他语句可执行&#xff0c;使得被赋值的寄存器一直保持不变&#xff0c;”锁存“住。 第二…

Spring Security与Maven教程

1.简介 在这篇文章中&#xff0c;我们将演示如何针对非常特定的用例将Maven依赖项用于Spring Security。 我们使用的所有库的最新版本都可以在Maven Central上找到。 在项目中&#xff0c;了解Maven依赖项的工作方式和管理方式对于有效的构建周期非常重要&#xff0c;并且对于…

EbN0、SNR、0.1nmOSNR的区别与联系

文章目录前言一、SNR与EbN0二、0.1nmOSNR1、波长宽度与带宽的换算2、0.1nmOSNR2.1、单极化信号2.2、双极化信号总结前言 无论是无线通信、光通信或者可见光通信系统中&#xff0c;我们经常会遇到信噪比的概念&#xff0c;但大多数&#xff0c;我们用的都是信号与噪声功率比&am…

利用Verilog HDL实现序列检测器,附上仿真程序。

文章目录一、序列检测器二、状态转移图三、序列检测器Verilog HDL程序1、源程序2、测试平台程序四、仿真结果五、总结一、序列检测器 序列检测器的逻辑功能就是将一个指定的比特序列从一串较长的比特流中识别出来。 例如&#xff1a;针对一个较长的比特流01001001001111010101…

在Java中使用Google的协议缓冲区

最近发布了 有效的Java第三版 &#xff0c;我一直对确定此类Java开发书籍的更新感兴趣&#xff0c;该书籍的最新版本仅通过Java 6进行了介绍 。 在此版本中&#xff0c;显然有与Java 7 &#xff0c; Java 8和Java 9密切相关的全新项目&#xff0c;例如第7章&#xff08;“ Lamb…

不同阶QAM调制星座图中,符号能量的归一化计算原理

文章目录前言一、归一化能量计算原理二、Matlab中如何得到归一化能量符号总结前言 在基于QAM调制的matlab仿真程序中&#xff0c;我们通常会产生二进制比特流&#xff0c;并最终映射成QAM符号&#xff0c;该符号大都是格雷编码的。在坐标系中&#xff0c;相邻符号之间的横纵坐…

4qam、16qam、64qam、256qam理论仿真曲线

本博文给出了4qam、16qam、64qam、256qam理论仿真曲线&#xff0c;画出了EbN0 vs BER的曲线图&#xff0c;可以作为大家学习的一个参考。 仿真结果: %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Theoretical ber curves of different orde…

建立时间、保持时间与亚稳态

文章目录一、建立时间与保持时间二、亚稳态现象总结一、建立时间与保持时间 建立时间&#xff08;set up time&#xff09;是指在触发器的时钟信号上升沿到来以前&#xff0c;数据从不稳定到稳定所需要的时间&#xff0c;一般用TsuT_{su}Tsu​表示。 保持时间是指在触发器的时…

java ee空指针_Java EE 7是最终版本。 思想,见解和进一步的指针。

java ee空指针我们花了不到三年的时间才推出了下一个Java EE版本 。 今年4月16日&#xff0c; JCP EC对JSR 342进行了投票并获得批准。 这是一个成功的故事&#xff0c;因为在去年八月下旬的最后时刻撤消了拥有云就绪平台的最初想法。 作为EG的成员&#xff0c;撰写即将发布的功…

Java的原始字符串文字

似乎“ 原始字符串文字 ”即将出现在Java中。 JEP 326开始的&#xff08;“原始字符串字面量”&#xff09; 发行JDK-8196004 &#xff0c;并宣布为“新JEP候选人” 3月2日的JEP和相关问题的人指出&#xff0c;“Java的仍然是一小群现代编程语言中的一个不为原始字符串提供语言…