DDD — Composite Aggregate Serialization — проблема проектирования

Я пытаюсь применить DDD к одному проекту Java. Это проблема, на которую я наткнулся:

В домене у меня есть Aggregate, который реализован с использованием шаблона Composite OOP. Методы этого агрегата создают некоторые объекты предметной области, которые необходимо сериализовать и отправить по сети. Вот варианты, о которых я думал:

  1. В части Application Service моего домена я беру агрегат, вызываю его методы и пытаюсь сериализовать результаты в DTO. Чтобы сериализовать его в DTO, я должен использовать instanceof, чтобы проверить, является ли текущий узел составным или дочерним, и продолжить сериализацию. Поскольку instanceof - это запах кода (так как я читал, что он нарушает принцип Open/Close и т. д.), я решил попробовать использовать шаблон Visitor.

  2. Чтобы применить шаблон посетителя, мой Composite Aggregate должен реализовать посетителя, который будет возвращать DTO, а затем DTO станет частью уровня домена, что также не является хорошим дизайном (поскольку домен должен содержать только концепции домена, а DTO не являются частью этого) . Сериализация DTO — это только техническая деталь, которая не должна переходить на уровень домена.

Есть ли какое-либо другое решение, не противоречащее этим принципам проектирования?

Есть ли способ имитировать динамическую привязку в java для перегруженных методов (кроме instanceof - поскольку это решило бы мою проблему с вариантом 1)?


person Bojan Vukasovic    schedule 20.09.2019    source источник


Ответы (3)


Если посетитель имеет общий тип возвращаемого значения, то посещенные классы не связаны с этим типом.

public interface Node {
    <T> T accept(NodeVisitor<T> visitor);
}

public class ANode implements Node {
    @Override
    public <T> T accept(NodeVisitor<T> visitor) {
        return visitor.visit(this);
    }
}

public class BNode implements Node {
    @Override
    public <T> T accept(NodeVisitor<T> visitor) {
        return visitor.visit(this);
    }
}

public interface NodeVisitor<T> {
    T visit(ANode aNode);
    T visit(BNode bNode);
}

public class DtoNodeVisitor implements NodeVisitor<DTO> {
    @Override
    public DTO visit(ANode aNode) {
        return new DTO(); //use ANode to build this.
    }
    @Override
    public DTO visit(BNode bNode) {
        return new DTO(); //use BNode to build.
    }
}

ANode и BNode ничего не знают о DTO здесь.

person jaco0646    schedule 20.09.2019
comment
Это решение выглядит нормально - для общедоступного совокупного состояния. Единственная проблема, которую я здесь вижу, заключается в том, что если мне нужно сериализовать внутреннее состояние агрегата (состояние, которое не должно быть доступно извне), мне нужно будет реализовать некий шаблон Visitor+Memento, а затем домен должен знать о DTO (поскольку в этом случае DtoNodeVisitor будет частью домена), но я думаю, что это неизбежно. - person Bojan Vukasovic; 21.09.2019
comment
Я согласен с оценкой Memento; но это не должно тянуть DtoNodeVisitor в домен. Домен может добавлять новые (доступные только для чтения) абстракции вокруг конкретных состояний узлов, и DtoNodeVisitor может зависеть от абстракций, а не от самих конкретных узлов. - person jaco0646; 24.09.2019
comment
@ jaco0646 jaco0646, что происходит, когда вы хотите сериализовать ANode в ADTO и BNode в BDTO - тогда это решение не работает. И я думаю, когда у вас есть один DTO, вам вообще не нужны дженерики... - person Marko Kraljevic; 13.10.2020
comment
@MarkoKraljevic, логика, зависящая от конкретного типа, должна быть реализована в соответствующем методе visit. - person jaco0646; 14.10.2020

Во-первых, в пункте 2 я не понимаю:

мой Composite Aggregate должен реализовать Visitor

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

Есть ли способ имитировать динамическую привязку в java для перегруженных методов (кроме instanceof, поскольку это решило бы мою проблему с вариантом 1)?

Да, вы можете сделать это с помощью Reflections, но реальный вопрос в том, хотите ли вы их использовать?

Я думаю, что ответ зависит от того, сколько дел вам предстоит вести и как часто они меняются?

Если у вас есть «управляемое» количество различных случаев, решение с instanceof может быть хорошей сделкой:

