[2017-09-04]Abp系列——為什么值對象必須設計成不可變的



本系列目錄:Abp介紹和經驗分享-目錄

這篇是之前翻備忘錄發現漏了的,前陣子剛好同事又提及過這個問題,這里補上。
本文重點在於理解什么是值對象的不可變性。

Abp的ValueObject以及EF的ComplexType

Abp中對應DDD概念的值對象有個基類:ValueObject<T>
這個基類默認重寫了EqualsGetHashCode等用於比較兩個實例是否相等的方法和重載了==!=操作符。
在構建一些比較復雜的實體時,我們可以把屬於同一個概念的多個屬性或字段封裝成一個值對象。
這個值對象在實體中又對應EntityFramework的復雜類型ComplexType
所以在內存中或者在數據庫中,這個對象都可以作為一個整體被賦值、復制或修改

如果不加控制,修改很有可能發生,但是,這類對象,必須設計成不可變的!不能被修改

用兩個測試用例舉個反例

還是Personball.Demo解決方案。
這已經是我的御用示例項目了,我本地git庫保留master分支為當初下載的原始zip文件解壓后的源碼,新開一篇文章就建個新分支折騰。

我們假設一個場景:

我們有一個創業者A的住址信息,他要開辦一家公司A,由於資源不足,他希望把自己的住址登記成公司的辦公地址。

我們在Personball.Demo.Core項目根目錄加兩個值對象Address1Address2

public class Address1 : ValueObject<Address1>
{
    public string RegionCode { get; set; }

    public string Street { get; set; }
}

public class Address2 : ValueObject<Address2>
{
    protected Address2()
    {
        //for orm
    }

    //只能通過ctor構造
    public Address2(string regionCode, string street)
    {
        RegionCode = regionCode;
        Street = street;
    }

    //setter 被保護起來了
    public string RegionCode { get; protected set; }
    //setter 被保護起來了
    public string Street { get; protected set; }
}

新建Creators目錄,加實體Creator:

public class Creator : Entity
{
    public Address1 HouseAddress1 { get; set; }

    public Address2 HouseAddress2 { get; set; }
}

新建Companies目錄,加實體Company:

public class Company : Entity
{
    public string Name { get; set; }

    public Address1 OfficeAddress1 { get; set; }

    public Address2 OfficeAddress2 { get; set; }
}

Personball.Demo.Tests項目中新建目錄Companies加測試文件Company_Tests

如果測試資源管理器無法發現單元測試用例,可以刪掉臨時目錄%TEMP%\VisualStudioTestExplorerExtensions,再重啟VS即可。

第一個測試用例:

[Fact]
public void HouseAddress1_Should_Not_Be_Modified1()
{
    var creatorA = new Creator
    {
        HouseAddress1 = new Address1
        {
            RegionCode = "100100",
            Street = "xxxx路xxxx號101。"
        },
        HouseAddress2 = new Address2("100100", "xxxx路xxxx號101。")
    };

    var companyA = new Company
    {
        Name = "xxx初創公司",
        //公司地址用A的住址,合情合理
        OfficeAddress1 = creatorA.HouseAddress1,
        OfficeAddress2 = creatorA.HouseAddress2
    };

    //迭代N次后,可能會有這種需求(辦公地址后面追加個公司名稱)
    companyA.OfficeAddress1.Street += companyA.Name;

    //斷言失敗,creatorA.HouseAddress1.Street已被修改!
    //creatorA.HouseAddress1.Street.ShouldBe("xxxx路xxxx號101。");
    
    //是同一個實例!
    creatorA.HouseAddress1.ShouldBeSameAs(companyA.OfficeAddress1);
}

不要吐槽上面這個生造的需求,“辦公地址后面加個公司名稱”,只是表達這個意思:

我們很可能在維護了幾個月代碼后,不經意間,會將一個實體的一個值對象賦值給另一個實體,另一個實體又緊接着修改了自己的這個值對象中的某個屬性。

也可能加了這行代碼“辦公地址后面加個公司名稱”的已經是另一個人了。
這個問題如果發生了,很難定位排查。
那么如何防止這種情況發生?

第二個測試用例:

[Fact]
public void HouseAddress2_Should_Not_Be_Modified2()
{
    var creatorA = new Creator
    {
        HouseAddress1 = new Address1
        {
            RegionCode = "100100",
            Street = "xxxx路xxxx號101。"
        },
        HouseAddress2 = new Address2("100100", "xxxx路xxxx號101。")
    };

    var companyA = new Company
    {
        Name = "xxx初創公司",
        OfficeAddress1 = creatorA.HouseAddress1,
        OfficeAddress2 = creatorA.HouseAddress2 //不經意就會這么干
    };

    //迭代N次后,不小心可能會這么干
    //編譯器報錯,setter無法訪問!
    //companyA.OfficeAddress2.Street += companyA.Name;
    
    //想改就必須new一個!
    companyA.OfficeAddress2 = 
        new Address2(companyA.OfficeAddress2.RegionCode, 
                    companyA.OfficeAddress2.Street + companyA.Name);
    
    //斷言通過,creatorA.HouseAddress2.Street不受影響!
    creatorA.HouseAddress2.Street.ShouldBe("xxxx路xxxx號101。");
    
    //不同實例!
    creatorA.HouseAddress2.ShouldNotBeSameAs(companyA.OfficeAddress2);
}

當我們用了Address2,其屬性的setter都被protected限制了從外部直接賦值時,companyA.OfficeAddress2.Street不能被直接修改了!
想修改它時,必須new一個新實例!這時候,對creatorA.HouseAddress2自然是沒有影響的。
這並不是特別高深的原理(很基礎的OOP知識點),但是能從根本上預防上述問題。

總結

值對象必須被設計成不可變的,當你(或者其他人)想修改它時,必須new一個新實例!

值對象必須被設計成不可變的,當你(或者其他人)想修改它時,必須new一個新實例!

值對象必須被設計成不可變的,當你(或者其他人)想修改它時,必須new一個新實例!

重要的事說三遍,剛才說了預防問題的原理很簡單,這個導致問題的原理其實也很簡單。
就是OOP編程語言,其主要類型都是引用類型,變量hold的大多時候都是一個地址。
很多時候都是地址傳來傳去,一個不注意,修改對象的影響范圍是在你預料之外的。
因此,OOP語言基本都有限制可訪問性的關鍵字(基於類的鐵定有,基於原型的沒仔細研究過,不確定)

話說回來,如果有幾年工作經驗了,卻發現好多語言層面的關鍵字被冷落,是不是該反思下。。。

如果說從貧血模型到充血模型是一次成長,從充血模型到重新審視語言層面提供的特性,又是一次成長。

最后啰嗦一句,其實實體的各種屬性更應該被保護起來,限制必須通過方法去修改
否則,你如何保證以后維護代碼的三個月后的自己或者其他人會遵守之前的業務規則?

如果是主從關系的多個實體,那就通過聚合根去約束,更復雜的,通過領域服務去約束。

Over.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM