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