767 重构字符串_重构字符串型系统
767 重構字符串
去年,我加入了一個項目,該項目從另一個軟件公司接手,但未能滿足客戶需求。 如您所知,在“繼承”的項目及其代碼庫中,有許多事情可以并且應該加以改進。 可悲的是(但并不奇怪)領域模型就是這樣一個孤零零,被遺忘已久的領域之一,它大聲呼救。
我們知道我們需要動手,但是您如何在一個陌生的項目中改進領域模型,在該項目中,所有事情都是如此混雜,糾結和雜草叢生,并具有偶然的復雜性? 您設置邊界(分而治之!),在一個區域中進行較小的改進,然后移至另一區域,同時了解景觀,并發現隱藏在那些可怕的顯而易見的事物背后的更大問題,這些事物乍一看會傷害您的眼睛。 您可能會感到驚訝,您可以通過進行一些小的改進并選擇低掛的水果來取得多少成就,但同時您也會傻傻地認為它們可以解決由于缺少(或沒有足夠)從項目開始之初就開始進行建模工作。 但是,如果沒有這些小的改進,將很難解決大多數主要的領域模型問題。
對我來說,通過引入簡單的值對象將更多的表現力和類型安全性帶入代碼中,始終是掛在最下面的成果之一。 這是一個總能奏效的技巧,尤其是在處理散布著原始癡迷代碼氣味的代碼庫時,所提到的系統是一個字符串類型的系統。 到處都是這樣的代碼:
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-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
767 重構字符串
總結
以上是生活随笔為你收集整理的767 重构字符串_重构字符串型系统的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 反射式ddos(应用层反射型ddos攻击
- 下一篇: linux u盘制作工具(linux u
