簡體   English   中英

驗證邏輯應該在哪里生效?

[英]Where Should The Validation Logic Live?

如何最好地管理需要復雜驗證邏輯的對象圖的構造? 出於可測試性原因,我想保留依賴注入,無操作構造函數。

可測試性對我來說非常重要,您的建議如何維護代碼的這個屬性?

背景

我有一個普通的java對象,它為我管理一些業務數據的結構:

class Pojo
{
    protected final String p;

    public Pojo(String p) {
        this.p = p;
    }
}

我想確保p的格式是有效的,因為如果沒有這種保證,這個業務對象真的沒有意義; 如果p是無意義的話,甚至不應該創建它。 但是,驗證p是非平凡的。

捕獲

真的,它需要足夠復雜的驗證邏輯,邏輯應該完全可以測試它,所以我在一個單獨的類中有這個邏輯:

final class BusinessLogic() implements Validator<String>
{
    public String validate(String p) throws InvalidFoo, InvalidBar {
        ...
    }
}

可能重復的問題

  • 應該在何處實施驗證邏輯? - 接受的答案對我來說是難以理解的。 我讀作“在班級的本土環境中運行”作為重言式,如何在“班級的本地環境”以外的任何其他方面運行驗證規則? 第2點我沒有理解,所以我無法發表評論。
  • 在哪里提供驗證的邏輯規則? - 兩個答案都表明責任在於客戶/數據提供者,我原則上喜歡這個提供者。 但是,在我的情況下,客戶端可能不是數據的發起者,無法驗證它。
  • 在哪里保持驗證邏輯? - 建議驗證可以由模型擁有,但我發現這種方法不太適合測試。 具體來說,對於每個單元測試,我需要關心所有驗證邏輯,即使在我測試模型的其他部分時 - 我也無法通過遵循建議的方法完全隔離我想要測試的內容。

我的思考到目前為止

雖然以下構造函數公開聲明Pojo的依賴關系並保留其簡單的可測試性,但它完全不安全。 這里沒有任何東西阻止客戶提供一個聲稱每個輸入都可以接受的驗證器!

public Pojo(Validator businessLogic, String p) throws InvalidFoo, InvalidBar {
    this.p = businessLogic.validate(p);
}

所以,我稍微限制了構造函數的可見性,並提供了一個工廠方法,確保驗證然后構造:

@VisibleForTesting
Pojo(String p) {
    this.p = p;
}

public static Pojo createPojo(String p) throws InvalidFoo, InvalidBar {
    Validator businessLogic = new BusinessLogic();
    businessLogic.validate(p);
    return new Pojo(p);
}

現在我可以將createPojo重構為工廠類,這將恢復Pojo的“單一責任”並簡化工廠邏輯的測試,更不用說不再浪費在每個新Pojo上創建新的(無狀態)BusinessLogic的性能優勢。

我的直覺是我應該停下來,要求外界的意見。 我在正確的軌道上嗎?

下面的一些回應要素......讓我知道它是否有意義/回答你的問題。


簡介 :我認為您的系統可以是一個簡單的庫,一個多層應用程序,也可以是一個復雜的分布式系統,它在驗證時實際上沒有太大區別:

  • 客戶端:遠程客戶端(例如HTTP客戶端)或只是另一個調用您的庫的類。
  • 服務:遠程服務(例如REST服務)或您公開API的內容。

在哪里驗證?

您通常希望驗證輸入參數:

  • 客戶端,在將參數傳遞給服務之前,要確保早期對象在線下有效。 如果它是遠程服務或者在參數的生成和對象創建之間存在復雜的流程,則這是特別期望的。

  • 服務方面:

    • 在類級別,在構造函數中,以確保您創建有效的對象;
    • 在子系統級別,即管理這些對象的層(例如DAL持久化你的Pojo );
    • 在您的服務邊界,例如您的庫或控制器的外觀或外部API(如在MVC中,例如REST端點,Spring控制器等)。

如何驗證?

假設如上所述,由於您可能必須在多個位置重用驗證邏輯,因此在實用程序類中提取它可能是個好主意。 這條路:

  • 你不復制它(干!);
  • 你確信系統的每一層都會以同樣的方式驗證;
  • 你可以輕松地單獨測試這個邏輯(因為它是無狀態的)。

更具體地說,您至少會在構造函數中調用此邏輯,以強制實現對象的有效性(考慮將有效的依賴項作為Pojo方法中的算法的前提條件 ):

實用類

public final class PojoValidator() {
    private PojoValidator() {
        // Pure utility class, do NOT instantiate.
    }

