Bean驗證反模式 - reflectoring.io

banq發表於2019-09-21

Bean驗證是在Java生態系統中實施驗證邏輯的事實上的標準,它是一個很好的工具。

但是,在最近的專案中,我對Bean驗證進行了更深入的思考,並確定了一些我認為是反模式的實踐。

反模式免責宣告

就像每一次關於模式和反模式的討論一樣,都涉及一些觀點和個人經驗。在一種情況下使用反模式很可能是在另一種情況下的最佳實踐(反之亦然),因此,請不要將下面的討論視為宗教規則,而應將其作為對該主題進行思考和進行建設性討論的觸發點。

反模式1:僅在持久層中進行驗證

使用Spring,在持久層中設定Bean驗證非常容易。假設我們有一個帶有一些bean驗證批註的實體以及一個關聯的Spring Data儲存庫:

@Entity
public class Person {

  @Id
  @GeneratedValue
  private Long id;

  @NotEmpty
  private String name;

  @NotNull
  @Min(0)
  private Integer age;

  // getters and setters omitted

}

public interface PersonRepository extends CrudRepository<Person, Long> {

  // default CRUD methods provided by CrudRepository

}

只要我們在類路徑classpath上有一個像Hibernate Validator這樣的bean驗證實現庫包,每次呼叫儲存庫方法save()時,都會觸發一次驗證。如果傳入物件根據bean驗證註釋判斷是無效的,將丟擲ConstraintViolationException

持久層是驗證的正確地方嗎?

我認為至少它不是唯一可以驗證的地方。

在常見的Web應用程式中,持久層是最底層。我們通常在上面有一個業務層和一個Web層。資料通過業務層流入Web層,最後到達持久層。

如果僅在持久層中進行驗證,那麼我們將承擔Web和業務層使用無效資料的風險!

無效的資料可能會導致業務層中的嚴重錯誤(如果我們希望業務層中的資料有效)或導致超級防禦性的程式設計,並且需要在整個業務層中進行手動驗證檢查(一旦我們瞭解到其中的資料業務層不能被信任)。

總之,對業務層的輸入應該已經有效。這樣,在持久層中的驗證就可以充當附加的安全網,但不是唯一的驗證位置。

反模式2:驗證太多

除了驗證得過少之外,我們會驗證得太多。這不是特定於Bean驗證的問題,而是通常具有驗證功能都會具有的問題。

在通過Web層進入系統之前,使用Bean驗證對資料進行驗證。Web控制器將傳入的資料轉換為可以傳遞給業務服務的物件。業務服務不信任Web層,因此它使用Bean驗證再次驗證該物件。

在執行實際的業務邏輯之前,業務服務將以程式設計方式檢查我們能想到的每個約束,以便絕對不會出錯。最後,持久層在將資料儲存到資料庫之前再次對其進行驗證。

這好像是一種不錯的防禦性驗證方法,但它帶來的問題多於我的經驗。

首先,如果我們在很多地方使用Bean驗證,那麼到處都會有Bean驗證註釋。如有疑問,我們將向物件新增Bean驗證批註,即使它畢竟可能不會得到驗證。最後,我們花時間在新增和修改可能根本不執行的驗證規則上。

其次,到處進行驗證會導致意圖明確但最終導致錯誤的驗證規則。

想象一下,我們正在驗證一個人的名字和姓氏,以使其至少包含三個字元。這不是必需的,但是無論如何我們都新增了此驗證,因為在我們的環境中,不驗證是不禮貌的。有一天,我們會收到一個錯誤報告,稱一個名為“ Ed Sheeran”的人未能在我們的系統中註冊,並且剛剛在推特上引發了一場狗屎風暴。

第三,到處驗證會減慢開放速度。

如果我們在整個程式碼庫中散佈了驗證規則,其中一些在Bean驗證批註中,而另一些在純程式碼中,則其中的某些可能會妨礙我們正在構建的新功能。但是我們不能僅僅刪除那些驗證,畢竟,有人將它們放在那裡一定有道理。我們放慢了腳步,因為我們必須仔細考慮每個驗證,然後才能應用更改。

最後,由於驗證規則遍及整個程式碼,如果遇到意外的驗證錯誤,我們將不知道在哪裡尋找解決方案。

簡而言之,我們應該有一個明確而集中的驗證策略,而不是在任何地方驗證所有內容。

反模式3:使用驗證組進行用例驗證

Bean驗證JSR提供了稱為驗證組的功能。此功能使我們可以將驗證註釋與某些組相關聯,以便我們可以選擇要驗證的組:

public class Person {

  @Null(groups = ValidateForCreate.class)
  @NotNull(groups = ValidateForUpdate.class)
  private Long id;

  @NotEmpty
  private String name;

  @NotNull
  @Min(value = 18, groups = ValidateForAdult.class)
  @Min(value = 0, groups = ValidateForChild.class)
  private int age;

  // getters and setters omitted

}

當一個Person建立時,Id可以為空,但是修改時,不能為空。

首先,我們故意違反“單一責任原則”。

其次,它很難閱讀。

我提議不使用驗證組。

特定於用例的語義使用程式碼驗證,並且模型程式碼不依賴於用例。業務規則使用程式碼實現,成為“豐富”充血領域模型的一部分,並且可以通過查詢方法進行訪問。

有意識地驗證

Bean驗證是一個觸手可及的好工具,但好的工具會帶來很大的責任感(聽起來有些陳詞濫調,但是如果您問我的話,這很重要)。

我們應該有一個清晰的驗證策略,告訴我們在哪裡進行驗證以及何時使用哪種工具進行驗證,而不是對所有內容都使用Bean驗證並在各處進行驗證。

我們應該將句法驗證與語義驗證分開。語法驗證是Bean驗證批註支援的宣告式樣式的完美用例,而語義驗證在純程式碼中更易讀。

相關文章