Складирование десанта DDD

задняя часть Дизайн, управляемый доменом

Введение

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

Вчера в больнице заболела невестка.Когда она была на капельнице,у меня не было свободного времени.Нашел время сделать первую версию демки DDD.

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

image.png

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

Посмотрите демо, нажмите здесь, если вы найдете это полезным, добро пожаловать в звезду

Блог серии DDD

  1. Одна статья приведет вас к земле DDD
  2. Событийная модель приземления DDD
  3. Складирование десанта DDD
  4. Многоуровневая архитектура лендинга DDD

Эта статья познакомит вас с точкой знаний в DDD, которую легко понять и реализовать —Складирование.

Эта серия представляет собой миграцию фреймворка MVC на DDD, учитывая, что mybatis по-прежнему используется в качестве основного направления для развития бизнеса в крупных отечественных компаниях. Таким образом, миграция в демоверсии и соответствующие примеры в этой статье демонстрируются с помощью mybatis. Что касается того, является ли выбор хранилища приложений mybatis или jpa, он будет проанализирован в статье, пожалуйста, внимательно прочитайте эту статью.

Мой первый буклет Nuggets«Углубленный ДДД»Он уже онлайн в Наггетс, приглашаем всех попробовать~

Я также создал группу WeChat DDD, так как QR-код нельзя разместить в статье, вы можете добавить меня в WeChat.baiyan_lou, Обратите внимание на обмен DDD, я втяну вас в группу, добро пожаловать на обмен и вместе добивайтесь прогресса.

2. Складирование

2.1 Что такое складирование

оригинал"Дизайн, ориентированный на предметную область: решение проблемы сложности ядра программного обеспечения«Соответствующее объяснение складирования в:

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

С точки зрения непрофессионала, после создания модели предметной области вас не должно волновать, как осуществляется доступ к модели предметной области. Складское хозяйство эквивалентно мощному складу, вы сообщаете ему уникальный идентификатор:例如订单id, он может собрать все данные, которые вы хотите, в соответствии с заданной моделью предметной области и вернуть их вам за один раз. То же самое и с хранилищем.Вы отдаете ему все данные о заказе.Что касается того,как их разделить и на какой носитель их поместить【DB,Redis,ES等等】, это не ваше дело должно быть обеспокоено. Вы полностью доверяете ему в работе по управлению данными.

2.2 Зачем использовать складирование

во-первых贫血模型Недостатки:

Некоторые друзья уже предлагали определение неосознанности модели анемии, вот объяснение. Модель анемии: общие бизнес-объекты POJO, такие как PO, DTO и VO, являются носителями данных в data java, и внутри нет бизнес-логики. Вся бизнес-логика определена в различных сервисах, а сервисы выполняют различную логическую обработку между различными моделями, что раздуто, а логика неясна. Модель перегрузки: установите модель домена для формирования корня агрегата, где корень агрегата представляет бизнес, а методы и логика бизнес-обработки в текущем домене определяются в агрегате. Подтяните разбросанную логику.

  1. Целостность и непротиворечивость объектов модели не могут быть защищены:Поскольку все свойства объекта общедоступны, непротиворечивость модели может поддерживать только вызывающая сторона, что не гарантируется; в предыдущем случае вызывающая сторона не смогла обеспечить согласованность данных модели, что привело к использованию грязные данные Ошибки появляются время от времени, и такие ошибки особенно скрыты и их трудно обнаружить.
  2. Обнаруживаемость объектных операций крайне плохая:Просто из свойств объекта трудно увидеть, какая там бизнес-логика, когда его можно вызывать и какие границы можно назначать, например, может ли значение типа Long быть 0 или отрицательным?
  3. Логика кода повторяется:Например, логика проверки и логика вычислений легко появляются в нескольких службах и нескольких блоках кода, что увеличивает затраты на обслуживание и вероятность ошибок; распространенный тип ошибки заключается в том, что при изменении модели анемии логика проверки появляется в Во многих местах невозможно отследить никакие изменения, что приводит к сбою проверки или признанию ее недействительной.
  4. Плохая надежность кода:Например, изменение модели данных может привести к изменению всего кода сверху донизу.
  5. Сильная зависимость от базовой реализации:Бизнес-код в значительной степени зависит от базовой базы данных, сетевых протоколов/протоколов промежуточного ПО, сторонних сервисов и т. д., что приводит к жесткости основного логического кода и высоким затратам на обслуживание.

