Построение графа объектов из плоского DTO с использованием шаблона посетителя - PullRequest
12 голосов
/ 02 июня 2011

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

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

То, что я хочу сделать, - это позволить посетителю назначать объекты для графа при его посещении каждого элемента, с целью использования шаблона Special Case для объектов, в которых данные отсутствуют. 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 ]

7 голосов
/ 06 июня 2011

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

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

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

5 голосов
/ 06 июня 2011

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

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

По сути, вы хотите создать Customer из фрагмента 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, которое говорит нам:

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 голосов
/ 09 июня 2011

Для преобразования между классом модели и DTO я предпочитаю сделать одну из четырех вещей:

a.используйте оператор неявного преобразования (особенно при обработке переходов 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

b.Используйте проекцию 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());
0 голосов
/ 06 июня 2011

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

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

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

...