2011-09-13 64 views
6

我想模擬一個Address作爲值對象。因爲這是一個不變的好習慣,所以我選擇不提供任何setter,以便稍後修改它。DDD:如何保持一個複雜的值對象不可變?

一種常用的方法是將數據傳遞給構造函數;然而,當值對象是相當大的,這可能變得相當臃腫:

class Address { 
    public function __construct(
     Point $location, 
     $houseNumber, 
     $streetName, 
     $postcode, 
     $poBox, 
     $city, 
     $region, 
     $country) { 
     // ... 
    } 
} 

另一種方法對子級是提供的參數作爲一個數組,從而得到一個無塵的構造,但是,很可能陷入困境的實施構造函數:

class Address { 
    public function __construct(array $parts) { 
     if (! isset($parts['location']) || ! $location instanceof Point) { 
      throw new Exception('The location is required'); 
     } 
     $this->location = $location; 
     // ... 
     if (isset($parts['poBox'])) { 
      $this->poBox = $parts['poBox']; 
     } 
     // ... 
    } 
} 

這對我來說也有點不自然。

關於如何正確實現一個相當大的值對象的任何建議?

+1

就我個人而言,我認爲如果您的價值對象足夠大,這會導致問題,它需要分解成多個值對象。地址示例對我個人的敏感度似乎很好,但如果發現它太大,它可能會成爲位置+街道地址+城市(城市包括地區和國家)。 – Domenic

+0

@Domenic:這也是一個有趣的方法! – Benjamin

回答

12

large list of parameters的主要問題是可讀性和您將混淆參數的危險。您可以按照Effective Java中所述的Builder pattern解決這些問題。這使得代碼更易讀(不支持命名的特別語言和可選參數):

  • 參數命名,因此更具有可讀性
  • 更難混合起來:

    public class AddressBuilder { 
        private Point _point; 
        private String _houseNumber; 
    
        // other parameters 
    
        public AddressBuilder() { 
        } 
    
        public AddressBuilder WithPoint(Point point) { 
         _point = point; 
         return this; 
        } 
    
        public AddressBuilder WithHouseNumber(String houseNumber) { 
         _houseNumber = houseNumber; 
         return this; 
        } 
    
        public Address Build() { 
         return new Address(_point, _houseNumber, ...); 
        } 
    } 
    
    Address address = new AddressBuilder() 
        .WithHouseNumber("123") 
        .WithPoint(point) 
        .Build(); 
    

    優勢門牌號碼

  • 可以使用您自己的參數命令
  • 可選參數可以省略

我可以想到的一個缺點是,忘記指定其中一個參數(例如,不會調用WithHouseNumber)將導致運行時錯誤,而不是使用構造函數時的編譯時錯誤。您還應該考慮使用更多的值對象,例如PostalCode(與傳遞字符串相反)。

在相關說明中,有時業務需求需要更改Value對象的一部分。例如,最初輸入地址時,街道號可能拼寫錯誤,現在需要更正。由於您將Address模型化爲不可變對象,因此不存在setter。解決這個問題的一個可能的方法是在地址值對象上引入一個「Side-Effect-Free」函數。該函數將返回對象本身的副本,新的街道名稱的除外:

public class Address { 
    private readonly String _streetName; 
    private readonly String _houseNumber; 

    ... 

    public Address WithNewStreetName(String newStreetName) { 
     // enforce street name rules (not null, format etc) 

     return new Address(
      newStreetName 
      // copy other members from this instance 
      _houseNumber); 
    } 

    ... 
} 
+0

非常有趣的答案,謝謝。這就是我們稱之爲工廠的原因,還是工廠模式爲實體保留? – Benjamin

+0

DDD Factory可用於構建複雜的值對象。這個Builder可以被認爲是一個DDD工廠。 – Dmitry

+0

這個答案是正確的! – jpierson

-1

不變的是適合並行計算,不堵,不鎖的,一成不變的是高性能和良好的擴展性。

因此Value Object可以在併發系統中運行得更好,包含在分發系統中,用新的VO取代舊的VO,不需要更新,因此不會阻塞。

0

這是域驅動設計示例的常見問題。領域專家失蹤了,那個人會告訴你什麼是地址和它的要求。我懷疑域名專家會告訴你,一個地址沒有重點。你可能能夠從一個地址產生一個點,但它不需要一個點。也是P.O. Box在地址中不會是單獨的值。您可能需要一個郵局地址類(POBoxAddress)我說這是因爲這個類看起來像它是由開發商而不是發貨或計費域專家定義的。通過與域專家交談,您可以減少構造函數的參數數量。您可以開始將參數分組爲值對象。你可以創建一個City值對象。這可能需要城市,地區/州和國家。除非我知道地區和國家,否則我認爲城市名稱沒有多大意義。說法巴黎意味着什麼,但巴黎,伊利諾伊州,美國或巴黎,法蘭西島,FR給你一個完整的圖片。所以這也會減少地址對象的計數參數。

如果你走下DDD的道路找到你正在編碼的域的域專家,你不應該是專家。有時問題不應該由代碼或漂亮的設計模式來解決。