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

发布于 2024-11-12 23:31:33 字数 3749 浏览 3 评论 0原文

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

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

此结构完全与我必须使用的遗留数据库不一致,所以我定义了一个平面 DTO,其中包含客户图中每个元素的数据 - 我在数据库中有视图和存储过程,这允许我使用这个平面结构在两个方向上与数据进行交互,这一切都工作得很好。 dandy :)

将域模型扁平化到 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...
}

这就是理论,当它像这样简单地布局时,它似乎是一个不错的主意:)

但是要使整个对象图起作用需要是在访问者访问之前建造,否则我会得到 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...
    }
}

I've written myself a nice simple little domain model, with an object graph that looks like this:

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

This structure is completely at odds with the legacy database I'm having to work with, so I've defined a flat DTO which contains the data for each element in the customer graph - I have views and stored procedures in the database which allow me to interact with the data using this flat structure in both directions, this all works fine & dandy :)

Flattening the domain model into a DTO for insert/update is straightfoward, but what I'm having trouble with is taking a DTO and creating the domain model from it... my first thought was to implement a visitor which would visit each element in the customer graph, and inject values from the DTO as necessary, something a bit like this:

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...
}

That's the theory and it seems like a sound idea when it's laid out simply like that :)

But for this to work the entire object graph would need to be constructed before the visitor erm, visited, otherwise I'd get NRE's left right and centre.

What I want to be able to do is let the visitor assign objects to the graph as it visits each element, with the goal being to utilize the Special Case pattern for objects where data is missing in the DTO, eg.

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;
    }
}

Which I honestly thought would work, but the C# throws me an error on:

myVisitor.VisitHomePhone(out customer.HomePhoneNumber);

Since you can't pass ref/out parameters in this way :(

So I'm left with visiting independent elements and reconstructing the graph when its done:

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

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

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

At this point I'm aware that I'm quite far away from the Visitor Pattern and am much closer to a Factory, and I'm starting to wonder whether I approached this thing wrong from the start..

Has anyone else run into a problem like this? How did you overcome it? Are there any design patterns which are well suited to this scenario?

Sorry for posting such a looong question, and well done for reading this far :)

EDIT In response to the helpful answers from Florian Greinacher and gjvdkamp, I settled on a relatively simple factory implementation that looks like this:

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...
}

I then wrote a ModelMediator class to handle interaction between the data layer and the domain model...

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...
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

最好是你 2024-11-19 23:31:33

我认为你真的把事情搞得太复杂了。只需使用工厂方法,让您的域对象清楚地说明它们依赖于哪些其他域对象。

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 作为附加参数传递给构造函数,可能会包装在附加抽象中。

这样事情就会保持干净、可测试且易于理解。

I think you are really over-complicating things here. Just use a factory method and let your domain objects clearly state on which other domain objects they depend.

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

If you need to take a dependency from Customer to CustomerDTO pass the DTO as additional argument to the constructor, probably wrapped in an additional abstraction.

This way things will keep clean, testable and easy to understand.

凉世弥音 2024-11-19 23:31:33

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

您在这里要做的是从 DTO 创建类的实例。由于类的结构和 DTO 紧密相连(您在数据库中进行映射,我假设您处理该侧的所有映射问题并具有直接映射到客户结构的 DTO 格式),您知道设计您需要的时间。不需要太多的灵活性。 (不过,您希望代码能够健壮,以便代码可以处理 DTO 的更改,例如新字段,而不引发异常)

基本上,您希望从 DTO 的片段构造一个 Customer。您采用什么格式,只是 XML 还是其他格式?

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

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

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

var c = new Customer(xCustomerNode)

这可以处理高级模式选择。到目前为止你同意吗?
这是对您提到的尝试“通过引用”传递属性的具体问题的刺探。我确实看到 DRY 和 KISS 在那里可能会发生冲突,但我会尽量不要想得太多。一个非常简单的解决方案可以解决这个问题。

因此,对于 PostalAddress,它也将有自己的构造函数,就像客户本身一样:

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 片段。您还没有提到您的 DTO 格式,它也适用于其他格式。

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

问候,

格特-扬

I don't think i would go with a visitor. That would be appropriate if you don't know at design time, what operations you need to perform on it later, so you open up the class to allow for others to write visitors that implement that logic. Or there are so many things that you need to do on it that you don't want to clutter your class with this.

What you want to do here is create an instance of a class from a DTO. Since the structure of the class and the DTO are closely linked (you do your mapping in the DB, I assume you handle all mapping issues on that side and have a DTO format that maps directly to the structure of your customer), you know at design time what you need to. There's no need for much flexibility. (You want to be robust though, that the code can handle changes to the DTO, like new fields, without throwing exceptions)

Basically you want to construct a Customer from a snippet of a DTO. What format do you have, just XML or something else?

I think I would just go for a constructor that accepts the DTO and returns a Customer (example for XML:)

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

The Customer class can 'wrap around' an instance of the DTO and 'become one'. This allows you to very naturally project an instance of your DTO into a customer instance:

var c = new Customer(xCustomerNode)

This handles the high level pattern choice. Do you agree so far?
Here's a stab at the specific issue you mention with trying to pass properties 'by ref'.I do see how DRY and KISS can be at odds there, but I would try not to overthink it. A pretty straight forward solution could fix that.

So for the PostalAddress, it would have it's own constructor too, just like the Customer itself:

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

on the customer:

var adr = new PostalAddress(xAddressNode);

The problem I see here is, where do you put the code that figures out if this if the InvoiceAddress or the HomeAddress? This does not belong in the constructor of the PostalAddress, because there could be other uses for the PostalAddress later, you don't want to hardcode it in the PostalAddress class.

So that task should be handled in the Customer class. This is where he usage of the PostalAddress is determined. It needs to be able to tell from the returned Address what type of address it is. I guess the simplest approach would be to just add a property on PostalAddress that tells us:

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

}

and in the DTO just specify it:

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

Then you can look at it in the Customer class and 'stick it' in the right property:

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");
}

A simple attribute that tells the Customer what type of address it is would be enough I guess.

How does it sound so far? Code below puts it all together.

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; }
    }

and a snippet of XML. You haven't said anything about your DTO format, would work for other formats too.

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

Regards,

Gert-Jan

如此安好 2024-11-19 23:31:33

为了在模型类和 DTO 之间进行转换,我倾向于执行以下四件事之一

:使用隐式转换运算符(尤其是在处理 json 到 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.定义一个扩展方法(也可以在 linq 投影中使用)

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

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

For doing conversions between a model class and a DTO, my preference is to do one of four things:

a. use an implicit conversion operator (especially when dealing json-to-dotnet transitions).

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

and then usage is

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

or more simply

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

voila, instant conversion :)

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

b. Use linq projection to change the shape of the data

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

c. Use some combination of the two

d. Define an extension method (that could also be used in the linq projection)

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

CarRepository.GetCars().Select(car => car.ToCarJson());
无需解释 2024-11-19 23:31:33

您可以采用我在这里描述的方法:将平面数据库结果集转换为 C# 中的分层对象集合

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

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

You could take the approch I described here: convert a flat database resultset into hierarchical object collection in C#

The idea behind is to read an object, like Customer and put it into a Dictionary. When reading the data for e.g. CustomerAccount, you can now take the Customer from the Dictionary and add the Customer Account to the customer.

You'll have only one iteration over all data to build your object graph.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文