DDD值物件:被遺忘的價值 – SoftwareMill Tech Blog

banq發表於2019-06-15

讓我們看一看為什麼將值物件方法應用於我們的程式碼是真的很有用哦。

我相信我們中的很多人都聽說過域驅動設計(DDD),無處不在的語言以及所有這些奇特的東西。然而,我看到許多程式碼並不使用於基於這種方法的想法。為什麼會這樣?

也許它太複雜了,不能同時適合這一切?應用DDD需要付出巨大的努力和慎重的選擇,這是很多領域知識和變化應用於我們的程式碼以及我們開發人員過去常常思考的方式。

人們更習慣於在我們的程式碼中使用技術級別(如字串,整數,Map,List,迴圈等)術語,而不是使用業務術語或甚至點檢視問題觀點。

那麼為什麼不逐步地部分地使用DDD呢?這可能是一個很好的起點。從我的觀點來看,其中一個起點是值物件(VO)。令人驚訝的是,我們經常使用原始值作為物件,方法或函式的輸入值。VO怎麼了?我們忘記了這個概念嗎?

值物件
值物件是一個包含一些資料的不可變類的例項。重要的是 :這樣的物件是由其屬性的值定義的,沒有標識身份(如實體)。都是一些彼此“公正平等”的資料。(banq注:實體ID標識高於其他屬性資料)

我不會假裝我會在這個概念上取得任何突破,也不會向你展示任何新的東西。如果您想要了解VO概念的更詳細描述,請檢視Martin Fowler不久前寫的內容。即便如此,如果你熟悉它,那麼值得重新閱讀這篇優秀的帖子,以瞭解VO的細微差別。

玩程式碼
讓我們考慮兩個例子,說明VO被“遺忘”的價值。我們將從使用原始最初版本開始。接下來,我們將應用值物件方法,並檢視程式碼如何更改。

雖然我將在程式碼示例中使用Java,但在Lombok功能的支援下,整體構思也可以在其他語言中使用。所以,繼續閱讀;)

什麼是你的身份標識?
第一個示例如下:假設我們有一個服務接受訪問令牌,我們需要從中提取業務識別符號值以進行進一步處理。當前示例中,令牌是String型別。
讓我們從程式碼的初始版本開始。當然,我們正在使用OOP並使用專用的提取器來查詢令牌的值。這裡是:

class BusinessIdExtractor {
   String extract(String accessToken) {
     return JWT.decode(accessToken).getClaim("businessId");
   }
}


很簡單吧?幾行易於閱讀的程式碼。那麼,這個版本有什麼問題?我可以發現一些問題:
- 返回的值可以是任何值; 它是一些字串,
且提供的值也可以是任何東西,
且程式碼不能很好地記錄自己。

我想到的第一件事是包裝返回的值,因此使用此類的編碼器確切地知道返回的資料是什麼。因此,我們引入了BusinessId類:

@Value
@Accessors(fluent = true)
class BusinessId {
    private final String value;
}

class BusinessIdExtractor {
    BusinessId extract(String accessToken) {
        return new BusinessId(JWT.decode(accessToken).getClaim("businessId"));
    }
}

當我們現在看程式碼時,我們可以看到,我們不再處理原始String,但我們已經命名了返回值。程式碼記錄了自己。從該方法返回的值不是任何字串; 我們期待的是一個商業識別符號型別:BusinessId類。

輸入資料怎麼樣?它也是一個字串值。也許我們也應該在這裡實施VO。我們來試試吧。我們建立AccessToken類並應用於該方法:

@Value
@Accessors(fluent = true)
class AccessToken {
    private final String value;
}

class BusinessIdExtractor {
    BusinessId extract(AccessToken accessToken) {
        return new BusinessId(JWT.decode(accessToken.value()).getClaim("businessId"));
    }
}


現在知我們已經道如何應該提供和期望的輸入資料。但是,仍有改進的餘地。既然我們遵循OOP原則,為什麼不將提取機制移到AccessToken類中呢?這值得麼?讓我們來看看。

@Value
@Getter(NONE)
class AccessToken {
    private final String value;

    public BusinessId businessId() {
        return JWT.decode(value).getClaim("businessId");
    }
}

