12

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

-- 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;
// ...

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

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

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

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

4 回答 4

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

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

于 2011-06-06T10:01:08.563 回答
5

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

您在这里要做的是从 DTO 创建一个类的实例。由于类的结构和 DTO 密切相关(您在 DB 中进行映射,我假设您在该端处理所有映射问题并具有直接映射到客户结构的 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,它也有自己的构造函数,就像 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。你还没有说你的 DTO 格式,也适用于其他格式。

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

问候,

格特-扬

于 2011-06-06T09:01:08.240 回答
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。定义扩展方法(也可以在 linq 投影中使用)

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

CarRepository.GetCars().Select(car => car.ToCarJson());
于 2011-06-09T00:55:41.767 回答
0

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.

于 2011-06-06T08:59:49.333 回答