2011-06-02 83 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