現在,這很方便!更改導致程式碼更少 - 我們不需要單獨的類提取業務識別符號值。我們唯一需要的是在令牌例項上呼叫一個方法。我們在類中封裝了行為以及資料。之前使用過兩種而不是三種。下一步可能是在建構函式中解碼一個令牌值,所以我們不是每次都要求它businessId。

第二個案例:傳送電子郵件給我
我想考慮的另一個例子是使用接受電子郵件地址和訊息的方法傳送電子郵件通知。

我們有一個專門的類,負責我們系統中的特定功能。我們稱之為EmailSender:

public class EmailSender {
    public boolean send(String email, String body) {
        return doSend(email, body);
    }
}

很簡單吧?但是,正如前面的例子,我們可以指出一些我們可以做得更好的地方。

由於我們對字串進行操作,因此我們可以將任何資料傳遞給方法,如上例所示。接下來,由於有兩個相同型別的輸入引數,我們甚至可以使用輸入引數的順序上犯一個愚蠢的錯誤,並按以下方式呼叫該方法:send(body, email)。

首先,我們將引入VO輸入引數:EmailAddress和Payload分別是電子郵件地址和郵件內容:

@Value
@Accessors(fluent = true)
public class EmailAddress {
    private final String value;
}

@Value
@Accessors(fluent = true)
public class Payload {
    private final String message;
}

public class EmailSender {
    public boolean send(EmailAddress emailAddress, Payload payload) {
        return doSend(emailAddress.value(), payload.message());
    }
}


這一舉措解決了一些問題:我們知道了以什麼順序傳遞給方法 - 不再會犯愚蠢的錯誤!透過EmailAddress使用小驗證邏輯擴充套件類,我們可以控制類所持有的資料。
透過引入Payload類可以更輕鬆地新增新欄位(例如,訊息的附件)。

返回值怎麼樣?我們也要重構它,不能用boolean,我們絕對可以做得更好:

@Value
@Accessors(fluent = true)
public class SendingResult {
    private final boolean isSend;
}

public class EmailSender {
    public SendingResult send(EmailAddress emailAddress, Payload payload) {
        return new SendingResult(doSend(emailAddress, payload.message));
    }
}

對於SendingResult類,我們可以對結果進行更復雜的處理,例如,我們可以提供有關發生錯誤的更多詳細資訊,或者為成功傳送操作的持續時間新增更多花哨的資料(如果在域中有意義的話)。

沒有什麼能阻止我們將兩個引數包裝到單個值物件中,所以我們也這樣做:

@Value
@Accessors(fluent = true)
public class Payload {
    private final String body;
}

@Value
@Accessors(fluent = true)
public class EmailMessage {
    private final Email email;
    private final Payload payload;
}

public class EmailSender {
    public SendingResult send(EmailMessage message) {
        return new SendingResult(doSend(message.email(), message.payload().body()));
    }
}


這是開啟一些可能性:使用不同的實現技術作為輸入引數:如將通知傳送給外界的輸入引數的另一個版本可能是KafkaSender或JmsSender。

總結
問題是 - 它是否值得?值物件方法新增了一些額外的程式碼來維護。然而,使用VO簡化程式碼,更容易理解程式碼的意圖是很棒的。此外,它還極大地改善了文件方面的程式碼。

透過使用VO的正確名稱(命名很難,對嗎?),我們正在記錄程式碼。只需要一瞥就可以看到對服務/元件/方法的輸入有什麼期望,以及在呼叫系統的這一部分之後我們得到了什麼。您不必考慮給定String引數所包含的資料型別。它是一些識別符號嗎?或者也許是一個人的地址?可能是任何文字嗎?透過包裝值,我們提供了更易於閱讀和理解的上下文。

最後,如果我希望你從這個閱讀中只有一件事,那麼就這樣吧 - 不要害怕(或者懶惰!)透過對其進行少量修改來使用你的程式碼。誰知道一個簡單的改變(比如在一個原始價值的地方引入VO)將引導你,以及在移動一些程式碼後的下一個想法或想法。

畢竟,如果我們不自己試驗,我們怎麼能獲得任何經驗呢?不,像閱讀部落格或書籍,作為與會者參加會議,或觀看有關網際網路上最令人興奮的主題的簡報等被動活動,如果您不實踐所提出的想法和知識,則無關緊要。你必須付出努力並弄髒你的手。您和只有您負責推進軟體開發人員的道路。但這與開發技能有關,但卻很重要。

 

相關文章