【问题标题】:Constructing an object graph from a flat DTO using visitor pattern使用访问者模式从平面 DTO 构造对象图
【发布时间】:2026-01-25 06:10:02
【问题描述】:

我为自己编写了一个漂亮的简单小领域模型,其对象图如下所示:

-- 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 参数:(

所以我只剩下访问独立元素并在完成后重建图形:

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

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

customer.HomePhoneNumber = homePhone;
customer.EmailAddress = email;
// ...

在这一点上,我意识到我离访问者模式很远,离工厂更近了,我开始怀疑我是否从一开始就错误地处理了这个问题..

还有其他人遇到过这样的问题吗?你是怎么克服的?有没有非常适合这种场景的设计模式?

很抱歉发布了这样一个冗长的问题,并且阅读到这里做得很好:)

编辑为了回应 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...
}

然后我写了一个 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...
    }
}

【问题讨论】:

  • Automapper 之类的东西会为您工作吗? automapper.codeplex.com
  • @bentayloruk 感谢您的回复,我已经评估了 automapper(和 valueinjecter),但不幸的是,这两个都不合适,因为我的域对象实现了 ISupportInitialize,我需要在注入任何属性之前 BeginInit() DTO,然后是 EndInit() - 我认为 Automapper 不可能做到这一点:(
  • 如果你使用的是 .net 4,那么我建议 Lazy 类会帮助你一点:)
  • @MattDavey imho CustomerDTO 数据应该在 CreateCustomer 方法中接收,而不是在工厂构造函数中。
  • @MattDavey 我个人偏向于 DI,因此我通常避免使用静态并坚持使用 DI 传递给构造函数的常规实例(在本例中为 ModelMediator)。这就是说,它本质上是静态的(没有要维护的状态),将其更改为意味着更少的实例。只有在特定情况下才真正有所不同。这是一个完全不同的问题:)

标签: c# factory-pattern dto visitor-pattern domain-model


【解决方案1】:

我认为你在这里真的过于复杂了。只需使用工厂方法,让您的域对象清楚地说明它们依赖于哪些其他域对象。

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(...));
    }
}

如果您需要从 Customer 获取 CustomerDTO 的依赖项,则将 DTO 作为附加参数传递给构造函数,可能包含在附加抽象中。

通过这种方式,事情将保持干净、可测试且易于理解。

【讨论】:

  • 感谢您的回答,您说得对,我让事情变得太复杂了。我不希望域模型类对 DTO 有任何了解,因此必须有一些可以在它们之间映射的中介。我认为你提到的工厂类是处理的方法:)
  • 我已经找到了解决方案,并将我的答案放在原始问题中。因为你自己和 gjvdkamp 都在同等程度上帮助了我,所以我将让赏金到期,届时它将自动转到得票最多的答案。我认为这是最公平的方式:)
【解决方案2】:

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

您在这里要做的是从 DTO 创建一个类的实例。由于类的结构和 DTO 密切相关(您在 DB 中进行映射,我假设您在该端处理所有映射问题并具有直接映射到客户结构的 DTO 格式),您知道设计时间你需要什么。不需要太大的灵活性。 (不过,您希望变得健壮,代码可以处理对 DTO 的更改,例如新字段,而不会引发异常)

基本上,您希望从 DTO 的 sn-p 构造一个 Customer。您有什么格式,只是 XML 或其他格式?

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

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

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

var c = new Customer(xCustomerNode)

这处理高级模式选择。到目前为止你同意吗? 这是您提到的试图通过 ref 传递属性的具体问题。我确实看到 DRY 和 KISS 在那里可能存在矛盾,但我会尽量不要过度思考。一个非常直接的解决方案可以解决这个问题。

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

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

关于客户:

var adr = new PostalAddress(xAddressNode);

我在这里看到的问题是,您将确定是 InvoiceAddress 还是 HomeAddress 的代码放在哪里?这不属于 PostalAddress 的构造函数,因为稍后 PostalAddress 可能还有其他用途,您不想在 PostalAddress 类中对其进行硬编码。

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

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

}

并在 DTO 中指定它:

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

然后您可以在 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");
}

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

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

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 的 sn-p。你还没有说你的 DTO 格式,也适用于其他格式。

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

问候,

格特-简

【讨论】:

  • 您好,感谢您的回答。从我的问题中可以看出,我的 DTO 是 POCO 类而不是 XML,但是除了 XML 之外,您的回答与 Florian 的回答基本相同。我想尝试避免在接受 DTO 的域模型中使用额外的构造函数,因为我希望它们彼此不了解。我认为数据和模型之间应该有一些中介可以双向转换......
  • 嗨,然后在类本身上插入一个构造函数,您可以拥有在单独的映射类中返回客户的静态方法。否则,逻辑将保持几乎相同,尽管您可能会遇到一些封装问题,因为您不再属于类本身。您可以通过使它们受保护而不是私有并从客户派生映射类来解决这个问题。这就是我对访问者模式的不满:要真正让它工作,你通常必须拆除类的封装或以其他方式解决它。
  • 是的,我认为这是要走的路,工厂实现会吃掉客户 DTO 并吐出完全形成的客户域模型。封装不会成为太大的问题,因为它们将存在于同一个组件中,并且工厂需要访问的任何东西都可以是内部的:)
  • 我已经找到了一个解决方案,并将我的答案放在原始问题中。因为你和弗洛里安都在同等程度上帮助了我,所以我将让赏金到期,届时它将自动转到得票最多的答案。我认为这是最公平的方式:)
【解决方案3】:

为了在模型类和 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

b.使用linq投影改变数据的形状

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

c。使用两者的某种组合

d。定义一个扩展方法(也可以在 linq 投影中使用)

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

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

【讨论】:

  • 两个很好的建议 :) 隐式转换运算符很好,但确实需要 DTO 对域模型有深入的了解,这对我来说是禁忌。 linq 投影的想法实际上是一个非常好的想法,我将在处理 DTO 的集合时使用它,尽管 linq 表达式将简单地遵循 CustomerFactory 来进行转换......
  • 我听到了。就个人而言,我可以多耦合一点,因为 DTO 与模型密切相关;此外,添加多个双向转换的能力也很好。 p.s.,我在上面添加了另一个选项,我不时使用 linq 投影。祝你好运!
【解决方案4】:

您可以采用我在这里描述的方法:convert a flat database resultset into hierarchical object collection in C#

背后的想法是读取一个对象,例如 Customer 并将其放入字典中。在读取数据时,例如CustomerAccount,您现在可以从字典中获取客户并将客户帐户添加到客户。

您只需对所有数据进行一次迭代即可构建对象图。

【讨论】:

  • 这与我的场景有点不同 - 我有一组有限的离散值表示层次图中的每个数据点,而不是需要添加到集合中的一系列相似数据点.不过还是谢谢!