Построение графа объекта из плоского DTO с использованием шаблона посетителя

Я написал себе симпатичную простую небольшую модель предметной области с графом объектов, который выглядит так:

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

На данный момент я понимаю, что я довольно далеко от паттерна посетителей и гораздо ближе к фабрике, и я начинаю задаваться вопросом, не правильно ли я подошел к этому вопросу с самого начала ...

Кто-нибудь еще сталкивался с такой проблемой? Как вы это преодолели? Существуют ли какие-либо шаблоны проектирования, которые хорошо подходят для этого сценария?

Приносим извинения за такой длинный вопрос, и молодцы, что дочитали до этого места :)

ИЗМЕНИТЬ В ответ на полезные ответы Флориана Грайнахера и 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...
    }
}

person MattDavey    schedule 02.06.2011    source источник
comment
Будет ли у вас работать что-то вроде Automapper? automapper.codeplex.com   -  person bentayloruk    schedule 02.06.2011
comment
@bentayloruk, спасибо за ответ, я оценил automapper (и valueinjecter), но, к сожалению, ни один из них не подходит, потому что мои объекты домена реализуют ISupportInitialize, и мне нужно BeginInit (), прежде чем вводить какие-либо свойства из DTO, и EndInit () впоследствии - Не думаю, что это возможно с Automapper :(   -  person MattDavey    schedule 03.06.2011
comment
Если вы используете .net 4, я бы посоветовал вам немного помочь с классом Lazy ‹T› :)   -  person Nathan    schedule 06.06.2011
comment
@MattDavey imho Данные CustomerDTO следует получать в методе CreateCustomer, а не в конструкторе фабрики.   -  person eglasius    schedule 06.06.2011
comment
@eglasius Я думал об этом. В этом случае всю фабрику можно было бы сделать статической ... есть ли в этом какое-то конкретное преимущество?   -  person MattDavey    schedule 07.06.2011
comment
@MattDavey лично я склонен к DI, поэтому я обычно избегаю статики и придерживаюсь обычных экземпляров, передаваемых DI конструктору (в данном случае ModelMediator). При этом он по своей природе статичен (нет состояния для поддержки), изменение его на это означает меньшее количество экземпляров. Только в определенных сценариях это действительно разница. Это совсем другой вопрос :)   -  person eglasius    schedule 07.06.2011


Ответы (4)


Я думаю, вы здесь действительно слишком усложняете. Просто используйте фабричный метод и позвольте вашим объектам домена четко указывать, от каких других объектов домена они зависят.

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 в качестве дополнительного аргумента конструктору, возможно, заключенного в дополнительную абстракцию.

Таким образом, все будет оставаться чистым, доступным для тестирования и легким для понимания.

person Florian Greinacher    schedule 06.06.2011
comment
спасибо за ответ, вы правы, я слишком усложняю ситуацию. Я не хочу, чтобы классы модели предметной области имели какие-либо знания о DTO, поэтому должен быть какой-то посредник, который может отображать между ними ... Я думаю, что фабричный класс, как вы упомянули, - это способ действовать :) - person MattDavey; 06.06.2011
comment
Я пришел к решению и поместил свой ответ в исходный вопрос. Поскольку вы и gjvdkamp помогли мне в равной степени, я собираюсь оставить награду до истечения срока, после чего она автоматически перейдет к ответу, набравшему наибольшее количество голосов. Думаю, это самый честный способ :) - person MattDavey; 06.06.2011

Не думаю, что пойду с посетителем. Это было бы уместно, если вы не знаете во время разработки, какие операции вам нужно выполнить над ним позже, поэтому вы открываете класс, чтобы другие могли писать посетителей, реализующих эту логику. Или есть так много вещей, которые вам нужно сделать на нем, что вы не хотите загромождать этим свой класс.

Здесь вы хотите создать экземпляр класса из DTO. Поскольку структура класса и DTO тесно связаны (вы выполняете сопоставление в базе данных, я предполагаю, что вы решаете все проблемы сопоставления на этой стороне и имеете формат DTO, который сопоставляется непосредственно со структурой вашего клиента), вы знаете на время разработки то, что вам нужно. Нет необходимости в большой гибкости. (Однако вы хотите быть надежным, чтобы код мог обрабатывать изменения в DTO, например, новые поля, без исключения исключений)