image.png

Хотя модель анемии имеет большие недостатки, в нашем ежедневном коде 99% кодов, которые я видел, основаны на модели анемии.Почему?

  1. Мышление базы данных:С того дня, как появилась база данных, образ мышления разработчиков постепенно изменился.写业务逻辑превратился в写数据库逻辑, что мы часто называем написаниемCRUD代码.
  2. Модель анемии «легкая»:Преимущество модели анемии заключается в том, что она «простая», это всего лишь сопоставление полей таблицы базы данных, поэтому ее можно использовать в едином формате от начала до конца. Здесь просто кавычки, потому что это только внешне просто На самом деле, когда в будущем произойдут изменения модели, вы обнаружите, что это не просто, и каждое изменение очень сложно.
  3. Сценарное мышление:Многие общие коды принадлежат脚本или胶水代码, это,流程式代码. Преимущество скриптового кода в том, что его легче понять, но ему не хватает надежности в долгосрочной перспективе, а стоимость обслуживания будет все выше и выше.

Но, возможно, основная причина в том, что на самом деле в нашем повседневном развитии мы смешиваем два понятия:

  • Модель данных:Относится к тому, как должны сохраняться бизнес-данные, и взаимосвязи между данными, что является традиционной моделью ER.
  • Бизнес-модель/модель предметной области:Относится к тому, как связанные данные должны быть связаны в бизнес-логике.

Таким образом, фундаментальное решение этой проблемы состоит в том, чтобы различать в коде модель данных и модель предметной области.Конкретные спецификации будут подробно описаны позже. В реальной структуре кода модель данных и модель предметной области фактически будут находиться на разных уровнях.Модель данных существует только на уровне данных, а модель предметной области находится на уровне предметной области, а ключевым объектом, связывающим эти два слоя, является Репозиторий.

Возможность изолировать наше программное обеспечение (бизнес-логику) и прошивку/аппаратное обеспечение (DAO, DB) и сделать наше программное обеспечение более надежным — основная ценность репозитория.

image.png

3. Посадка

3.1 Концептуальная карта посадки

1.png

Ассемблер DTO:На прикладном уровне[Служебный уровень приложения],EntityприбытьDTOПреобразователь имеет стандартное названиеDTO Assembler 【Ассемблер】.

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

Конвертер данных:на уровне инфраструктуры[Слой инфраструктуры],EntityприбытьDOПреобразователь не имеет стандартного имени, но для того, чтобы отличить Data Mapper, мы называем этот преобразовательData Converter. Здесь следует отметить, что Data Mapper обычно относится к DAO, например Mapper of Mybatis.

3.2. Спецификация репозитория

во-первых聚合и仓储между一一对应Отношение.仓储Это просто средство настойчивости и не должно содержать никаких бизнес-операций.

  1. Имена интерфейсов не должны использовать синтаксис базовой реализации.

    Определите интерфейс репозитория.В интерфейсе есть метод, похожий на save, который отличается от репозитория, ориентированного на коллекцию: репозиторий, ориентированный на коллекцию, может вызывать add только при добавлении, а репозиторий, ориентированный на сохранение, должен вызывать save независимо от того, является ли он новый или модифицированный.

  2. Параметры не должны использовать базовый формат данных:

    Следует помнить, что репозиторий работает с объектом Entity (на самом деле это должен быть совокупный корень) и не должен напрямую работать с нижележащим DO. Идя дальше, интерфейс репозитория должен фактически существовать на уровне предметной области, а реализация DO вообще не видна. Это также является надежной гарантией предотвращения проникновения базовой логики реализации в бизнес-код.

  3. Следует избегать так называемого «общего» шаблона репозитория.

    Многие фреймворки ORM предоставляют «общий» интерфейс репозитория, а затем фреймворк автоматически реализует интерфейс с помощью аннотаций. Типичными примерами являются Spring Data, Entity Framework и т. д. Преимущество этого фреймворка в том, что его легко реализовать с помощью конфигурации в простых сценариях. , но недостатком является то, что в принципе нет возможности расширения (например, добавления пользовательской логики кеша), и это все еще может быть отменено и переработано в будущем. Конечно, отказ от универсальных здесь не означает, что не может быть базовых интерфейсов и универсальных вспомогательных классов.

  4. Не пишите бизнес-логику в репозиторий

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

