Уровень приложения Java 8 и конкретное выходное преобразование

У меня есть мультипроект gradle с двумя подпроектами, пытающимися эмулировать гексагональную архитектуру:

  1. остальное-адаптер
  2. прикладной уровень

Я не хочу, чтобы сервисы приложений выставляли доменные модели, и не хочу принудительно указывать конкретное представление в качестве вывода. Так что я хотел бы что-то вроде сервисов приложений потреблять 2 аргумента (команда и something) и вернуть T, Клиент настраивает сервис.

Остальной адаптер не имеет доступа к модели домена, поэтому я не могу вернуть модели домена и позволить адаптеру создать свое представление.

Что насчет something, Я старался:

  1. иметь подпись <T> List<T> myUseCase(Command c, Function<MyDomainModel, T> fn), Прикладной уровень является владельцем функций преобразования (поскольку подпись использует MyDomainModel) и предоставляет словарь функций. Таким образом, контроллер остальных ссылается на одну из Fn. Оно работает. И я ищу лучшего пути. Более элегантный способ, если он существует.
  2. иметь подпись <T> List<T> myUseCase(Command c, FnEnum fn) Для каждого перечисления я связал функцию. С этим я обнаружил, что подпись более изящна: потребитель обеспечивает, какую трансформацию он хочет из перечисления. Но не работает, потому что универсальный метод не компилируется. Не может быть решена. В настоящее время я не нашел пути.
  3. что-то с потребителем или поставщиком java 8 или что-то еще, но мне не удалось обернуться.

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

2 ответа

Решение

Я думаю, что вам нужно реализовать так называемый шаблон "Преобразователь данных".

Представьте, что у вас есть сценарий использования, который возвращает определенный объект домена (например, "Пользователь"), но вы не должны предоставлять домен клиентам. И вы хотите, чтобы каждый клиент выбирал формат возвращаемых данных.

Итак, вы определяете интерфейс преобразователя данных для объекта домена:

public interface UserDataTransformer {

    public void write ( User user );

    public String read();

}

Для каждого выходного формата ваши клиенты должны определить класс, реализующий интерфейс. Например, если вы хотите представить пользователя в формате XML:

public class UserXMLDataTransformer implements UserDataTransformer {

    private String xmlUser;

    @Override
    public void write(User user) {
        this.xmlUser = xmlEncode ( user );
    }

    private String xmlEncode(User user) {
        String xml = << transform user to xml format >>;
        return xml;
    }

    @Override
    public String read() {
        return this.xmlUser;
    }

}

Затем вы делаете свою прикладную службу зависимой от интерфейса trasnsformer данных, вы вставляете его в конструктор:

public class UserApplicationService {

    private UserDataTransformer userDataTransformer;

    public UserApplicationService ( UserDataTransformer userDataTransformer ) {
        this.userDataTransformer = userDataTransformer;
    }

    public void myUseCase ( Command c ) {
        User user = << call the business logic of the domain and construct the user object you wanna return >> ;
        this.userDataTransformer.write(user);
    }

}

И, наконец, клиент может выглядеть примерно так:

public class XMLClient {

    public static void main ( String[] args ) {

        UserDataTransformer userDataTransformer = new UserXMLDataTransformer();
        UserApplicationService userService = new UserApplicationService(userDataTransformer);
        Command c = << data input needed by the use case >>;
        userService.myUseCase(c);
        String xmlUser = userDataTransformer.read();
        System.out.println(xmlUser);
    }

}

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

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

Кроме того, это был просто пример. Вы можете использовать инфраструктуру внедрения зависимостей, такую ​​как Spring, чтобы создавать экземпляры компонентов и связывать их все. А также вы должны использовать корневой шаблон композиции, чтобы сделать это.

Надеюсь, этот пример помог.

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

Вы отправляете данные через границу между приложением и уровнем REST (и предположительно между приложением и потребителем REST); может быть полезно подумать о шаблонах обмена сообщениями.

Например, приложение может определять интерфейс поставщика услуг, который определяет контракт / протокол для приема данных из приложения.

interface ResponseBuilder {...}

void myUseCase(Command c, ResponseBuilder builder)

Адаптер REST обеспечивает реализацию ResponseBuilder которые могут принимать входные данные и генерировать из них некоторую полезную структуру данных.

Семантика построителя ответов (имена функций в интерфейсе) может быть получена из модели предметной области, но аргументы обычно будут либо примитивами, либо сообщениями других типов.

CQS подразумевает, что запрос должен возвращать значение; так что в этом случае вы можете предпочесть что-то вроде

interface ResponseBuilder<T> {
    ...
    T build();
}

<T> T myUseCase(Command c, ResponseBuilder<T> builder)

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

РЕДАКТИРОВАТЬ

Мое первое решение заключается в использовании Function<MyDomainModel, T> который немного отличается от вашего ResponseBuilder; но в том же духе.

Это почти двойственно. Возможно, вам было бы немного лучше с менее строгой подписью на myUseCase

<T> 
List<T> myUseCase(Command c, Function<? super MyDomainModel, T> fn)

Структура зависимостей по сути одна и та же - единственная реальная разница в том, с чем связан адаптер REST. Если вы думаете, что модель предметной области стабильна, а выходные представления будут сильно меняться, то функциональный подход дает вам стабильный API.

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

Другие вопросы по тегам