предисловие
Приношу свои извинения читателям.В связи с загруженностью работой и погоней за качеством статьи вывод данной статьи идет медленно, но могу вас заверить, что содержание в статье неоднократно отрабатывалось и наступало. Первые несколько статей из серии DDD можно прочитать, нажав под текстом~
За последний год нашей командой было сделано много рефакторинга и миграции старых систем.Много кода принадлежит журнальному коду.Обычно видно,что код бизнес-логики написан непосредственно во внешнем интерфейсе API,или в Большое количество интерфейсов кучи приводит к тому, что фактическая бизнес-логика не может сходиться, а возможности повторного использования интерфейса относительно плохи. Таким образом, эта лекция в основном направлена на систематическое объяснение того, как преобразовать исходный код журнала в модуль с четкой логикой и четкими обязанностями посредством реконструкции DDD.
Введение в дело
Вот простой распространенный случай: размещение ссылки на заказ. Предположим, мы делаем интерфейс оформления заказа, нам нужно выполнить различные проверки, запросить информацию о продукте, вызвать службу инвентаризации, чтобы вычесть запасы, а затем сгенерировать заказ:
Более типичный код выглядит следующим образом:
@RestController
@RequestMapping("/")
public class CheckoutController {
@Resource
private ItemService itemService;
@Resource
private InventoryService inventoryService;
@Resource
private OrderRepository orderRepository;
@PostMapping("checkout")
public Result<OrderDO> checkout(Long itemId, Integer quantity) {
// 1) Session管理
Long userId = SessionUtils.getLoggedInUserId();
if (userId <= 0) {
return Result.fail("Not Logged In");
}
// 2)参数校验
if (itemId <= 0 || quantity <= 0 || quantity >= 1000) {
return Result.fail("Invalid Args");
}
// 3)外部数据补全
ItemDO item = itemService.getItem(itemId);
if (item == null) {
return Result.fail("Item Not Found");
}
// 4)调用外部服务
boolean withholdSuccess = inventoryService.withhold(itemId, quantity);
if (!withholdSuccess) {
return Result.fail("Inventory not enough");
}
// 5)领域计算
Long cost = item.getPriceInCents() * quantity;
// 6)领域对象操作
OrderDO order = new OrderDO();
order.setItemId(itemId);
order.setBuyerId(userId);
order.setSellerId(item.getSellerId());
order.setCount(quantity);
order.setTotalCost(cost);
// 7)数据持久化
orderRepository.createOrder(order);
// 8)返回
return Result.success(order);
}
}
Почему этот типичный журнальный код проблематичен в практических приложениях? Основная проблема заключается в том, что это нарушает принцип единой ответственности SRP (принцип единой ответственности). Этот код перемешан с бизнес-вычислениями, проверочной логикой, инфраструктурой и протоколами связи.В будущем, независимо от того, какая часть логики, изменения будут напрямую влиять на этот код, когда будущие поколения будут еще долго накладывать на него новую логику. , что усложнит код, вызовет все больше и больше логических ответвлений и, в конечном итоге, приведет к ошибкам или историческим отягощениям, которые никто не осмеливается рефакторить.
Поэтому нам нужно использовать идею многоуровневого DDD для рефакторинга вышеприведенного кода.Благодаря различным уровням кода и спецификациям мы можем разделить уровни и модули с четкой логикой и четкими обязанностями, что также облегчает выделение некоторых общих возможностей.
Основные этапы делятся на:
- Отдельный независимый уровень интерфейса интерфейса, отвечающий за обработку логики, связанной с сетевым протоколом.
- Узнайте конкретные варианты использования (Use Cases) из реальных бизнес-сценариев, а затем реализуйте конкретные варианты использования с помощью специальных инструкций Command, запросов Query и объектов события Event.
- Отделите независимый прикладной уровень, отвечающий за оркестровку бизнес-процессов, и отвечайте на команды, запросы и события. Каждый метод прикладного уровня должен представлять узел в общем бизнес-процессе.
- Решайте некоторые сквозные проблемы на разных уровнях, такие как аутентификация, обработка исключений, проверка, кэширование, ведение журнала и т. д.
Каждый пункт будет подробно объяснен ниже.
Уровень интерфейса интерфейса
С популяризацией архитектуры REST и MVC часто видно, что разработчики напрямую пишут бизнес-логику в контроллере, как в приведенном выше типичном случае, но на самом деле контроллер MVC — не единственная наиболее пострадавшая область. Следующие распространенные методы написания кода часто могут содержать одну и ту же проблему:
- Фреймворк HTTP: например, фреймворк Spring MVC, Spring Cloud и т. д.
- Фреймворк RPC: например, Dubbo, HSF, gRPC и т. д.
- «Потребитель» очереди сообщений MQ: например, onMessage JMS, MessageListener RocketMQ и т. д.
- Связь через сокет: получение связи через сокет, onMessage из WebSocket и т. д.
- Файловая система: WatcherService и т. д.
- Распределенное планирование задач: SchedulerX и т. д.
У всех этих методов есть одна общая черта: все они имеют свои собственные сетевые протоколы, и если наш бизнес-код и сетевые протоколы смешаны вместе, это напрямую приведет к тому, что код будет привязан к сетевому протоколу и не может быть повторно использован. Поэтому в многоуровневой архитектуре DDD мы будем извлекать уровень интерфейса интерфейса отдельно, как и все внешние порталы, чтобы отделить сетевые протоколы и бизнес-логику.
Состав интерфейсного слоя
Уровень интерфейса в основном состоит из следующих функций:
- Преобразование сетевых протоколов: Обычно это инкапсулируется различными фреймворками. Классы, которые нам нужно создать, представляют собой либо аннотированные компоненты, либо компоненты, наследующие интерфейс.
- Унифицированная проверка подлинности: например, в некоторых сценариях, требующих AppKey+Secret, необходимо выполнить проверку подлинности для определенного арендатора, включая проверку некоторых зашифрованных строк.
- Управление сеансом: как правило, в ориентированном на пользователя интерфейсе или в состоянии входа текущий вызываемый пользователь может быть получен через контекст сеанса или RPC для доставки нижестоящим службам.
- Конфигурация ограничения тока: ограничьте ток на интерфейсах, чтобы предотвратить попадание большого трафика на нижестоящие службы.
- Предварительное кэширование: для сценариев только для чтения с редкими изменениями результаты могут быть предварительно кэшированы на уровне интерфейса.
- Обработка исключений: обычно необходимо избегать прямого раскрытия исключений вызывающей стороне на уровне интерфейса, поэтому необходимо выполнять унифицированный захват исключений на уровне интерфейса и преобразовывать их в формат данных, понятный вызывающей стороне.
- Журнал: журнал вызовов на уровне интерфейса для статистики и отладки. Общие микросервисные платформы могут напрямую включать эти функции.
Конечно, при наличии независимого шлюза/приложения можно извлечь логику аутентификации, сессию, текущий лимит, журнал и т. д., но в настоящее время API-шлюз может выполнять только часть функций, даже в сценарии Шлюз API В будущем по-прежнему необходим отдельный уровень интерфейса в приложении. На уровне интерфейса аутентификация, сеанс, ограничение тока, кэширование, ведение журнала и т. д. относительно просты, и есть только один момент обработки исключений, который необходимо подчеркнуть.
Спецификация возвращаемого значения и обработки исключений, результат и исключение
Примечание. Эта часть в основном предназначена для интерфейсов REST и RPC, другие протоколы должны генерировать возвращаемые значения в соответствии со спецификациями протокола.
В каком-то коде, который я видел, возвращаемое значение интерфейса более разнообразно, некоторые напрямую возвращают DTO или даже DO, а другие возвращают Result. Основная ценность уровня интерфейса является внешней, поэтому, если вы просто вернете DTO или DO, вы неизбежно столкнетесь с исключениями и утечкой стека ошибок пользователю, включая потребление сериализуемого и десериализуемого стека ошибок. Итак, вот спецификация:
Спецификация: интерфейсы HTTP и RPC уровня интерфейса, возвращаемое значение — результат, перехват всех исключений.
Спецификация: возвращаемое значение всех интерфейсов прикладного уровня — DTO, и он не отвечает за обработку исключений.
Конкретные спецификации прикладного уровня будут обсуждаться позже, а здесь сначала будет показана логика уровня интерфейса.
Например:
@PostMapping("checkout")
public Result<OrderDTO> checkout(Long itemId, Integer quantity) {
try {
CheckoutCommand cmd = new CheckoutCommand();
OrderDTO orderDTO = checkoutService.checkout(cmd);
return Result.success(orderDTO);
} catch (ConstraintViolationException cve) {
// 捕捉一些特殊异常,比如Validation异常
return Result.fail(cve.getMessage());
} catch (Exception e) {
// 兜底异常捕获
return Result.fail(e.getMessage());
}
}
Конечно, писать логику обработки исключений для каждого интерфейса будет утомительно, поэтому можно использовать АОП в качестве аннотации.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ResultHandler {
}
@Aspect
@Component
public class ResultAspect {
@Around("@annotation(ResultHandler)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
Object proceed = null;
try {
proceed = joinPoint.proceed();
} catch (ConstraintViolationException cve) {
return Result.fail(cve.getMessage());
} catch (Exception e) {
return Result.fail(e.getMessage());
}
return proceed;
}
}
Затем окончательный код упрощается до:
@PostMapping("checkout")
@ResultHandler
public Result<OrderDTO> checkout(Long itemId, Integer quantity) {
CheckoutCommand cmd = new CheckoutCommand();
OrderDTO orderDTO = checkoutService.checkout(cmd);
return Result.success(orderDTO);
}
Количество интерфейсов на интерфейсном уровне и изоляция между сервисами
В спецификации интерфейса традиционных REST и RPC обычно интерфейс домена, будь то GET/POST/DELETE ресурса REST Resource или метод RPC, является относительно фиксированным и унифицированным и будет соответствовать единому домену. служба или контроллер домена.
Однако я обнаружил, что в процессе фактического ведения бизнеса, особенно когда поддерживается много вышестоящих предприятий, преднамеренное стремление к унификации интерфейсов обычно приводит к расширению параметров в методе или расширению метода. Например: предположим, что есть карта для домашних животных и бизнес-карты для родителей и детей, совместно использующие услугу регистрации карты, но домашнее животное должно пройти в типе домашнего животного, а родитель-ребенок должен пройти в возрасте ребенка.
// 可以是RPC Provider 或者 Controller
public interface CardService {
// 1)统一接口,参数膨胀
Result openCard(int petType, int babyAge);
// 2)统一泛化接口,参数语意丢失
Result openCardV2(Map<String, Object> params);
// 3)不泛化,同一个类里的接口膨胀
Result openPetCard(int petType);
Result openBabyCard(int babyAge);
}
Видно, что независимо от того, как выполняется операция, это может привести к тому, что сервис CardService будет становиться все более и более сложным в обслуживании в будущем, а методов становится все больше. весь сервис/контроллер и в конечном итоге становятся необслуживаемыми. Служба, в которой я принимал участие, предоставляет десятки методов и десятки тысяч строк кода Можно предположить, что и понимание пользователем интерфейса, и стоимость обслуживания кода чрезвычайно высоки. Итак, вот еще одна спецификация:
Спецификация: класс уровня интерфейса должен быть «маленьким и красивым», и он должен быть ориентирован на «один бизнес» или «класс бизнеса с одинаковыми требованиями». Необходимо избегать использования одного и того же класса для выполнения требования различных видов бизнеса.
Основываясь на приведенной выше спецификации, можно сделать вывод, что, хотя карточки для домашних животных и карточки для родителей и детей, похоже, имеют схожие потребности, они не являются "одними и теми же потребностями". Можно предвидеть, что в какой-то момент в будущем потребности и потребности этих два бизнеса будут предоставлены Интерфейс будет идти все дальше и дальше, поэтому два класса интерфейса необходимо разделить:
public interface PetCardService {
Result openPetCard(int petType);
}
public interface BabyCardService {
Result openBabyCard(int babyAge);
}
Преимущество этого заключается в том, что он соответствует принципу единой ответственности, что означает, что класс интерфейса будет меняться только из-за изменений в одном (или классе) бизнеса. Одно из предложений состоит в том, что, когда существующий класс интерфейса чрезмерно расширен, вы можете рассмотреть возможность разделения класса интерфейса.Принцип разделения согласуется с SRP.
Некоторые люди могут спросить, если следовать этому подходу, не будет ли сгенерировано большое количество классов интерфейса, что приведет к дублированию логики кода? Ответ — нет, потому что в многоуровневой архитектуре DDD основной функцией класса интерфейса является только уровень протокола.Протокол каждого типа бизнеса может быть разным, а реальная бизнес-логика будет отложена на прикладном уровне. То есть отношения между интерфейсом и приложением являются многими ко многим:
Поскольку бизнес-требования быстро меняются, интерфейсный уровень также должен быстро меняться.Независимый интерфейсный уровень может избежать взаимного влияния между бизнесами, но мы надеемся, что логика прикладного уровня относительно стабильна. Итак, давайте взглянем на некоторые характеристики прикладного уровня.
Прикладной уровень
Компоненты прикладного уровня
Несколько основных классов прикладного уровня:
- Служба приложений ApplicationService: основной класс, отвечающий за оркестровку бизнес-процессов, но не отвечающий за саму бизнес-логику.
- Сборщик DTO: отвечает за преобразование внутренних моделей предметной области во внешние DTO.
- Объекты Command, Query, Event: как входные параметры ApplicationService
- Возвращаемый DTO: как выходной параметр ApplicationService
Основным объектом прикладного уровня является ApplicationService, и его основная функция заключается в выполнении «бизнес-процессов». Но прежде чем говорить о спецификации ApplicationService, мы должны сосредоточиться на нескольких специальных типах объектов, а именно на Command, Query и Event.
Команда, запрос, объекты событий
По сути, все эти виды объектов являются объектами-значениями, но есть большие различия в семантике:
- Командная инструкция: относится к инструкции, которую вызывающий явно хочет, чтобы система работала, и его ожидание состоит в том, чтобы повлиять на систему, то есть на операцию записи. Обычно инструкции должны иметь явное возвращаемое значение (например, результат синхронной операции или принятая асинхронная инструкция).
- Запрос: Относится к вещам, которые вызывающий явно хочет запросить, включая параметры запроса, фильтрацию, разбиение по страницам и другие условия, которые, как ожидается, вообще не будут влиять на данные системы, то есть операции только для чтения.
- Событие события: относится к существующему факту, который уже произошел, и системе необходимо внести изменения или отреагировать на этот факт.Обычно обработка события будет иметь определенную операцию записи. Обработчики событий не имеют возвращаемого значения. Здесь следует отметить, что концепция события уровня приложения и доменного события уровня домена являются похожими концепциями, но не обязательно одним и тем же.Событие здесь является скорее внешним механизмом уведомления.
Кратко резюмируя:
Command | Query | Event | |
---|---|---|---|
семантика | Действия, которые может вызвать «надежда» | Запрос с различными условиями | что случилось |
читай пиши | Писать | только чтение | обычно пишут |
возвращаемое значение | DTO или логическое значение | DTO или коллекция | Void |
Зачем использовать объекты CQE?
Обычно во многих кодах вы можете видеть, что в интерфейсе есть несколько параметров, например, в случае выше:
Result<OrderDO> checkout(Long itemId, Integer quantity);
Если вам нужно добавить параметры в интерфейс с учетом прямой совместимости, вам нужно добавить метод:
Result<OrderDO> checkout(Long itemId, Integer quantity);
Result<OrderDO> checkout(Long itemId, Integer quantity, Integer channel);
Или общий метод запроса, который приводит к нескольким методам из-за разных условий:
List<OrderDO> queryByItemId(Long itemId);
List<OrderDO> queryBySellerId(Long sellerId);
List<OrderDO> queryBySellerIdWithPage(Long sellerId, int currentPage, int pageSize);
Видно, что с традиционным методом написания интерфейса есть несколько проблем:
- Расширение интерфейса: одно условие запроса один метод
- Сложно расширять: каждый новый параметр может потребовать обновления от вызывающей стороны.
- Сложно тестировать: когда есть много интерфейсов, обязанности усложняются, бизнес-сценарии различаются, а тестовые случаи сложно поддерживать.
Но еще одна важнейшая проблема: этот тип списка параметров не имеет никакой бизнес-семантики, это просто набор параметров, и он не может четко выразить намерение.
Спецификация CQE:
Поэтому в интерфейсе прикладного уровня настоятельно рекомендуется следующая спецификация:
Спецификация: входным параметром интерфейса ApplicationService может быть только объект Command, Query или Event, а объект CQE должен иметь возможность представлять семантику текущего метода. Единственным возможным исключением является случай запроса на основе одного идентификатора, вы можете не создавать объект запроса.
В соответствии с приведенной выше спецификацией случай реализации:
public interface CheckoutService {
OrderDTO checkout(@Valid CheckoutCommand cmd);
List<OrderDTO> query(OrderQuery query);
OrderDTO getOrder(Long orderId); // 注意单一ID查询可以不用Query
}
@Data
public class CheckoutCommand {
private Long userId;
private Long itemId;
private Integer quantity;
}
@Data
public class OrderQuery {
private Long sellerId;
private Long itemId;
private int currentPage;
private int pageSize;
}
Преимущества этой спецификации: повысить стабильность интерфейса, уменьшить повторение на низком уровне и сделать параметры интерфейса более семантическими.
CQE vs DTO
Как видно из приведенного выше кода, входным параметром ApplicationService является объект CQE, а выходным параметром является DTO.Из формата кода все они являются простыми объектами POJO, так в чем же между ними разница?
- CQE: объект CQE является входом ApplicationService и имеет четкое «намерение», поэтому этот объект должен обеспечивать его «правильность».
- DTO: объекты DTO — это просто контейнеры данных, только для взаимодействия с внешним миром, поэтому они не содержат никакой логики, это просто анемичные объекты.
Но, пожалуй, самый важный момент: поскольку CQE — это «намерение», поэтому объекты CQE теоретически могут иметь «неограниченное количество», каждый из которых представляет свое намерение; но DTO, как контейнер данных модели, соответствует модели один к одному, поэтому ограничено из.
Проверка CQE
Как ввод ApplicationService, CQE должен гарантировать его правильность, так где же находится эта проверка? В самом раннем коде была такая логика проверки, которая была написана в то время в сервисе:
if (itemId <= 0 || quantity <= 0 || quantity >= 1000) {
return Result.fail("Invalid Args");
}
Этот тип кода очень распространен в повседневной жизни, но его самая большая проблема заключается в том, что много кода, не связанного с бизнесом, смешивается с кодом бизнеса, что явно нарушает принцип единой ответственности. Но поскольку входным параметром в то время был только простой int, эта логика может появиться только в сервисе. Теперь, когда входной параметр изменен на CQE, мы можем использовать Bean Validation стандарта Java JSR303 или JSR380, чтобы добавить эту логику проверки.
Спецификация. Проверка объектов CQE должна выполняться заранее, чтобы избежать проверки параметров в ApplicationService. Может быть достигнуто с помощью JSR303/380 и Spring Validation.
Предыдущий пример можно преобразовать в:
@Validated // Spring的注解
public class CheckoutServiceImpl implements CheckoutService {
OrderDTO checkout(@Valid CheckoutCommand cmd) { // 这里@Valid是JSR-303/380的注解
// 如果校验失败会抛异常,在interface层被捕捉
}
}
@Data
public class CheckoutCommand {
@NotNull(message = "用户未登陆")
private Long userId;
@NotNull
@Positive(message = "需要是合法的itemId")
private Long itemId;
@NotNull
@Min(value = 1, message = "最少1件")
@Max(value = 1000, message = "最多不能超过1000件")
private Integer quantity;
}
Преимущество этого подхода заключается в том, что служба ApplicationService становится более обновленной, а различные сообщения об ошибках можно настраивать с помощью API проверки компонентов.
Избегайте мультиплексирования CQE
Поскольку у CQE есть «намерение» и «семантика», нам нужно максимально избегать повторного использования объектов CQE, даже если все параметры одинаковы, пока их семантика различна, попробуйте использовать разные объекты.
Спецификация: для инструкций с другой семантикой избегайте повторного использования объектов CQE.
❌ Пример встречного: Обычный сценарий — «Создать» и «Обновить». Вообще говоря, единственное различие между этими двумя типами объектов — это идентификатор. У создания нет идентификатора, а у обновления — есть. Поэтому часто можно увидеть, что некоторые студенты используют один и тот же объект в качестве входного параметра двух методов, разница только в том, присваивается ли идентификатор. Это неправильное использование, потому что семантика этих двух операций совершенно разная, и условия их проверки могут быть совершенно разными, поэтому повторно использовать один и тот же объект нельзя. Правильный способ - создать два объекта:
public interface CheckoutService {
OrderDTO checkout(@Valid CheckoutCommand cmd);
OrderDTO updateOrder(@Valid UpdateOrderCommand cmd);
}
@Data
public class UpdateOrderCommand {
@NotNull(message = "用户未登陆")
private Long userId;
@NotNull(message = "必须要有OrderID")
private Long orderId;
@NotNull
@Positive(message = "需要是合法的itemId")
private Long itemId;
@NotNull
@Min(value = 1, message = "最少1件")
@Max(value = 1000, message = "最多不能超过1000件")
private Integer quantity;
}
ApplicationService
ApplicationService отвечает за координацию бизнес-процессов. Это остающийся процесс после того, как исходный код бизнес-журнала лишен логики проверки, расчета домена, постоянства и другой логики. Это код «клеевого слоя».
Обратитесь к простому процессу транзакции:
В этом случае видно, что в области транзакции есть всего 5 вариантов использования: размещение заказа, успешная оплата, закрытие заказа в случае сбоя платежа, обновление информации о логистике и закрытие заказа. Эти 5 вариантов использования можно заменить 5 объектами Command/Event, что соответствует 5 методам.
Я видел 3 организационные формы ApplicationService:
- Класс ApplicationService — это законченный бизнес-процесс, в котором каждый метод отвечает за обработку варианта использования. Преимущество этого в том, что он может полностью свести воедино всю бизнес-логику, и вы можете получить определенное представление о бизнес-логике из класса интерфейса, что подходит для относительно простых бизнес-процессов. Минус в том, что для сложных бизнес-процессов в классе будет слишком много методов, а объем кода может оказаться слишком большим. Конкретными случаями этого типа являются:
public interface CheckoutService {
// 下单
OrderDTO checkout(@Valid CheckoutCommand cmd);
// 支付成功
OrderDTO payReceived(@Valid PaymentReceivedEvent event);
// 支付取消
OrderDTO payCanceled(@Valid PaymentCanceledEvent event);
// 发货
OrderDTO packageSent(@Valid PackageSentEvent event);
// 收货
OrderDTO delivered(@Valid DeliveredEvent event);
// 批量查询
List<OrderDTO> query(OrderQuery query);
// 单个查询
OrderDTO getOrder(Long orderId);
}
- Для более сложных бизнес-процессов можно уменьшить объем кода в классе, добавив независимые CommandHandler и EventHandler:
@Component
public class CheckoutCommandHandler implements CommandHandler<CheckoutCommand, OrderDTO> {
@Override
public OrderDTO handle(CheckoutCommand cmd) {
//
}
}
public class CheckoutServiceImpl implements CheckoutService {
@Resource
private CheckoutCommandHandler checkoutCommandHandler;
@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
return checkoutCommandHandler.handle(cmd);
}
}
- Более радикально, через CommandBus, EventBus, напрямую передать команду или событие соответствующему обработчику, EventBus встречается чаще. Конкретный код случая выглядит следующим образом: после получения сообщения MQ через очередь сообщений генерируется событие, а затем EventBus направляет его соответствующему обработчику:
// Application层
// 在这里框架通常可以根据接口识别到这个负责处理PaymentReceivedEvent
// 也可以通过增加注解识别
@Component
public class PaymentReceivedHandler implements EventHandler<PaymentReceivedEvent> {
@Override
public void process(PaymentReceivedEvent event) {
//
}
}
// Interface层,这个是RocketMQ的Listener
public class OrderMessageListener implements MessageListenerOrderly {
@Resource
private EventBus eventBus;
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
PaymentReceivedEvent event = new PaymentReceivedEvent();
eventBus.dispatch(event); // 不需要指定消费者
return ConsumeOrderlyStatus.SUCCESS;
}
}
⚠️ Не рекомендуется: этот подход может реализовать полную статическую развязку слоя интерфейса и определенного ApplicationService или обработчика, динамически отправлять во время выполнения и улучшать структуру, такую как AxonFramework. Хотя это кажется очень удобным, согласно нашей собственной деловой практике и наступая на яму, когда в коде появляется все больше и больше объектов CQE и обработчик становится все более и более сложным, в диспетчеризации во время выполнения отсутствует связь между статическими кодами, в результате в коде сложно читать, особенно когда нужно проследить сложную цепочку вызовов, т.к. диспетчеризация выполняется во время выполнения, сложно разобраться в конкретном объекте вызова. Поэтому, хотя мы пробовали это, больше не рекомендуется это делать.
Служба приложений является инкапсуляцией бизнес-процесса и не имеет отношения к бизнес-логике.
Хотя до этого неоднократно повторялось, что ApplicationService отвечает только за конкатенацию бизнес-процессов, а не за бизнес-логику, но как определить, является ли фрагмент кода бизнес-процессом или логикой? В качестве примера из ранее, после первоначального рефакторинга кода: Есть несколько моментов, по которым можно судить о том, является ли бизнес-процесс:
- Не иметь логики перехода if/else: то есть цикломатическая сложность кода должна быть равна 1, насколько это возможно.
Обычно существует логика ветвления, которая представляет некоторые бизнес-суждения, и логика должна быть инкапсулирована в DomainService или Entity. Но это не значит, что у вас вообще не может быть логики if, например, в этом коде: логическое значение withholdSuccess = inventoryService.withhold (cmd.getItemId(), cmd.getQuantity()); если (! удержать успех) { выбросить новое исключение IllegalArgumentException("Недостаточно инвентаря"); } Хотя CC > 1, это представляет собой только условие прерывания, и не влияет на конкретную обработку бизнес-логики. Думайте об этом как о предварительном условии.
@Service
@Validated
public class CheckoutServiceImpl implements CheckoutService {
private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE;
@Resource
private ItemService itemService;
@Resource
private InventoryService inventoryService;
@Resource
private OrderRepository orderRepository;
@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
ItemDO item = itemService.getItem(cmd.getItemId());
if (item == null) {
throw new IllegalArgumentException("Item not found");
}
boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity());
if (!withholdSuccess) {
throw new IllegalArgumentException("Inventory not enough");
}
Order order = new Order();
order.setBuyerId(cmd.getUserId());
order.setSellerId(item.getSellerId());
order.setItemId(item.getItemId());
order.setItemTitle(item.getTitle());
order.setItemUnitPrice(item.getPriceInCents());
order.setCount(cmd.getQuantity());
Order savedOrder = orderRepository.save(order);
return orderDtoAssembler.orderToDTO(savedOrder);
}
}
- Нет расчетов:
В самом раннем коде есть такой расчет:
// 5)领域计算
Long cost = item.getPriceInCents() * quantity;
order.setTotalCost(cost);
Инкапсулируя эту логику вычислений в объект, избегайте выполнения вычислений в ApplicationService.
@Data
public class Order {
private Long itemUnitPrice;
private Integer count;
// 把原来一个在ApplicationService的计算迁移到Entity里
public Long getTotalCost() {
return itemUnitPrice * count;
}
}
order.setItemUnitPrice(item.getPriceInCents());
order.setCount(cmd.getQuantity());
- Некоторые преобразования данных могут быть переданы другим объектам для выполнения:
Например, DTO Assembler помещает логику преобразования между объектами в отдельный класс, чтобы упростить ApplicationService.
OrderDTO dto = orderDtoAssembler.orderToDTO(savedOrder);
Общие «подпрограммы» ApplicationService
Мы видим, что код ApplicationService обычно имеет схожую структуру: AppService обычно не принимает никаких решений (кроме Precondition), а только передает все решения в DomainService или Entity, а внешнее взаимодействие передает интерфейсу Infrastructure, такому как Repository или антикоррозийный пол.
Общая «рутина» такова:
- Подготовить данные: включая извлечение соответствующих Entity, VO и DTO, возвращенных внешней службой из внешней службы или постоянного источника.
- Выполнение операций: включая создание новых объектов, назначение и вызов методов объектов предметной области для работы с ними. Следует отметить, что это время, как правило, чистая работа с памятью, непостоянство.
- Постоянство: сохраняйте результаты операции или управляйте внешними системами для создания соответствующих эффектов, включая асинхронные операции, такие как отправка сообщений.
Если это связано с изменениями в нескольких внешних системах (включая собственную БД), это обычно происходит в сценарии «распределенной транзакции», независимо от того, находится ли он в режиме распределенной TX, TCC или Saga, в зависимости от конкретного. Дизайн сцены временно здесь пропущено.
DTO Assembler
Часто упускают из виду вопрос, должен ли ApplicationService возвращать Entity или DTO? Вот спецификация в многоуровневой архитектуре DDD:
ApplicationService всегда должен возвращать DTO вместо Entity
Зачем?
- Построение границы домена: входным параметром ApplicationService является объект CQE, а выходным параметром — DTO.Это в основном простые POJO, чтобы гарантировать, что внутренняя и внешняя части уровня приложения не влияют друг на друга.
- Уменьшите зависимость от правил: Entity обычно содержит бизнес-правила. Если ApplicationService возвращает Entity, это заставит вызывающую сторону напрямую полагаться на бизнес-правила. Если внутренние правила изменения могут напрямую повлиять на внешние.
- Снижение затрат за счет комбинации DTO: Сущность ограничена. DTO может быть свободной комбинацией нескольких сущностей и VO. Она может быть инкапсулирована в сложные DTO одновременно, или некоторые параметры могут быть выборочно извлечены и инкапсулированы в DTO для снижения внешних затрат.
Поскольку объектом, с которым мы работаем, является Entity, а выходным объектом является DTO, нам нужен выделенный тип объекта, который называется DTO Assembler. Единственной обязанностью ассемблера DTO является преобразование одного или нескольких Entity/VO в DTO. Примечание. Ассемблер DTO обычно не рекомендует обратные операции, то есть он не будет преобразовывать DTO в Entity, потому что обычно при преобразовании DTO в Entity точность Entity не может быть гарантирована.
Обычно Entity to DTO имеет свою стоимость, будь то объем кода или операция во время выполнения. Написанный от руки код преобразования подвержен ошибкам. Чтобы сэкономить объем кода, Reflection приведет к большой потере производительности. Так что здесь я по-прежнему не жалею сил, чтобы рекомендовать библиотеку MapStruct. MapStruct генерирует код во время статической компиляции и может генерировать соответствующий код путем написания аннотаций интерфейса и конфигурации, а поскольку сгенерированный код назначается напрямую, его производительность практически незначительна.
С помощью MapStruct код можно упростить до:
import org.mapstruct.Mapper;
@Mapper
public interface OrderDtoAssembler {
OrderDtoAssembler INSTANCE = Mappers.getMapper(OrderDtoAssembler.class);
OrderDTO orderToDTO(Order order);
}
public class CheckoutServiceImpl implements CheckoutService {
private final OrderDtoAssembler orderDtoAssembler = OrderDtoAssembler.INSTANCE;
@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
// ...
Order order = new Order();
// ...
Order savedOrder = orderRepository.save(order);
return orderDtoAssembler.orderToDTO(savedOrder);
}
}
В сочетании с предыдущим Data Mapper отношения между DTO, Entity и DataObject выглядят следующим образом:
Result vs Exception
Наконец, как упоминалось выше, Result должен быть возвращен на уровне интерфейса, а DTO должен быть возвращен на уровне приложения, и здесь повторяется спецификация:
Уровень приложения возвращает только DTO и может создавать исключения напрямую без единой обработки. Все вызываемые службы также могут генерировать исключения напрямую. Если не требуется специальной обработки, нет необходимости преднамеренно перехватывать исключения.
Преимущество исключений заключается в том, что может быть четко известен источник ошибки, стек и т. д. Унифицированный захват исключений на уровне интерфейса предотвращает утечку информации о стеке исключений за пределы API, но на уровне приложения механизм исключений по-прежнему является наиболее информативным, а структура кода наиболее ясной. Метод позволяет избежать некоторых распространенных и сложных суждений Result.isSuccess о Result. Таким образом, на уровне приложения, уровне домена и уровне инфраструктуры наиболее разумным методом является создание исключения непосредственно при обнаружении ошибки.
Коротко о антикоррозийном слое Anti-Corruption Layer
Эта статья лишь кратко описывает принцип и функции ACL, а конкретную спецификацию реализации, возможно, придется отложить до следующей статьи.
В ApplicationService часто полагаются на внешние службы, а внешняя система зависит от уровня кода. Например выше:
ItemDO item = itemService.getItem(cmd.getItemId());
boolean withholdSuccess = inventoryService.withhold(cmd.getItemId(), cmd.getQuantity());
Вы обнаружите, что наша ApplicationService будет сильно зависеть от объектов ItemService, InventoryService и ItemDO. Если метод какой-либо службы изменен или поле ItemDO изменено, это может повлиять на код ApplicationService. Другими словами, наш собственный код будет изменен из-за сильной зависимости от изменений во внешних системах, чего следует по возможности избегать в сложных системах. Итак, как изолировать внешнюю систему? Необходимо добавить антикоррозийный слой ACL.
Простой принцип антикоррозионного слоя ACL заключается в следующем:
- Для зависимых внешних объектов мы извлекаем необходимые поля и генерируем внутренний требуемый класс VO или DTO.
- Создайте новый фасад, инкапсулируйте ссылку вызова в фасаде и преобразуйте внешний класс во внутренний класс.
- Для внешних системных вызовов тот же метод Facade используется для инкапсуляции ссылки на внешний вызов.
Без антикоррозийного покрытия:
С антикоррозийным слоем:
Простая реализация, предполагающая, что все внешние зависимости называются ExternalXXXService:
// 自定义的内部值类
@Data
public class ItemDTO {
private Long itemId;
private Long sellerId;
private String title;
private Long priceInCents;
}
// 商品Facade接口
public interface ItemFacade {
ItemDTO getItem(Long itemId);
}
// 商品facade实现
@Service
public class ItemFacadeImpl implements ItemFacade {
@Resource
private ExternalItemService externalItemService;
@Override
public ItemDTO getItem(Long itemId) {
ItemDO itemDO = externalItemService.getItem(itemId);
if (itemDO != null) {
ItemDTO dto = new ItemDTO();
dto.setItemId(itemDO.getItemId());
dto.setTitle(itemDO.getTitle());
dto.setPriceInCents(itemDO.getPriceInCents());
dto.setSellerId(itemDO.getSellerId());
return dto;
}
return null;
}
}
// 库存Facade
public interface InventoryFacade {
boolean withhold(Long itemId, Integer quantity);
}
@Service
public class InventoryFacadeImpl implements InventoryFacade {
@Resource
private ExternalInventoryService externalInventoryService;
@Override
public boolean withhold(Long itemId, Integer quantity) {
return externalInventoryService.withhold(itemId, quantity);
}
}
После преобразования ACL код нашего ApplicationService меняется на:
@Service
public class CheckoutServiceImpl implements CheckoutService {
@Resource
private ItemFacade itemFacade;
@Resource
private InventoryFacade inventoryFacade;
@Override
public OrderDTO checkout(@Valid CheckoutCommand cmd) {
ItemDTO item = itemFacade.getItem(cmd.getItemId());
if (item == null) {
throw new IllegalArgumentException("Item not found");
}
boolean withholdSuccess = inventoryFacade.withhold(cmd.getItemId(), cmd.getQuantity());
if (!withholdSuccess) {
throw new IllegalArgumentException("Inventory not enough");
}
// ...
}
}
Очевидно, что преимущество этого заключается в том, что код ApplicationService больше не зависит напрямую от внешних классов и методов, а зависит от наших собственных внутренне определенных классов значений и интерфейсов. Если в будущем будут какие-либо изменения во внешних службах, необходимо изменить класс Facade и логику преобразования данных, но логику ApplicationService изменять не нужно.
Репозиторий можно рассматривать как специальный ACL, который скрывает детали конкретных операций с данными.Даже если изменяется структура базовой базы данных, изменяется тип базы данных или добавляются другие методы сохранения, интерфейс репозитория остается стабильным, а ApplicationService может остаться без изменений.
В некоторых теоретических рамках фасад ACL также называется шлюзом, и это означает то же самое.
Orchestration vs Choreography
В конце этой статьи я хотел бы поговорить о спецификациях проектирования сложных бизнес-процессов. В сложных бизнес-процессах мы обычно сталкиваемся с двумя моделями: оркестровкой и хореографией. Очень беспомощный, перевод Baidu/перевод Google этих двух английских слов — «расстановка», но на самом деле эти два режима — совершенно разные режимы дизайна. Оркестровка оркестровки (например, оркестровка сервисов SOA/микросервисов Оркестровка сервисов) — обычное использование, с которым мы обычно знакомы Хореография — это недавнее появление управляемой событиями архитектуры EDA. В Интернете могут быть и другие переводы, такие как компиляция, хореография, коллаборация и т. д., но ни один из них не выражает толком значения английских слов, поэтому во избежание недоразумений я стараюсь максимально использовать оригинальные английские слова. возможный. Если у кого-то есть лучший метод перевода, пожалуйста, свяжитесь со мной.
Введение в шаблоны
Оркестровка: обычно на ум приходит симфонический оркестр (оркестр, обратите внимание на схожесть этих двух слов), как показано ниже. Ядром симфонического оркестра является один дирижер, Дирижер В симфоническом оркестре все музыканты должны подчиняться дирижеру дирижера и не могут выступать в одиночку. Таким образом, в шаблоне Orchestration все процессы запускаются узлом или службой. Наш общий код бизнес-процесса, включая вызов внешних служб, — это Orchestration, который единообразно запускается нашими службами.
Хореография: Сцена, которая обычно приходит на ум, — это танцевальная драма (от греческого слова «танец» «хорос»), изображенная ниже. Каждый из этих разных танцоров занимается своим делом, но единого централизованного дирижера нет. Через кооперацию и сотрудничество каждый занимается своим делом, и весь танец может показать полную и гармоничную картину. Так что в режиме Хореография каждый сервис является самостоятельным объектом и может реагировать на какие-то внешние события, но вся система представляет собой единое целое.
кейс
На обычном примере: оплатите и отправьте после заказа Если этот случай Orchestration, бизнес-логика такова: списать средства с предварительно сохраненного счета при оформлении заказа и сгенерировать логистический заказ на доставку, который выглядит так:
Если этот случай — хореография, бизнес-логика такова: разместить заказ, затем дождаться события успешной оплаты, а затем доставить товар, например:
Различия в режимах и выбор
Хотя кажется, что эти две модели могут достичь одной и той же бизнес-цели, но в реальной разработке они имеют огромные различия:
Из зависимостей кода:
- Оркестрация: служба вызывает другую службу.Для вызывающей стороны это сильно зависимый поставщик услуг.
- Хореография: каждый сервис просто делает свое дело, а затем запускает другие сервисы посредством событий. Прямая зависимость между вызовами между сервисами отсутствует. Но следует отметить, что нисходящий поток по-прежнему будет зависеть от кода восходящего потока (например, классов событий), поэтому можно считать, что нисходящий поток зависит от восходящего потока.
С точки зрения гибкости кода:
- Оркестровка: поскольку зависимости между службами жестко закодированы, добавление новых бизнес-процессов неизбежно потребует модификации кода.
- Хореография. Поскольку между службами нет прямой взаимосвязи вызова, службы можно добавлять или заменять без изменения исходного кода.
Из вызывающей ссылки:
- Оркестрация: он активно вызывает другую службу из одной службы, поэтому он управляется инструкциями, управляемыми командами.
- Хореография: каждая служба пассивно запускается внешними событиями, поэтому она управляется событиями.
С точки зрения деловых обязанностей:
- Оркестровка: есть активные абоненты (например, заказывающие услуги). Независимо от того, кто является нижестоящими зависимостями, активный вызывающий объект несет ответственность за весь бизнес-процесс и результаты.
- Хореография: нет активного вызывающего абонента, каждая служба заботится только о своих собственных условиях запуска и результатах, и ни одна служба не несет ответственности за всю бизнес-цепочку.
Подводя итог сравнения:
Orchestration | Choreography | |
---|---|---|
движущая сила | Управляемый командами | Событийный |
зависимость вызова | Восходящий поток сильно зависит от нисходящего | Нет прямых зависимостей вызова, но зависимости кода можно рассматривать как нижестоящие зависимости вверх по течению. |
гибкость | бедных | выше |
деловые обязанности | Upstream отвечает за бизнес | Нет глобального ответственного лица |
Еще один важный момент, который следует прояснить: разница между «управляемым инструкциями» и «управляемым событиями» не в «синхронном» и «асинхронном». Инструкции могут быть синхронными вызовами или триггерами асинхронных сообщений (но асинхронные инструкции не являются событиями); и наоборот, события могут быть асинхронными сообщениями, но также могут быть внутрипроцессными синхронными вызовами. Таким образом, суть различия между управляемым инструкциями и управляемым событиями заключается не в способе вызова, а в том, произошло ли что-то.
Итак, когда вы сталкиваетесь с необходимостью в своей повседневной работе, как вы выбираете, использовать ли оркестровку или хореографию?
Вот два метода оценки:
- Укажите направление зависимостей:
Зависимости в коде относительно ясны: если вы нижестоящий, а вышестоящий не знает о вас, вы можете работать только на основе событий; если вышестоящий должен знать о вас, вы можете управлять инструкциями. И наоборот, если вы восходящий поток и вам нужно сильно зависеть от нижестоящего, он управляется инструкциями; если не имеет значения, кто является нижестоящим, вы можете управлять событиями.
- Узнайте, кто «главный» в бизнесе:
Второй способ – узнать «ответственное лицо» по бизнес-сценарию. Например, если бизнесу необходимо уведомить продавца, единственная ответственность системы заказов не должна нести ответственность за уведомление о сообщении, но система управления заказами должна активно инициировать сообщение в соответствии с ходом статуса заказа, поэтому она является лицом, ответственным за эту функцию. В сложном бизнес-процессе обычно требуются оба паттерна, но также легко спроектировать ошибки. Если есть странная зависимость, или непонятна ссылка вызова/ответственное лицо в коде, можно попробовать конвертировать режим, что может быть намного лучше.
Какой режим лучше?
Очевидно, что лучшей модели не существует, есть только модель, наиболее подходящая для вашего бизнес-сценария.
❌ Контрпример: более популярная в последние годы событийно-управляемая архитектура Event-Driven Architecture (EDA), а также реактивное программирование Reactive-Programming (такое как RxJava), хоть и есть много нововведений, но в определенной степени «при есть молоток, классический случай, когда все проблемы связаны с гвоздем». Они чудесным образом справляются с некоторыми проблемами, связанными с событиями и потоковой обработкой, но если вы используете эти фреймворки для жестко заданного бизнеса, управляемого инструкциями, вы почувствуете, что код чрезвычайно «несвязный», и затраты на когнитивные функции возрастут. Поэтому в ежедневном отборе все же необходимо разобраться, какие части процесса являются Оркестрацией, а какие Хореографией по бизнес-сценарию, а затем выбрать соответствующий фреймворк.
Связь с многоуровневой архитектурой DDD
Наконец, после стольких разговоров о O и C, какое это имеет отношение к DDD? Это просто:
- O&C на самом деле находится в центре внимания уровня интерфейса, Orchestration = внешний API, а Choreography = сообщения или события. Когда вы выбираете O или C, вам нужно взять на себя эти «движущие силы» на уровне интерфейса.
- Независимо от того, как спроектирован O&C, прикладной уровень «не знает», потому что ApplicationService по своей природе может обрабатывать команды, запросы и события.Что касается того, откуда появляются эти объекты, это решение уровня интерфейса.
Следовательно, хотя оркестровка и хореография — это два совершенно разных шаблона бизнес-дизайна, код, который в конечном итоге попадает на прикладной уровень, должен быть одним и тем же, вот почему прикладной уровень — это «вариант использования», а не «интерфейс», который относительно стабильный.
Суммировать
Пока вы занимаетесь бизнесом, вам обязательно нужно будет писать бизнес-процессы и оркестровку сервисов, но это не значит, что качество этого кода обязательно будет плохим. Благодаря разумному разделению уровня интерфейса и уровня приложения в многоуровневой архитектуре DDD код может стать элегантным и гибким и может быстрее реагировать на бизнес, но в то же время он может быстрее выполняться. В этой статье в основном представлены некоторые спецификации дизайна кода, которые помогут вам освоить определенные навыки.
Интерфейсный слой:
- Обязанности: В основном отвечает за преобразование сетевых протоколов, управление сеансами и т. д.
- Количество интерфейсов: Избегайте так называемого унифицированного API. Нет необходимости искусственно ограничивать количество классов интерфейсов. Каждому/каждому виду бизнеса может соответствовать набор интерфейсов. Параметры интерфейса должны соответствовать бизнес-требованиям и избегать больших и всеобъемлющие входные параметры.
- Параметры интерфейса: унифицированный возврат Результат
- Обработка исключений: все исключения должны быть перехвачены, чтобы избежать утечки информации об исключениях. Его можно обрабатывать единообразно через АОП, чтобы избежать большого количества повторяющегося кода в коде.
Прикладной уровень:
- Входные параметры: Конкретные объекты Command, Query и Event в качестве входных параметров ApplicationService.Единственным исключением является сценарий запроса с одним идентификатором.
- Семантика CQE: Объекты CQE имеют семантику, и семантика разных вариантов использования различна.Даже если параметры одинаковы, следует избегать повторного использования.
- Проверка входных параметров: базовая проверка выполняется с помощью API-интерфейса Bean Validation. Spring Validation поставляется с AOP Validation, или вы можете написать свой собственный AOP.
- Выходные параметры: равномерно возвращать DTO вместо Entity или DO.
- Преобразование DTO: DTO Assembler используется для преобразования Entity/VO в DTO.
- Обработка исключений: исключения захватываются неравномерно, и исключения могут создаваться по желанию.
Часть инфра-слоя:
- Используйте антикоррозийный слой ACL для преобразования внешних зависимостей во внутренний код для изоляции внешних воздействий.
Шаблоны проектирования бизнес-процессов:
- Лучшей модели не существует, все зависит от бизнес-сценария, зависимостей и наличия у бизнеса «собственника». Не ищите гвозди молотком.
перспективный прогноз
- CQRS — это шаблон проектирования прикладного уровня, концепция проектирования, основанная на разделении команд и запросов, от простейшего разделения объектов до самого сложного в настоящее время источника событий. В этой теме много подробных моментов, и ее часто можно использовать, особенно в связке со сложными Агрегатами. Позже он будет вытащен отдельно, и его предварительное название будет «Сфера 7-го слоя CQRS».
- В современной сложной среде разработки микросервисов неизбежно полагаться на сервисы, разработанные внешними командами, но стоимость сильной связи (будь то изменения, зависимости кода или даже косвенные зависимости от пакетов Maven Jar) — это сложная система, которую нельзя игнорировать для давно дело. Антикоррозийный слой ACL — это концепция изоляции, которая устраняет внешнюю связь и делает внутренний код более чистым. Существует много видов антикоррозионных слоев ACL.Репозиторий — это специальный ACL для сохранения данных.K8S-sidecar-istio можно назвать ACL сетевого уровня, но в Java/Spring он может быть более эффективным и действенным, чем Istio Общий метод будет представлен позже.
- Когда вы начнете использовать DDD, вы обнаружите, что многие шаблоны кода очень похожи.Например, основной и подзаказы являются моделью общей оценки, модель CPV системы категорий также может использоваться в некоторых действиях, а Модель ECS может играть роль в интерактивном бизнесе и т.д. Позже я попытаюсь обобщить некоторые распространенные шаблоны проектирования предметной области, идеи их построения, типы проблем, которые можно решить, и методы практической реализации.
Добро пожаловать в контакт, продолжайте просить резюме
Добро пожаловать, чтобы увидеть одноклассников здесь, чтобы задать мне любые вопросы о DDD, я постараюсь ответить. Варианты кода в статье будут опубликованы на github для ознакомления позже. мой почтовый ящик:guangmiao.lgm@alibaba-inc.com, вы также можете добавить мой номер ногтя: luangm (Инь Хао)
Параллельно продолжается набор в нашу команду. Моя команда отвечает за индустрию Taobao и руководство по покупкам, включая четыре основные отрасли Tmall и Taobao (одежда, товары народного потребления, бытовая электроника и товары для дома) и несколько крупных горизонтальных направлений деятельности Taobao (корпоративные услуги, глобальные покупки, хорошие товары и т. д.) ежедневные потребности бизнеса и инновационные предприятия (3D / AR, панорамное видео 360, сопоставление, настройка, руководство по покупкам по размеру, руководство по покупкам SPU и т. д.), фронт-офис (iFashion, глобальные покупки, хорошие товары, и т. д.), а также некоторые сложные финансовые и транзакционные связи, выполнение контрактов (сопоставление IP-адресов, финансовые услуги, настройка транзакций, распределение, комиссии CPS, стыковка цепочки поставок услуг и т. д.), общее количество DAU (количество пользователей в день в среднем) составляет около 3000 Вт. Наша команда связалась с большим количеством бизнес-форм, от внешнего руководства по покупкам до выполнения внутренних контрактов, есть чрезвычайно богатые сценарии приложений. В новом финансовом году мы надеемся углубиться в отрасль, изучить новые бизнес-модели и связи с производительностью, охватить некоторые бизнес-модели, которые не могут быть охвачены традиционной моделью B2C, и помочь продавцам расти в новом направлении. Заинтересованные студенты могут присоединиться.
Автор|Инь Хао
Редактор | Orange Jun
Произведено | Alibaba New Retail Tao Technology