图片1.png

Внутри репозитория можно полагаться только на инструменты инфраструктуры упаковки носителей mapper, es и redis. Действие сохранения только анализирует входящий совокупный корень и помещает его на другой носитель данных.Если вы хотите поместить его в Redis, базу данных или es, преобразователь выполнит преобразование и анализ сводного корня. Точно так же данные, запрошенные с разных носителей, передаются преобразователю для сборки.

  1. Не контролировать транзакции внутри репозитория

    Ваш репозиторий используется для управления одним агрегатом, и контроль транзакций должен зависеть от выполнения бизнес-логики, а не от хранения и обновления данных.

3.3. Складирование CQRS

2222.pngОглядываясь назад на эту картинку, вы можете обнаружить, что модель данных добавления, удаления и модификации ушла из модели DDD. Запрос проникает непосредственно с уровня службы приложений на уровень инфраструктуры.

Это модель CQRS. С точки зрения данных добавление, удаление и изменение данных не является идемпотентной операцией. Любое действие может изменить данные, что называется危险行为. Запрос не изменит данные из-за изменения времени вашего запроса, которое называется安全行为. В процессе итерации функции логика модификации данных все еще сложна, поэтому моделирование также направлено на добавление, удаление и изменение данных.

Итак, каковы принципы запроса данных?

  1. Создайте независимый репозиторий

    Репозиторий запросов и репозиторий в DDD должны быть двумя методами, независимыми друг от друга. Строго говоря, в DDD всего три метода хранения: save, delete и byId, внутренней бизнес-логики нет, только данные разбиваются и объединяются. Метод хранилища запросов может настраивать структуру данных, возвращаемых хранилищем, в соответствии с потребностями пользователя и потребностями в исследованиях и разработках.

  2. Не превышайте свои полномочия

    Не делайте слишком много логики SQL в хранилище запросов и оставьте сборку запроса данных на Ассемблер.

  3. Правильно используйте ассемблер

    Подобно домашней странице, данные, возвращаемые интерфейсом, могут поступать из разных полей и могут даже не принадлежать его собственной бизнес-службе.

    Этот сложный результирующий набор передается Ассемблеру для завершения сборки и возврата окончательного результирующего набора. Когда структура достаточно проста, уровень взаимодействия с пользователем [контроллер, mq, rpc] может даже напрямую запрашивать результаты репозитория и возвращать их.

    Конечно, есть много других сообщений в блогах, в которых говорится, что если результаты запроса достаточно просты, вы даже можете вызвать mapper непосредственно на уровне контроллера, чтобы вернуть результаты запроса. Если вы не являетесь фиксированной службой словаря или таблицей правил, даже если бизнес прост, ваш бизнес будет повторяться, и последующая модель запроса изменится, а логика запроса на уровне dao будет перенесена на уровень взаимодействия с пользователем, который явно не стоит потери.

3.4. Выбор структуры ORM

В настоящее время основными платформами ORM являются mybatis и jpa. Mybatis больше используется в Китае, а jpa — за границей. Сравнение двух фреймворков в этой статье не будет расширено, если вы не знаете, в чем разница между двумя фреймворками, вы можете самостоятельно использовать Baidu.

Итак, если мы делаем DDD-моделирование, какую форму лучше выбрать?

mybatis — это полуавтоматический фреймворк.(当然现在有mybatis-plus的存在,mybatis也可以说是跻身到全自动框架里面了), внутреннее использование его в качестве основы формы является основным. Почему это мейнстрим?Потому что это достаточно просто.После разработки структуры таблицы можно сопоставить и развить поля, а также использовать бизнес-логику.胶水Приклейте по одному. А с точки зрения поддержки архитектуры, mybatis не поддерживает вложенные сущности, что лучше, чем mybatis в приложениях после моделирования предметной области.