    public static String validateFoo(final String foo) {
        // Validate the provided foo here.
        // Validation logic can throw:
        // - checked exceptions if you can/want to recover from an invalid foo, 
        // - unchecked exceptions if you consider these as runtime exceptions and can't really recover (less clutering of your API).
        return foo;
    }

    public static Bar validateBar(final Bar bar) {
        // Same as above...
        // Can itself call other validators.
        return bar;
    }
}

Pojo類 :注意靜態導入語句以提高可讀性。

import static PojoValidator.validateFoo;
import static PojoValidator.validateBar;

class Pojo {
    private final String foo;
    private final Bar bar;

    public Pojo(final String foo, final Bar bar) {
        validateFoo(foo);
        validateBar(bar);
        this.foo = foo;
        this.bar = bar;
    }
}

如何測試我的Pojo?

  1. 您應該添加創建單元測試以確保在構造時調用驗證邏輯(以避免回歸,因為人們稍后通過刪除原因X,Y,Z的驗證邏輯來​​“簡化”構造函數)。

  2. 如果它們很簡單,可以內聯創建依賴項,因為它使您的測試更具可讀性,因為您使用的所有內容都是本地的(滾動較少,心理占用空間較小等)

  3. 但是,如果您的Pojo依賴項的設置足夠復雜/冗長以至於測試不再可讀,您可以將此設置考慮在@Before / setUp方法中,以便測試Pojo邏輯的單元測試真正專注於驗證你的Pojo的行為。

無論如何,我同意Jeff Storey:

  1. 使用有效參數編寫測試,
  2. 沒有驗證的構造函數只是為了您的測試 當你混合生產測試代碼時,它確實是代碼味道,並且肯定會被某人在某個時刻無意中使用,包括服務的穩定性。

最后,將您的測試視為代碼示例,示例或可執行規范:您不希望通過以下方式給出一個令人困惑的示例:

  • 注入無效參數;
  • 注入驗證器,這將使你的API混亂/讀取“怪異”。

如果Pojo需要非常復雜的依賴項怎么辦?

[ 如果是這種情況,請告訴我們 ]

生產代碼 :您可以嘗試在工廠中隱藏這種復雜性。

測試代碼 :要么:

  • 如上所述,以不同的方法考慮這一點; 要么
  • 用你的工廠; 要么
  • 使用模擬參數,並對它們進行配置,以便它們驗證要通過的測試要求。

編輯:一些關於安全上下文中的輸入驗證的鏈接,這也是有用的:

首先,我要說驗證邏輯不應該只存在於客戶端。 這有助於確保您不會在數據存儲中放入無效數據。 如果您添加其他客戶端(例如,您可能有一個胖客戶端,並且您添加了一個Web服務客戶端,則需要在兩個位置維護驗證)。

我不認為你應該有一個構造函數來構造一個不驗證的對象(使用@VisibleForTesting注釋)。 您通常應該使用有效對象進行測試(除非您正在測試錯誤情況)。 此外,在生產代碼中添加用於測試的其他代碼是代碼味道,因為它不是真正的生產代碼。

我認為放置驗證邏輯的適當位置在域對象本身內。 這將確保您不會創建無效的域對象。

我真的不喜歡將驗證器傳遞給域對象的想法。 這為需要了解驗證器的域對象的客戶端提供了大量工作。 如果你想創建一個單獨的驗證器,這可能會增加重用和模塊化的好處,但我不會注入它。 在測試中,您始終可以使用完全繞過驗證的模擬對象。

向域模型添加驗證是Web框架常見的(grails / rails)。 我認為這是一個很好的方法,它不應該損害可測試性。

我想確保p的格式是有效的,因為如果沒有這種保證,這個業務對象真的沒有意義; 如果p是無意義的話,甚至不應該創建它。 但是,驗證p是非平凡的。

  1. “有效格式的p ”意味着p的類型不僅僅是String ,而是一些更具體的類型。

例如, EmailString

class Pojo
{
    protected final EmailString p;

    public Pojo(EmailString p) {
        this.p = p;
    }
}
  1. StringEmailString之間必須有一個轉換器。 這個轉換器作為業務邏輯的一部分,確保p不能無效。 每當消費者擁有String並想要創建Pojo他就會使用這個轉換器。

有幾種變體如何實現此轉換器以方便地捕獲不可轉換的情況,例如.net樣式:

class Converter
{
    public static bool TryParse(String s, out EmailString p) {

    }
}

可測試性對我來說非常重要,您的建議如何維護代碼的這個屬性?

這樣,您可以分別從String解析/轉換邏輯測試Pojo邏輯。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM