2011-06-02 85 views
12

我寫我自己一個漂亮簡單的小域模型,與對象圖,看起來像這樣從構建扁平DTO的對象圖:使用訪問者模式

-- Customer 
    -- Name : Name 
    -- Account : CustomerAccount 
    -- HomeAddress : PostalAddress 
    -- InvoiceAddress : PostalAddress 
    -- HomePhoneNumber : TelephoneNumber 
    -- WorkPhoneNumber : TelephoneNumber 
    -- MobilePhoneNumber : TelephoneNumber 
    -- EmailAddress : EmailAddress 

這種結構是完全賠率我必須使用遺留數據庫,所以我定義了一個扁平的DTO,它包含客戶圖中每個元素的數據 - 我在數據庫中具有視圖和存儲過程,這些視圖和存儲過程允許我使用數據進行交互這兩個方向的扁平結構,這一切工作正常&丹迪:)

將領域模型扁平化爲DTO以進行插入/更新很簡單,但是我遇到的問題是採用DTO並從中創建領域模型......我的第一個想法是實現訪問每個元素的訪問者在客戶圖形,並在必要時注入從DTO值,東西有點像這樣:

class CustomerVisitor 
{ 
    public CustomerVisitor(CustomerDTO data) {...} 

    private CustomerDTO Data; 

    public void VisitCustomer(Customer customer) 
    { 
     customer.SomeValue = this.Data.SomeValue; 
    } 

    public void VisitName(Name name) 
    { 
     name.Title  = this.Data.NameTitle; 
     name.FirstName = this.Data.NameFirstName; 
     name.LastName = this.Data.NameLastName; 
    } 

    // ... and so on for HomeAddress, EmailAddress etc... 
} 

這是理論和它看起來像時,它的佈局只是這樣的:)

完善思路但爲了這個工作,整個對象圖需要在訪問者erm訪問之前構建,否則我會得到NRE的左側和中間。

我想要做的就是讓訪問者在訪問每個元素時將對象分配給圖形,目標是對DTO中缺少數據的對象使用特殊情況模式,例如。

public void VisitMobilePhoneNumber(out TelephoneNumber mobileNumber) 
{ 
    if (this.Data.MobileNumberValue != null) 
    { 
     mobileNumber = new TelephoneNumber 
     { 
      Value = this.Data.MobileNumberValue, 
      // ... 
     }; 
    } 
    else 
    { 
     // Assign the missing number special case... 
     mobileNumber = SpecialCases.MissingTelephoneNumber.Instance; 
    } 
} 

這一點我真的以爲會工作,但C#引發我一個錯誤的:

myVisitor.VisitHomePhone(out customer.HomePhoneNumber); 

因爲你不能用這種方式:(

所以通過REF/out參數我留下來訪問獨立元素,並在完成時重建圖形:

在此po詮釋我知道我離訪客模式很遠,而且離工廠更近,我開始懷疑我是否從一開始就錯誤地接近了這個事情..

有其他人跑過成這樣的問題?你是如何克服它的?有沒有適合這種情況的設計模式?

對不起張貼這樣的looong問題,並閱讀這遠:)

編輯在回答弗洛裏安格萊納赫和gjvdkamp的有用的答案做得很好,我最後選擇了一個相對簡單的工廠實現看起來像這樣:

class CustomerFactory 
{ 
    private CustomerDTO Data { get; set; } 

    public CustomerFactory(CustomerDTO data) { ... } 

    public Customer CreateCustomer() 
    { 
     var customer = new Customer(); 
     customer.BeginInit(); 
     customer.SomeFoo = this.Data.SomeFoo; 
     customer.SomeBar = this.Data.SomeBar 
     // other properties... 

     customer.Name = this.CreateName(); 
     customer.Account = this.CreateAccount(); 
     // other components... 

     customer.EndInit(); 
     return customer; 
    } 

    private Name CreateName() 
    { 
     var name = new Name(); 
     name.BeginInit(); 
     name.FirstName = this.Data.NameFirstName; 
     name.LastName = this.Data.NameLastName; 
     // ... 
     name.EndInit(); 
     return name; 
    } 

    // Methods for all other components... 
} 

然後我寫了一個ModelMediator類來處理數據層和領域模型之間的互動...

class ModelMediator 
{ 
    public Customer SelectCustomer(Int32 key) 
    { 
     // Use a table gateway to get a customer DTO.. 
     // Use the CustomerFactory to construct the domain model... 
    } 

    public void SaveCustomer(Customer c) 
    { 
     // Use a customer visitor to scan for changes in the domain model... 
     // Use a table gateway to persist the data... 
    } 
} 
+4

像Automapper這樣的東西會爲你工作嗎? HTTP://automapper.codeplex。com/ – bentayloruk 2011-06-02 16:52:42

+0

@bentayloruk謝謝你的迴應,我已經評估過automapper(和valueinjeter),但不幸的是,這兩者都不合適,因爲我的域對象實現了ISupportInitialize,而且在注入DTO中的任何屬性之前需要BeginInit(),EndInit ()後 - 我不認爲這是可能的與Automapper :( – MattDavey 2011-06-02 20:47:02

+0

如果您使用.net 4,那麼我會建議懶惰類將幫助你一點:) – Nathan 2011-06-06 08:31:19

回答

7

我覺得你真的很過分複雜的東西在這裏。只需使用工廠方法,讓域對象清楚地說明他們依賴的其他域對象。

class Customer 
{ 
    private readonly Name name; 
    private readonly PostalAddress homeAddress; 

    public Customer(Name name, PostalAddress homeAddress, ...) 
    { 
     this.name = name; 
     this.homeAddress = homeAddress; 
     ... 
    } 
} 

class CustomerFactory 
{ 
    Customer Create(CustomerDTO customerDTO) 
    { 
     return new Customer(new Name(...), new PostalAdress(...)); 
    } 
} 

如果您需要從客戶的依賴,以CustomerDTO傳遞DTO作爲附加參數傳遞給構造,可能包裹在一個附加的抽象。

這種方式將保持清潔,可測試和易於理解。

+2

感謝您的回答,您是對的我讓事情變得太複雜。我不希望領域模型類去了解DTO的任何知識,所以必須有一些中介可以在它們之間映射。我認爲你提到的一個工廠類是進行處理的方式:) – MattDavey 2011-06-06 10:57:21

+0

我已經來了解決方案並將我的答案放在原始問題中。因爲你和gjvdkamp都以同樣的方式幫助我,所以我要離開賞金到期,屆時它會自動以最多的票進入答案。我認爲這是最公平的方式:) – MattDavey 2011-06-06 14:46:38

0

你可以拿我這裏所描述的計算策略:convert a flat database resultset into hierarchical object collection in C#

背後的想法是閱讀的對象,如客戶,並把它變成一個字典。當讀取例如CustomerAccount,您現在可以從詞典獲取客戶並將客戶帳戶添加到客戶。

您只需要對所有數據進行一次迭代來構建對象圖。

+0

這與我的場景有點不同 - 我有一組代表分層圖中每個數據點的謹慎值,而不是一系列需要添加到集合中的類似數據點。不過謝謝! – MattDavey 2011-06-06 10:41:22

5

我不認爲我會去一個訪客。如果您在設計時不知道您需要在後面執行哪些操作,那麼這樣做會比較合適,因此您可以開放課程以允許其他人編寫實現該邏輯的訪問者。或者有很多事情需要你去做,你不想用這個混亂你的課堂。

你想要做的是從DTO創建一個類的實例。由於類和DTO的結構是緊密相關的(你在數據庫中做了你的映射,我假設你處理所有的映射問題,並且有一個直接映射到你的客戶結構的DTO格式),你知道設計時間你需要什麼。不需要太多的靈活性。 (儘管你想變得健壯,但代碼可以處理對DTO的更改,就像新字段一樣,不會拋出異常)

基本上,您希望從DTO的片段構建客戶。你有什麼格式,只有XML或其他什麼?

我想我會只是去接受的DTO,並返回一個客戶(例如對於XML :)

class Customer { 
     public Customer(XmlNode sourceNode) { 
      // logic goes here 
     } 
    } 

Customer類可以「環繞」的DTO和「成爲一個實例的構造一'。這允許您非常自然地將您的DTO實例投影到客戶實例中:

var c = new Customer(xCustomerNode) 

這將處理高級別模式選擇。你到目前爲止同意嗎? 下面是你試圖通過屬性'ref'.提到的特定問題的刺傷。我確實看到DRY和KISS如何在那裏發生衝突,但是我會盡量不要過時。一個非常簡單的解決方案可以解決這個問題

所以對於的PostalAddress,那就有它自己的構造太,就像客戶本身:在客戶

public PostalAddress(XmlNode sourceNode){ 
    // here it reads the content into a PostalAddress 
} 

var adr = new PostalAddress(xAddressNode); 

我就是看到這裏,你在哪裏的問題如果發票地址或HomeAddress發生變化,你把代碼弄清楚了嗎?這不屬於PostalAddress的構造函數,因爲稍後可能會有其他用途用於PostalAddress,您不希望將其硬編碼到PostalAddress類中。

因此,該任務應該在客戶類中處理。這是他使用PostalAddress的地方。它需要能夠從返回的地址告訴它是什麼類型的地址。我想最簡單的方法是隻補充一點,告訴我們的PostalAddress屬性:

public class PostalAddress{ 
    public string AdressUsage{get;set;} // this gets set in the constructor 

} 

,並在DTO就指定:

<PostalAddress usage="HomeAddress" city="Amsterdam" street="Dam"/> 

然後你可以看看它在客戶類而在財產權利「堅持到底」:

var adr = new PostalAddress(xAddressNode); 
switch(adr.AddressUsage){ 
case "HomeAddress": this.HomeAddress = adr; break; 
case "PostalAddress": this.PostalAddress = adr; break; 
default: throw new Exception("Unknown address usage"); 
} 

一個簡單的屬性,它告訴顧客它是什麼類型的地址就足夠我猜。

它聽起來如何?下面的代碼將它們放在一起。

class Customer { 

     public Customer(XmlNode sourceNode) { 

      // loop over attributes to get the simple stuff out 
      foreach (XmlAttribute att in sourceNode.Attributes) { 
       // assign simpel stuff 
      } 

      // loop over child nodes and extract info 
      foreach (XmlNode childNode in sourceNode.ChildNodes) { 
       switch (childNode.Name) { 
        case "PostalAddress": // here we find an address, so handle that 
         var adr = new PostalAddress(childNode); 
         switch (adr.AddressUsage) { // now find out what address we just got and assign appropriately 
          case "HomeAddress": this.HomeAddress = adr; break; 
          case "InvoiceAddress": this.InvoiceAddress = adr; break; 
          default: throw new Exception("Unknown address usage"); 
         }  
         break; 
        // other stuff like phone numbers can be handeled the same way 
        default: break; 
       } 
      } 
     } 

     PostalAddress HomeAddress { get; private set; } 
     PostalAddress InvoiceAddress { get; private set; } 
     Name Name { get; private set; } 
    } 

    class PostalAddress { 
     public PostalAddress(XmlNode sourceNode) { 
      foreach (XmlAttribute att in sourceNode.Attributes) { 
       switch (att.Name) { 
        case "AddressUsage": this.AddressUsage = att.Value; break; 
        // other properties go here... 
      } 
     } 
    } 
     public string AddressUsage { get; set; } 

    } 

    class Name { 
     public string First { get; set; } 
     public string Middle { get; set; } 
     public string Last { get; set; } 
    } 

和一小段XML。你還沒有說過你的DTO格式,也可以用於其他格式。

<Customer> 
    <PostalAddress addressUsage="HomeAddress" city="Heresville" street="Janestreet" number="5"/> 
    <PostalAddress addressUsage="InvoiceAddress" city="Theresville" street="Hankstreet" number="10"/> 
</Customer> 

問候,

格特 - 揚

+0

嗨,感謝您的回答。從我的問題中可以看出,我的DTO是一個POCO類而不是XML,但XML除了您的答案與Florian的答案基本相同。我想盡量避免在接受DTO的領域模型中有一個額外的構造函數,因爲我寧願他們不知道彼此。我認爲應該有數據和模型之間的一個調解器,它可以在兩個方向上進行轉換... – MattDavey 2011-06-06 10:53:37

+1

嗨,然後在類本身上構造一個構造函數,你可以使用靜態方法在一個單獨的映射類中返回一個Customer 。否則,邏輯將保持幾乎相同,儘管您可能會遇到一些封裝問題,因爲您不再是該類本身。你可以通過使它們保護而不是私有來解決這個問題,並從Customer中派生映射類。這就是我對訪問者模式的抱怨:要真正實現這個模式,你經常需要拆除你的類的封裝或者以其他方式解決它。 – gjvdkamp 2011-06-06 11:10:36

+0

是的,我認爲這是要走的路,一個工廠的實施,吃客戶DTO和吐出一個完全形成的客戶域模型。封裝不會太多的問題,因爲他們會住在同一個大會和工廠需要訪問的任何東西可以在內部:) – MattDavey 2011-06-06 11:34:23

2

對於模型類和DTO之間做轉換,我喜歡的是做的四兩件事之一:

一個。使用隱式轉換運算符(特別是在處理json-to-dotnet轉換時)。

public class Car 
{ 
    public Color Color {get; set;} 
    public int NumberOfDoors {get; set;}   
} 

public class CarJson 
{ 
    public string color {get; set;} 
    public string numberOfDoors { get; set; } 

    public static implicit operator Car(CarJson json) 
    { 
     return new Car 
      { 
       Color = (Color) Enum.Parse(typeof(Color), json.color), 
       NumberOfDoors = Convert.ToInt32(json.numberOfDoors) 
      }; 
    } 
} 

,然後使用是

Car car = Json.Decode<CarJson>(inputString) 

或更簡單地

var carJson = new CarJson {color = "red", numberOfDoors = "2"}; 
    Car car = carJson; 

瞧,即時轉換:)

http://msdn.microsoft.com/en-us/library/z5z9kes2.aspx

灣使用linq投影來改變數據的形狀

IQueryable<Car> cars = CarRepository.GetCars(); 
cars.Select(car => 
    new 
    { 
     numberOfDoors = car.NumberOfDoors.ToString(), 
     color = car.Color.ToString() 
    }); 

c。 d。使用這兩種組合的一種

d。定義一個擴展方法(也可以用在linq投影中)

public static class ConversionExtensions 
{ 
    public static CarJson ToCarJson(this Car car) 
    { 
     return new CarJson {...}; 
    } 
} 

CarRepository.GetCars().Select(car => car.ToCarJson()); 
+0

這兩個好建議:)隱式轉換運算符很好,但確實需要DTO對領域模型有深入的瞭解,對我來說這是一個不容否認的問題。 linq投影的想法實際上是一個非常好的主意,我將在處理DTO的集合時使用它,儘管linq表達式將簡單地推遲到CustomerFactory進行轉換... – MattDavey 2011-06-09 09:50:31

+0

我聽到你的聲音。就個人而言,由於DTO與模型密切相關,因此我可以多一點耦合。另外,添加多種雙向轉換的功能非常好。 p.s.,我在w/linq投影中不時使用了另一個選項。 G'luck! – Jason 2011-06-09 14:52:35