Конечно, сегодня мы говорим об архитектуре,В любое время выбор технологии не является ключевым фактором в определении нашей технологической архитектуры..

JPA рождается с преимуществом выполнения DDD. Но это не означает, что mybatis не может выполнять DDD.Мы можем полностью отделить определение модели предметной области от применения фреймворка orm и отдельно определить преобразователь, чтобы реализовать преобразование между моделью предметной области и моделью данных. также сделайте то же самое для вас в демонстрации.

image.png


Конечно, если это новая система или время миграции достаточно велико, я все равноJPA рекомендуется, благополучный и в трансе~

image.png

4. демо

Описание требований, пользовательское поле имеетЧетыре бизнес-сценария

  1. Новые пользователи
  2. изменить пользователя
  3. удалить пользователей
  4. Данные пользователя отображаются в пагинации на странице списка

Демо реализации ядра, выложен не весь код, полное демо можно получить из репозитория github в начале статьи

4.1 Модель предметной области

/**
 * 用户聚合根
 *
 * @author baiyan
 */
@Getter
@NoArgsConstructor
public class User extends BaseUuidEntity implements AggregateRoot {
​
    /**
     * 用户名
     */
    private String userName;
​
    /**
     * 用户真实名称
     */
    private String realName;
​
    /**
     * 用户手机号
     */
    private String phone;
​
    /**
     * 用户密码
     */
    private String password;
​
    /**
     * 用户地址
     */
    private Address address;
​
    /**
     * 用户单位
     */
    private Unit unit;
​
    /**
     * 角色
     */
    private List<Role> roles;
​
    /**
     * 新建用户
     *
     * @param command 新建用户指令
     */
    public User(CreateUserCommand command){
        this.userName = command.getUserName();
        this.realName = command.getRealName();
        this.phone = command.getPhone();
        this.password = command.getPassword();
        this.setAddress(command.getProvince(),command.getCity(),command.getCounty());
        this.relativeRoleByRoleId(command.getRoles());
    }
​
    /**
     * 修改用户
     *
     * @param command 修改用户指令
     */
    public User(UpdateUserCommand command){
        this.setId(command.getUserId());
        this.userName = command.getUserName();
        this.realName = command.getRealName();
        this.phone = command.getPhone();
        this.setAddress(command.getProvince(),command.getCity(),command.getCounty());
        this.relativeRoleByRoleId(command.getRoles());
    }
​
    /**
     * 组装聚合
     *
     * @param userPO
     * @param roles
     */
    public User(UserPO userPO, List<RolePO> roles){
        this.setId(userPO.getId());
        this.setDeleted(userPO.getDeleted());
        this.setGmtCreate(userPO.getGmtCreate());
        this.setGmtModified(userPO.getGmtModified());
        this.userName = userPO.getUserName();
        this.realName = userPO.getRealName();
        this.phone = userPO.getPhone();
        this.password = userPO.getPassword();
        this.setAddress(userPO.getProvince(),userPO.getCity(),userPO.getCounty());
        this.relativeRoleByRolePO(roles);
        this.setUnit(userPO.getUnitId(),userPO.getUnitName());
    }
​
    /**
     * 根据角色id设置角色信息
     *
     * @param roleIds 角色id
     */
    public void relativeRoleByRoleId(List<Long> roleIds){
        this.roles = roleIds.stream()
                .map(roleId->new Role(roleId,null,null))
                .collect(Collectors.toList());
    }
​
    /**
     * 设置角色信息
     *
     * @param roles
     */
    public void relativeRoleByRolePO(List<RolePO> roles){
        if(CollUtil.isEmpty(roles)){
            return;
        }
        this.roles = roles.stream()
                .map(e->new Role(e.getId(),e.getCode(),e.getName()))
                .collect(Collectors.toList());
    }
​
    /**
     * 设置用户地址信息
     *
     * @param province 省
     * @param city 市
     * @param county 区
     */
    public void setAddress(String province,String city,String county){
        this.address = new Address(province,city,county);
    }
​
    /**
     * 设置用户单位信息
     *
     * @param unitId
     * @param unitName
     */
    public void setUnit(Long unitId,String unitName){
        this.unit = new Unit(unitId,unitName);
    }
​
}

