网站 白名单,wordpress创建页面左侧导航栏,建设工程合同的分类,网站更换运营商要重新备案去年#xff0c;我加入了一个项目#xff0c;该项目接管了另一个未能满足客户需求的软件公司。 如您所知#xff0c;在“继承”的项目及其代码库中#xff0c;有许多事情可以并且应该加以改进。 可悲的是#xff08;但并不奇怪#xff09;领域模型就是这样一个孤零零我加入了一个项目该项目接管了另一个未能满足客户需求的软件公司。 如您所知在“继承”的项目及其代码库中有许多事情可以并且应该加以改进。 可悲的是但并不奇怪领域模型就是这样一个孤零零被遗忘已久的领域之一它大声呼唤帮助。 我们知道我们需要动手但是您如何在一个陌生的项目中改进领域模型在该项目中所有事情都是如此混杂纠结和长成并具有偶然的复杂性 您设置边界分而治之在一个区域中进行小幅改进然后移到另一个区域同时了解风景并发现隐藏在那些可怕的显而易见的事物背后的更大问题这些事物一见钟情。 您可能会感到惊讶您可以通过进行一些小的改进并选择低挂的水果来取得多少成就但同时您也会傻傻地认为它们可以解决由于缺少或没有从项目刚开始就进行的建模工作就足够了。 但是如果没有这些小的改进将很难解决大多数主要的领域模型问题。 对我来说通过引入简单的值对象将更多的表达能力和类型安全性带入代码中始终是挂在最下面的成果之一。 这是一个总能奏效的技巧尤其是在处理散布有原始痴迷代码气味的代码库时所提到的系统是一个字符串类型的系统。 到处都是这样的代码 public void verifyAccountOwnership(String accountId, String customerId) {...} 虽然我敢打赌每个人都希望它看起来像这样 public void verifyAccountOwnership(AccountId accountId, CustomerId customerId) {...} 这不是火箭科学 我会说这是不费吹灰之力的这总是让我感到惊讶的是找到在模糊无上下文的BigDecimals而不是AmountsQuantities或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 AttributeConverterAccountId, 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 StdSerializerAccountId {AccountIdSerializer() {super(AccountId.class);}Overridepublic void serialize(AccountId accountId, JsonGenerator generator, SerializerProvider provider) throws IOException {generator.writeString(accountId.asString());}
}class AccountIdDeserializer extends StdDeserializerAccountId {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