使用访客模式从平面DTO构建对象图

Mat*_*vey 12 c# visitor dto domain-model factory-pattern

我写了一个很好的简单的小域模型,其对象图如下所示:

-- Customer
    -- Name : Name
    -- Account : CustomerAccount
    -- HomeAddress : PostalAddress
    -- InvoiceAddress : PostalAddress
    -- HomePhoneNumber : TelephoneNumber
    -- WorkPhoneNumber : TelephoneNumber
    -- MobilePhoneNumber : TelephoneNumber
    -- EmailAddress : EmailAddress
Run Code Online (Sandbox Code Playgroud)

这个结构与我必须使用的遗留数据库完全不一致,所以我定义了一个平面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...
}
Run Code Online (Sandbox Code Playgroud)

这就是理论,当它像那样布局时,它似乎是一个合理的想法:)

但为了实现这一目标,需要在访问者访问之前构建整个对象图,否则我将得到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;
    }
}
Run Code Online (Sandbox Code Playgroud)

老实说我认为这会有用,但C#会给我一个错误:

myVisitor.VisitHomePhone(out customer.HomePhoneNumber);
Run Code Online (Sandbox Code Playgroud)

因为你不能以这种方式传递ref/out参数:(

所以我留下了访问独立元素并在完成时重建图形:

Customer customer;
TelephoneNumber homePhone;
EmailAddress email;
// ...

myVisitor.VisitCustomer(out customer);
myVisitor.VisitHomePhone(out homePhone);
myVisitor.VisitEmail(out email);
// ...

customer.HomePhoneNumber = homePhone;
customer.EmailAddress = email;
// ...
Run Code Online (Sandbox Code Playgroud)

在这一点上,我知道我离访客模式相当远,离工厂更近了,我开始怀疑从一开始我是否接近这个问题.

有没有其他人遇到这样的问题?你是怎么克服的?有没有适合这种情况的设计模式?

很抱歉发布这样一个looong问题,并为阅读这个很好:)

编辑响应Florian Greinacher和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...
}
Run Code Online (Sandbox Code Playgroud)

然后我写了一个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...
    }
}
Run Code Online (Sandbox Code Playgroud)

Flo*_*her 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(...));
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您需要从Customer到CustomerDTO的依赖关系将DTO作为构造函数的附加参数传递,可能包含在另外的抽象中.

这样事情就会保持清洁,可测试和易于理解.

  • 谢谢你的回答,你是对的我让事情变得太复杂了.我不希望域模型类对DTO有任何了解,所以必须有一些可以在它们之间进行映射的中介.我认为你提到的工厂类是继续进行的方式:) (2认同)

gjv*_*amp 5

我不认为我会和访客一起去.如果您在设计时不知道以后需要执行哪些操作,那么这是合适的,因此您打开该类以允许其他人编写实现该逻辑的访问者.或者你需要做很多事情,你不想让你的课堂混乱.

你想要做的是从DTO创建一个类的实例.由于类和DTO的结构紧密相连(您在数据库中进行映射,我假设您处理了该方面的所有映射问题并且具有直接映射到客户结构的DTO格式),您知道设计时间你需要什么.不需要太大的灵活性.(你想要健壮,代码可以处理对DTO的更改,比如新字段,而不会抛出异常)

基本上,您希望从DTO的片段构建客户.你有什么格式,只有XML或其他什么?

我想我会选择接受DTO并返回Customer的构造函数(XML示例:)

class Customer {
        public Customer(XmlNode sourceNode) {
            // logic goes here
        }
    }
Run Code Online (Sandbox Code Playgroud)

Customer类可以"环绕"DTO的实例并"成为一个".这使您可以非常自然地将DTO实例投影到客户实例中:

var c = new Customer(xCustomerNode)
Run Code Online (Sandbox Code Playgroud)

这处理高级模式选择.你到目前为止同意吗?这里是你试图通过ref'传递属性时提到的具体问题的一个例子.我确实看到DRY和KISS如何在那里发生争执,但我会尽量不去思考它.一个非常直接的解决方案可以解决这个问

所以对于PostalAddress,它也有它自己的构造函数,就像Customer本身一样:

public PostalAddress(XmlNode sourceNode){
   // here it reads the content into a PostalAddress
}
Run Code Online (Sandbox Code Playgroud)

对客户:

var adr = new PostalAddress(xAddressNode);
Run Code Online (Sandbox Code Playgroud)

我在这里看到的问题是,如果是InvoiceAddress或HomeAddress,你在哪里写出代码?这不属于PostalAddress的构造函数,因为以后可能有其他用途的PostalAddress,您不希望在PostalAddress类中对其进行硬编码.

所以该任务应该在Customer类中处理.这是他确定PostalAddress使用的地方.它需要能够从返回的地址告诉它是什么类型的地址.我想最简单的方法是在PostalAddress上添加一个告诉我们的属性:

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

}
Run Code Online (Sandbox Code Playgroud)

并在DTO中指定它:

<PostalAddress usage="HomeAddress" city="Amsterdam" street="Dam"/>
Run Code Online (Sandbox Code Playgroud)

然后,您可以在Customer类中查看它并将其"粘贴"在正确的属性中:

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");
}
Run Code Online (Sandbox Code Playgroud)

一个简单的属性告诉客户它是什么类型的地址就足够了.

到目前为止听起来怎么样?下面的代码将它们放在一起.

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; }
    }
Run Code Online (Sandbox Code Playgroud)

和一小段XML.你没有说过你的DTO格式,也适用于其他格式.

<Customer>  
  <PostalAddress addressUsage="HomeAddress" city="Heresville" street="Janestreet" number="5"/>
  <PostalAddress addressUsage="InvoiceAddress" city="Theresville" street="Hankstreet" number="10"/>
</Customer>
Run Code Online (Sandbox Code Playgroud)

问候,

格特 - 扬