4.2. Реализация хранилища DDD

/**
 *
 * 用户领域仓储
 *
 * @author baiyan
 */
@Repository
public class UserRepositoryImpl implements UserRepository {
​
    @Autowired
    private UserMapper userMapper;
​
    @Autowired
    private RoleMapper roleMapper;
​
    @Autowired
    private UserRoleMapper userRoleMapper;
​
    @Override
    public void delete(Long id){
        userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery().eq(UserRolePO::getUserId,id));
        userMapper.deleteById(id);
    }
​
    @Override
    public User byId(Long id){
        UserPO user = userMapper.selectById(id);
        if(Objects.isNull(user)){
            return null;
        }
        List<UserRolePO> userRoles = userRoleMapper.selectList(Wrappers.<UserRolePO>lambdaQuery()
                .eq(UserRolePO::getUserId, id).select(UserRolePO::getRoleId));
        List<Long> roleIds = CollUtil.isEmpty(userRoles) ? new ArrayList<>() : userRoles.stream()
                .map(UserRolePO::getRoleId)
                .collect(Collectors.toList());
        List<RolePO> roles = roleMapper.selectBatchIds(roleIds);
        return UserConverter.deserialize(user,roles);
    }
​
​
    @Override
    public User save(User user){
        UserPO userPo = UserConverter.serializeUser(user);
        if(Objects.isNull(user.getId())){
            userMapper.insert(userPo);
            user.setId(userPo.getId());
        }else {
            userMapper.updateById(userPo);
            userRoleMapper.delete(Wrappers.<UserRolePO>lambdaQuery().eq(UserRolePO::getUserId,user.getId()));
        }
        List<UserRolePO> userRolePos = UserConverter.serializeRole(user);
        userRolePos.forEach(userRoleMapper::insert);
        return this.byId(user.getId());
    }
​
}

4.3. Хранилище запросов

/**
 *
 * 用户信息查询仓储
 *
 * @author baiyan
 */
@Repository
public class UserQueryRepositoryImpl implements UserQueryRepository {
​
    @Autowired
    private UserMapper userMapper;
​
    @Override
    public Page<UserPageDTO> userPage(KeywordQuery query){
        Page<UserPO> userPos = userMapper.userPage(query);
        return UserConverter.serializeUserPage(userPos);
    }
​
}

5. план миграции мибатиса

Возьмем в качестве примера бизнес-сценарии OrderDO и OrderDAO.

  1. Создайте класс объекта Order, начальные поля могут быть согласованы с OrderDO
  2. Создайте OrderDataConverter, в основном 2 строки кода можно выполнить через MapStruct.
  3. Напишите модульные тесты, чтобы убедиться, что преобразование между Order и OrderDO на 100% правильно.
  4. Создайте интерфейс и реализацию OrderRepository и убедитесь в правильности OrderRepository посредством модульного тестирования.
  5. Измените место, где используется OrderDO в исходном коде, на Order
  6. Изменены места, где OrderDAO использовался в исходном коде, на OrderRepository.
  7. Обеспечьте согласованность бизнес-логики с помощью модульного тестирования.

6. Резюме

  1. Модель данных и модель предметной области нужно правильно различать, а складирование — это абстрактная реализация их взаимного преобразования.
  2. Хранилище защищает реализацию от бизнес-уровня, то есть уровню предметной области не нужно обращать внимание на то, как сохраняются объекты предметной области.
  3. Репозиторий — это контракт, а не уровень доступа к данным. Он явно указывает на манипулирование данными, необходимое для агрегирования.
  4. Репозиторий используется для управления одним агрегатом, он не должен контролировать транзакции.
  5. Выбор фреймворка ORM не является решающим в процессе миграции, поэтому конвертеры можно прививать, но JPA все же предпочтительнее.
  6. Хранилище запросов может преодолеть границу DDD, а уровень взаимодействия с пользователем может напрямую запрашивать.

7. Отдельное спасибо

lilpilot

8. Свяжитесь со мной

Если есть неточности в тексте, поправьте меня, текст писать непросто, ставьте лайк, ладно~

Диндин: louyanfeng25

WeChat: baiyan_lou

Общественный номер: дядя Бай Ян

image.png