По сути, вы хотите создать клиента из фрагмента DTO. Какой у вас формат, просто XML или что-то еще?

Думаю, я бы просто выбрал конструктор, который принимает DTO и возвращает клиента (пример для XML :)

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

Класс Customer может «обернуть» экземпляр DTO и «стать одним». Это позволяет очень естественно проецировать экземпляр вашего DTO в экземпляр клиента:

var c = new Customer(xCustomerNode)

Это обрабатывает выбор шаблона высокого уровня. Вы с этим согласны? Вот удар по конкретной проблеме, о которой вы упомянули, с попыткой передать свойства 'по ссылке'. Я действительно вижу, как 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, которое сообщает нам:

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>

С уважением,

Герт-Ян

person gjvdkamp    schedule 06.06.2011
comment
Привет, спасибо за ответ. Мой DTO, как вы можете видеть из моего вопроса, - это класс POCO, а не XML, но помимо XML, ваш ответ в основном такой же, как ответ Флориана. Я хотел бы попытаться избежать использования дополнительного конструктора в модели предметной области, который принимает DTO, поскольку я бы предпочел, чтобы они ничего не знали друг о друге. Я думаю, что между данными и моделями должен быть какой-то посредник, который может конвертировать в обоих направлениях ... - person MattDavey; 06.06.2011
comment
Привет, тогда вставьте конструктор в сам класс, у вас может быть статический метод, который возвращает клиента в отдельном классе сопоставления. В противном случае логика останется почти такой же, хотя вы можете столкнуться с несколькими проблемами инкапсуляции, потому что вы больше не находитесь в самом классе. Вы можете решить эту проблему, сделав их защищенными вместо частных и унаследовав класс сопоставления от Customer. Это моя проблема с шаблоном посетителя: чтобы он действительно работал, вам часто приходится разрушать инкапсуляцию ваших классов или обходить ее каким-либо другим способом. - person gjvdkamp; 06.06.2011
comment
Да, я думаю, что это правильный путь, реализация фабрики, которая съедает клиентский DTO и выплевывает полностью сформированную модель предметной области клиента. Инкапсуляция не будет большой проблемой, поскольку они будут жить в одной сборке, и все, к чему фабрике нужен доступ, может быть внутренним :) - person MattDavey; 06.06.2011
comment
Я пришел к решению и поместил свой ответ в исходный вопрос. Поскольку вы и Флориан в одинаковой мере помогли мне, я собираюсь оставить срок действия награды, после чего она автоматически перейдет к ответу, набравшему наибольшее количество голосов. Думаю, это самый честный способ :) - person MattDavey; 06.06.2011

Для преобразования между классом модели и 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());
person Jason    schedule 09.06.2011
comment
Оба хороших предложения :) Оператор неявного приведения хорош, но требует от DTO глубокого знания модели предметной области, что для меня нет-нет. Идея проекции linq на самом деле является действительно хорошей идеей, которую я собираюсь использовать при работе с коллекциями DTO, хотя выражение linq просто перекладывает на CustomerFactory преобразование ... - person MattDavey; 09.06.2011
comment
Я слышу тебя. Лично я согласен с большей связью, поскольку DTO очень тесно связан с моделью; Кроме того, приятно иметь возможность добавлять несколько двусторонних преобразований. p.s., я добавил еще один вариант выше, который я время от времени использую вместе с проекцией linq. Удачи! - person Jason; 09.06.2011

Вы можете воспользоваться подходом, который я описал здесь: преобразовать набор результатов плоской базы данных в коллекцию иерархических объектов на C #

Идея состоит в том, чтобы прочитать объект, например Customer, и поместить его в словарь. При чтении данных, например, CustomerAccount, теперь вы можете взять клиента из Словаря и добавить учетную запись клиента к клиенту.

У вас будет только одна итерация по всем данным для построения графа объектов.

person BitKFu    schedule 06.06.2011
comment
Это немного отличается от моего сценария - у меня есть конечный набор дискретных значений, представляющих каждую точку данных в иерархическом графике, а не серию аналогичных точек данных, которые необходимо добавить в коллекцию. Но все равно спасибо! - person MattDavey; 06.06.2011