重构字符串型系统
去年,我加入了一個項目,該項目接管了另一個未能滿足客戶需求的軟件公司。 如您所知,在“繼承”的項目及其代碼庫中,有許多事情可以并且應(yīng)該加以改進(jìn)。 可悲的是(但并不奇怪)領(lǐng)域模型就是這樣一個孤零零,被遺忘已久的領(lǐng)域之一,它大聲呼喚幫助。
我們知道我們需要動手,但是您如何在一個陌生的項目中改進(jìn)領(lǐng)域模型,在該項目中,所有事情都是如此混雜,糾結(jié)和長成,并具有偶然的復(fù)雜性? 您設(shè)置邊界(分而治之!),在一個區(qū)域中進(jìn)行小幅改進(jìn),然后移到另一個區(qū)域,同時了解風(fēng)景并發(fā)現(xiàn)隱藏在那些可怕的顯而易見的事物背后的更大問題,這些事物一見鐘情。 您可能會感到驚訝,您可以通過進(jìn)行一些小的改進(jìn)并選擇低掛的水果來取得多少成就,但同時您也會傻傻地認(rèn)為它們可以解決由于缺少(或沒有從項目剛開始就進(jìn)行的建模工作就足夠了。 但是,如果沒有這些小的改進(jìn),將很難解決大多數(shù)主要的領(lǐng)域模型問題。
對我來說,通過引入簡單的值對象將更多的表達(dá)能力和類型安全性帶入代碼中,始終是掛在最下面的成果之一。 這是一個總能奏效的技巧,尤其是在處理散布有原始癡迷代碼氣味的代碼庫時,所提到的系統(tǒng)是一個字符串類型的系統(tǒng)。 到處都是這樣的代碼:
public void verifyAccountOwnership(String accountId, String customerId) {...}雖然我敢打賭,每個人都希望它看起來像這樣:
public void verifyAccountOwnership(AccountId accountId, CustomerId customerId) {...}這不是火箭科學(xué)! 我會說這是不費(fèi)吹灰之力的,這總是讓我感到驚訝的是,找到在模糊,無上下文的BigDecimals而不是Amounts,Quantities或Percentages上運(yùn)行的實現(xiàn)是多么容易。
使用特定于域的值對象而不是無上下文基元的代碼是:
- 更具表現(xiàn)力(您無需將字符串映射到腦海中的客戶標(biāo)識符,也不必?fù)?dān)心其中的任何字符串都是空字符串)
- 更容易掌握(不變式被保護(hù)在一個地方,而不是分散在各處的if語句中的代碼庫中)
- 越野車少(我是否將所有這些字符串按正確的順序排列?)
- 更容易開發(fā)(顯式定義更明顯,不變量在您期望的位置得到保護(hù))
- 開發(fā)速度更快(IDE提供了更多幫助,編譯器提供了快速的反饋周期)
而這些只是您幾乎免費(fèi)獲得的一些東西(您只需要使用常識^^)即可。
對價值對象的重構(gòu)聽起來簡直是小菜一碟(此處未考慮命名),您只需在此處提取類,在此處遷移類型,就沒有什么特別的了。 通常就這么簡單,尤其是當(dāng)您要處理的代碼位于單個代碼存儲庫中并在單個進(jìn)程中運(yùn)行時。 但這一次并不那么瑣碎。 并不是說它復(fù)雜得多,它只需要更多的思考(這使得描述一件不錯的工作^^)。
這是一個分布式系統(tǒng),其服務(wù)邊界設(shè)置在錯誤的位置,并且在服務(wù)之間共享了過多的代碼(包括模型)。 邊界設(shè)置得如此糟糕,以至于系統(tǒng)中的許多關(guān)鍵操作都需要與多種服務(wù)進(jìn)行多次交互(大多數(shù)情況下是同步的)。 在描述的上下文中應(yīng)用提到的重構(gòu)存在一個挑戰(zhàn)(不是很大),但這種挑戰(zhàn)不會最終成為創(chuàng)建不必要的層并在服務(wù)邊界引入意外復(fù)雜性的練習(xí)。 在開始重構(gòu)之前,我必須設(shè)置一些規(guī)則,或者甚至是一個關(guān)鍵規(guī)則:服務(wù)(包括后備服務(wù))外部應(yīng)該看不到任何更改。 簡而言之,所有已發(fā)布的合同都保持不變,并且在支持服務(wù)方面不需要進(jìn)行任何更改(例如,無需更改數(shù)據(jù)庫架構(gòu))。 坦率地說,輕而易舉地完成了一些枯燥的工作。
讓我們以String accountId ,并演示必要的步驟。 我們要轉(zhuǎn)這樣的代碼:
public class Account {private String accountId;// rest omitted for brevity }到這個:
public class Account {private AccountId accountId;// rest omitted for brevity }這可以通過引入AccountId值對象來實現(xiàn):
@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只是一個值對象,沒有身份,不會隨時間變化,因此是不可變的。 它在單個位置執(zhí)行所有驗證,并且由于無法實例化AccountId而導(dǎo)致錯誤輸入快速失敗,而不是隨后在隱藏在調(diào)用堆棧下幾層的if語句中失敗。 如果需要保護(hù)任何不變式,您就知道將其放置在哪里以及在哪里尋找它們。
到目前為止一切順利,但是如果Account是一個實體怎么辦? 好吧,您只需實現(xiàn)一個屬性轉(zhuǎn)換器:
public class AccountIdConverter implements AttributeConverter<AccountId, String> {@Overridepublic String convertToDatabaseColumn(AccountId accountId) {return accountId.asString();}@Overridepublic AccountId convertToEntityAttribute(String accountId) {return AccountId.of(accountId);} }然后,您可以通過在轉(zhuǎn)換器實現(xiàn)上直接設(shè)置的@Converter(autoApply = true)或在實體字段上設(shè)置的@Convert(converter = AccountIdConverter.class)啟用@Convert(converter = AccountIdConverter.class) 。
當(dāng)然,并非所有事物都圍繞數(shù)據(jù)庫旋轉(zhuǎn),幸運(yùn)的是,在提到的項目中應(yīng)用的許多不太好的設(shè)計決策中,也有很多好的決策。 如此好的決定之一就是標(biāo)準(zhǔn)化用于進(jìn)程外通信的數(shù)據(jù)格式。 在提到的情況下,它是JSON,因此我需要使JSON有效負(fù)載不受執(zhí)行的重構(gòu)的影響。 最簡單的方法(如果使用Jackson的話)是在實現(xiàn)中添加幾個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 }我從最簡單的解決方案開始。 這不是理想的,但已經(jīng)足夠好了,那時我們還有更多重要的問題要處理。 在不到3個小時的時間里,就完成了JSON序列化和數(shù)據(jù)庫類型轉(zhuǎn)換的工作,我將前兩個服務(wù)從字符串類型的標(biāo)識符移到了基于值對象的服務(wù)中,這些值是系統(tǒng)中最常用的標(biāo)識符。 花了很長時間有兩個原因。
第一個很明顯:在此過程中,我必須檢查null值是否不可能(以及是否可以明確聲明該值)。 沒有這個,整個重構(gòu)將僅僅是代碼完善的練習(xí)。
第二個是我?guī)缀跸肽畹臇|西–您還記得從外部看不到更改的要求嗎? 在將帳戶ID轉(zhuǎn)換為值對象后,草簽定義也發(fā)生了變化,現(xiàn)在帳戶ID不再是字符串而是對象。 這也很容易解決,只需要指定swagger模型替換即可。 對于swagger-maven-plugin,您所需要做的就是將其提供給包含模型替換映射的文件 :
com.example.AccountId: java.lang.String重構(gòu)的結(jié)果是否有明顯的改善? 并非如此,但是您可以通過進(jìn)行許多小的改進(jìn)來改善很多。 盡管如此,這并不是一個小小的改進(jìn),它使代碼更加清晰,并使進(jìn)一步的改進(jìn)變得更加容易。 值得付出努力–我肯定會說:是的。 一個很好的指標(biāo)是其他團(tuán)隊也采用了這種方法。
快速完成一些沖刺,解決了一些更重要的問題,并開始將繼承的,糾結(jié)不清的混亂變成基于六邊形架構(gòu)的更好的解決方案,現(xiàn)在是時候應(yīng)對采用最簡單方法進(jìn)行支持的缺點(diǎn)了JSON序列化。 我們需要做的是將AccountId域?qū)ο笈c與該域無關(guān)的事物分離。 也就是說,我們必須移出定義如何序列化此值對象并刪除耦合到Jackson的域的部分。 為了實現(xiàn)這一點(diǎn),我們創(chuàng)建了處理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,則只需配置以下模塊即可在應(yīng)用程序上下文中注冊該模塊:
@Configuration class JacksonConfig {@BeanModule accountIdSerializationModule() {return new AccountIdSerializationModule();} }實現(xiàn)自定義序列化器也是我們所需要的,因為在所有改進(jìn)中,我們確定了更多的價值對象,其中一些對象更加復(fù)雜-但這是另一篇文章。
翻譯自: https://www.javacodegeeks.com/2018/01/refactoring-stringly-typed-systems.html
總結(jié)
- 上一篇: 如何用电脑摄影头录像(电脑如何用摄像头录
- 下一篇: jpa 事务嵌套事务_JPA 2 | E