使用访问者模式从平面 DTO 构建对象图
我自己编写了一个很好的简单的小域模型,其对象图如下所示:
-- 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
我认为你真的把事情搞得太复杂了。只需使用工厂方法,让您的域对象清楚地说明它们依赖于哪些其他域对象。
如果您需要获取从 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.
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.
我想我不会和访客一起去。如果您在设计时不知道稍后需要对其执行哪些操作,那么这将是合适的,因此您打开该类以允许其他人编写实现该逻辑的访问者。或者你需要做的事情太多了,你不想让你的课堂变得混乱。
您在这里要做的是从 DTO 创建类的实例。由于类的结构和 DTO 紧密相连(您在数据库中进行映射,我假设您处理该侧的所有映射问题并具有直接映射到客户结构的 DTO 格式),您知道设计您需要的时间。不需要太多的灵活性。 (不过,您希望代码能够健壮,以便代码可以处理 DTO 的更改,例如新字段,而不引发异常)
基本上,您希望从 DTO 的片段构造一个 Customer。您采用什么格式,只是 XML 还是其他格式?
我想我会选择一个接受 DTO 并返回 Customer 的构造函数(例如 XML:)
Customer 类可以“包裹”DTO 的实例并“成为一个”。这使您可以非常自然地将 DTO 实例投影到客户实例中:
这可以处理高级模式选择。到目前为止你同意吗?
这是对您提到的尝试“通过引用”传递属性的具体问题的刺探。我确实看到 DRY 和 KISS 在那里可能会发生冲突,但我会尽量不要想得太多。一个非常简单的解决方案可以解决这个问题。
因此,对于 PostalAddress,它也将有自己的构造函数,就像客户本身一样:
对于客户:
我在这里看到的问题是,您将确定 InvoiceAddress 还是 HomeAddress 的代码放在哪里?这不属于 PostalAddress 的构造函数,因为稍后 PostalAddress 可能有其他用途,您不想在 PostalAddress 类中对其进行硬编码。
因此该任务应该在 Customer 类中处理。这是确定 PostalAddress 的使用的地方。它需要能够从返回的地址判断它是什么类型的地址。我想最简单的方法是在 PostalAddress 上添加一个属性来告诉我们:
并在 DTO 中指定它:
然后您可以在 Customer 类中查看它并将其“粘贴”在正确的属性中:
一个简单的属性我想告诉客户地址是什么类型就足够了。
到目前为止听起来怎么样?下面的代码将它们放在一起。
和 XML 片段。您还没有提到您的 DTO 格式,它也适用于其他格式。
问候,
格特-扬
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:)
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:
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:
on the customer:
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:
and in the DTO just specify it:
Then you can look at it in the Customer class and 'stick it' in the right property:
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.
and a snippet of XML. You haven't said anything about your DTO format, would work for other formats too.
Regards,
Gert-Jan
为了在模型类和 DTO 之间进行转换,我倾向于执行以下四件事之一
:使用隐式转换运算符(尤其是在处理 json 到 dotnet 转换时)。
然后用法就是
或更简单地
瞧,即时转换:)
http://msdn.
microsoft.com/en-us/library/z5z9kes2.aspx 使用 linq 投影来改变数据的形状
c.使用两者的某种组合
d.定义一个扩展方法(也可以在 linq 投影中使用)
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).
and then usage is
or more simply
voila, instant conversion :)
http://msdn.microsoft.com/en-us/library/z5z9kes2.aspx
b. Use linq projection to change the shape of the data
c. Use some combination of the two
d. Define an extension method (that could also be used in the linq projection)
您可以采用我在这里描述的方法:将平面数据库结果集转换为 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.