public Something myMethod(Entity entity){
    if (entity instanceof AnEntity){
         //do stuffs
    else if (entity instanceof AnotherEntity){
         //do something else
    ...
    else {
         throw new RuntimeException("Not managed " + entity.getClass().getName());
    }
}

В противном случае, когда, возможно, у вас будет больше случаев и вы захотите разделить код на свои собственные методы, вы можете сделать это с помощью Java MethodHandle, позвольте мне опубликовать пример использования:

package virtualmethods;

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class MyObject {

    public String doStuffs(Object i) throws Throwable {
        try {
            final MethodType type = MethodType.methodType(String.class, i.getClass());
            return (String) MethodHandles.lookup()
                .findVirtual(getClass(), "doStuffs", type)
                .invoke(this, i);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Not managed " + i.getClass().getName(), e);
        }
    }

    private String doStuffs(Integer i) {
        return "You choose " + i;
    }
    private String doStuffs(Boolean b) {
        return "You choose boolean " + b;
    }
}

а затем используйте его:

package virtualmethods;

public class Main {

    public static void main(String[] args) throws Throwable {

        MyObject object = new MyObject();

        System.out.println("Integer => " + object.doStuffs(5));
        System.out.println("Boolean => " + object.doStuffs(true));
        try {
            System.out.println("String => " + object.doStuffs("something"));
        }
        catch (Throwable e) {
            System.out.println("KABOOM");
            e.printStackTrace();
        }
    }

}

Метод public в MyObject, принимающий Object, будет искать метод с именем doStuffs с результатом String и i.getClass() в качестве входных данных в классе MyObject (дополнительная информация здесь).
Используя этот способ, вы можете отправлять методы во время выполнения (использование перегрузки означает статическое связывание во время компиляции). Но у обоих методов есть проблема, заключающаяся в том, что вы не можете быть уверены, что будете управлять ВСЕМИ типами, которые расширяют/реализуют Entity в первом случае и/или Object во втором, оба решения имеют else или catch для проверки, когда нет управляемый тип передается методу.
Будьте на 100% уверены, что управление всеми типами может быть достигнуто только с помощью решения, предложенного @jaco0646, насколько я знаю, оно заставляет вас управлять всеми типами, иначе он выиграл не компилируется. Учитывая количество требуемого шаблона, я бы использовал его только тогда, когда выброс RuntimeException вызовет проблемы в бизнесе, и я не могу гарантировать, что он не будет выброшен с помощью надлежащего тестирования (кроме того, я нашел это очень увлекательным).

person rascio    schedule 20.09.2019
comment
Что касается реализации посетителя, я имел в виду, что домен должен знать о DTO, поскольку для сериализации внутреннего состояния агрегата, которое не должно быть раскрыто снаружи, методы сериализации должны быть внутри домена (например, этот класс DtoNodeVisitor ниже). Он должен иметь доступ к aNode.someProtectedMethodToSerializeToDTO(). - person Bojan Vukasovic; 21.09.2019
comment
Проблема раскрытия внутренних агрегатов заключается в том, что кто-то может изменить их без проверки инвариантов. Если вы глубоко скопируете внутреннее состояние агрегата и передадите его кому-то другому, это не вызовет никаких проблем с вашим агрегатом. Если состояние неизменно, нет проблем, чтобы кто-то другой мог его прочитать. Преобразование внутреннего состояния в неизменяемый объект или его глубокое копирование при чтении может позволить вам отделить слои. - person rascio; 21.09.2019

Похоже, вы слишком усложняете это. Если вам нужен typeof, то ваш агрегат не возвращает допустимый объект домена. Объект домена, который он возвращает, слишком общий. Чтобы противостоять этому, вы можете разделить свой совокупный метод на два метода; один возвращает Child, а другой — Composite. Затем ваша прикладная служба решает, какой из них вызывать (если это возможно).

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

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

person Louis    schedule 21.09.2019
comment
Насколько я знаю, есть дизайн для единообразия и дизайн для безопасности типов (en.wikipedia.org/wiki/Composite_pattern). Вы говорите о втором, а моя реализация - о первом. Единственный раз, когда мне нужно различать Composite и Child, это когда я выполняю сериализацию/десериализацию, но я думаю, что так и должно быть. - person Bojan Vukasovic; 21.09.2019
comment
Тег Domain Driven Design сбивает меня с толку. В некоторых случаях DDD не требуется, и это может быть одним из них. Когда вы делаете DDD, вы проектируете для SME, а не конкретно для обеспечения безопасности типов (хотя в конечном итоге это может быть результатом). Сказав это, то, что я сделал в прошлом, это то, что родитель и листья реализуют интерфейс INode, и этот интерфейс имеет свойство типа, которое содержит то, что вы ищете. - person Louis; 21.09.2019