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

时间:2011-06-02 15:31:47

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

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

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

这就是理论,当它的布局简直就像是一个好主意:)

但要实现这一点,整个对象图需要在访问者访问之前构建,否则我会让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;
// ...

此时我知道我距离访客模式相当远,而且离工厂更近了,我开始怀疑从一开始我是否接触过这个错误的东西..

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

很抱歉发布了这样一个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...
}

然后我写了一个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 个答案:

答案 0 :(得分: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(...));
    }
}

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

这样事情将保持干净,可测试且易于理解。

答案 1 :(得分:5)

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

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

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

我想我会选择接受DTO并返回Customer的构造函数(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);

我在这里看到的问题是,如果这是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>

此致

格特 - 扬

答案 2 :(得分:2)

为了在模型类和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() 
    } );

℃。使用两者的某种组合

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

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

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

答案 3 :(得分:0)

你可以接受我在这里描述的approch:convert a flat database resultset into hierarchical object collection in C#

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

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