Подтвердить что ты не робот

Построение графа объектов из плоского 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...
}

Это теория, и она кажется хорошей идеей, когда она складывается просто так:)

Но для этого для работы весь граф объектов нужно будет построить до посетителя, посетив, иначе я бы получил 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;
    }
}

Я честно думал, что это сработает, но С# выдает мне ошибку:

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

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

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

Извините за публикацию такого looong вопроса и хорошо сделано для прочтения до сих пор:)

EDIT В ответ на полезные ответы от Florian Greinacher и gjvdkamp, ​​я остановился на относительно простой реализации factory, которая выглядит так:

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

Ответ 1

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

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(...));
    }
}

Если вам нужно принять зависимость от клиента к CustomerDTO, передайте DTO в качестве дополнительного аргумента конструктору, возможно, завернутый в дополнительную абстракцию.

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

Ответ 2

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

Что вы хотите сделать здесь, это создать экземпляр класса из 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)

Это относится к выбору шаблона высокого уровня. Вы до сих пор согласны? Вот удар по конкретной проблеме, которую вы упомянули при попытке передать свойства "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>

Привет,

Герт-Ян

Ответ 3

Для выполнения преобразований между классом модели и 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;

voila, мгновенное преобразование:)

http://msdn.microsoft.com/en-us/library/z5z9kes2.aspx

б. Используйте линковую проекцию для изменения формы данных.

IQueryable<Car> cars = CarRepository.GetCars();
cars.Select( car => 
    new 
    { 
        numberOfDoors = car.NumberOfDoors.ToString(), 
        color = car.Color.ToString() 
    } );

с. Используйте комбинацию из двух

д. Определите метод расширения (который также может использоваться в проекции linq)

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

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

Ответ 4

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

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

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