DDD 实战2 - 集成限界上下文(Rest & Dubbo)

DDD 实战1 - 基础代码模型 介绍了 DDD 的基础代码模型,本文来实现与第三方上下文的调用。首先介绍 ordercenter 做为消费者引用第三方接口的方式(第三方接口分别提供 Rest 和 Dubbo 两种形式),然后介绍 ordercenter 做为服务提供者为第三方提供服务接口的方式。

代码:https://github.com/zhaojigang/ordercenter

引用第三方接口

设计原则:

  1. 第三方服务的接入需要使用防腐层进行包装,进行防腐设计
  2. 第三方服务由应用服务层进行编排
  3. 第三方服务的实现由基础设施层进行实现
  4. 根据 外层依赖内层 的原则,需要将第三方服务的防腐接口和防腐模型放置在应用服务层,其实现放置在基础设施层;应用层只关心业务逻辑,不关心具体的技术实现(不关心是 Rest 服务还是 Dubbo 服务),基础设施层来关心技术细节。

应用服务层

image.png

应用服务 io.study.order.app.service.OrderAppService

/**
 * 订单应用服务
 */
@Service
public class OrderAppService {
    @Resource(name = "restInventoryAdaptor")
    private InventoryAdaptor inventoryAdaptor;

    /**
     * 创建一个订单
     *
     * @param order
     */
    public void createOrder(Order order) {
        /**
         * 获取商品库存信息,进行校验
         */
        InventoryDTO inventoryDTO = inventoryAdaptor.getRemainQuality(order.getGoodsId());
        if (inventoryDTO.getRemainQuantity() - order.getBuyQuality() < 0) {
            throw new OrderException(400, "商品库存不足");
        }

        /**
         * 扣减库存
         */
        inventoryAdaptor.reduceRemainQuality(order.getGoodsId(), order.getBuyQuality());

        /**
         * 存储订单
         */
        order.saveOrder(order);
    }
}

第三方服务防腐接口 io.study.order.rpc.inventory.InventoryAdaptor

/**
 * 库存第三方服务接口
 */
public interface InventoryAdaptor {
    /**
     * 获取商品剩余库存信息
     * @param goodsId
     * @return
     */
    InventoryDTO getRemainQuality(Long goodsId);

    /**
     * 扣减库存
     * @param goodsId
     * @param reduceQuality 减少的库存数
     * @return
     */
    Boolean reduceRemainQuality(Long goodsId, Integer reduceQuality);
}

第三方服务防腐模型 io.study.order.rpc.inventory.InventoryDTO

/**
 * 库存 DTO
 */
@Data
public class InventoryDTO {
    /**
     * 商品 ID
     */
    private Long goodsId;
    /**
     * 剩余库存
     */
    private Integer remainQuantity;
}

基础设施层

image.png

在基础设施层,使用了 Rest 和 Dubbo 两种方式来实现了库存服务接口。分别来看下实现。

第三方服务实现 io.study.order.rpc.impl.RestInventoryAdaptorImpl

/**
 * 库存服务(Rest 实现)
 */
@Component("restInventoryAdaptor")
public class RestInventoryAdaptorImpl implements InventoryAdaptor {
    private static final CloseableHttpClient HTTP_CLIENT = HttpClientBuilder.create().build();

    @Override
    public InventoryDTO getRemainQuality(Long goodsId) {
        HttpGet httpGet = new HttpGet("http://localhost:8082/inventory/getInventoryInfo?goodsId=" + goodsId);
        try {
            CloseableHttpResponse response = HTTP_CLIENT.execute(httpGet);
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                return RestInventoryTranslator.translateRestResponse2InventoryDTO(EntityUtils.toString(response.getEntity()));
            }
        } catch (IOException e) {
            throw new OrderException(500, "调用库存服务异常, e:" + e);
        }
        return null;
    }

    @Override
    public Boolean reduceRemainQuality(Long goodsId, Integer reduceQuality) {
        HttpPost httpPost = new HttpPost("http://localhost:8082/inventory/reduceRemainInventory?goodsId=" + goodsId + "&reduceQuality=" + reduceQuality);
        try {
            CloseableHttpResponse response = HTTP_CLIENT.execute(httpPost);
            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
                return RestInventoryTranslator.translateRestResponse2Boolean(EntityUtils.toString(response.getEntity()));
            }
        } catch (IOException e) {
            throw new OrderException(500, "调用库存服务异常, e:" + e);
        }
        return null;
    }
}

第三方服务防腐对象转换器 io.study.order.rpc.impl.RestInventoryTranslator

/**
 * 库存服务类型转换器(Rest)
 */
public class RestInventoryTranslator {
    public static InventoryDTO translateRestResponse2InventoryDTO(String restResponse){
        return JSON.parseObject(restResponse, InventoryDTO.class);
    }

    public static Boolean translateRestResponse2Boolean(String restResponse){
        return JSON.parseObject(restResponse, Boolean.class);
    }
}

库存服务 Rest 实现

/**
 * 库存 rest 服务
 */
@RestController
@RequestMapping("inventory")
public class InventoryController {
    @GetMapping("getInventoryInfo")
    public InventoryInfoDTO getInventoryInfo(@RequestParam("goodsId") Long goodsId) {
        InventoryInfoDTO dto = new InventoryInfoDTO();
        dto.setGoodsId(goodsId);
        if (goodsId == 1L) {
            dto.setRemainQuantity(100);
            dto.setInTransitQuantity(101);
        } else {
            dto.setRemainQuantity(200);
            dto.setInTransitQuantity(202);
        }
        return dto;
    }

    @PostMapping("reduceRemainInventory")
    public Boolean getInventoryInfo(@RequestParam("goodsId") Long goodsId, @RequestParam("reduceQuality") Integer reduceQuality) {
        return true;
    }
}

再来看下 Dubbo 的服务实现方式,Dubbo 的配置方式通常会有两种:xml 和注解方式。这里以注解方式进行演示。首先看库存服务提供的 Dubbo 服务。

库存服务(Dubbo 形式)

/************************* 接口 *************************/
/**
 * 库存服务对外接口
 */
public interface InventoryFacade {
    /**
     * 获取商品库存信息
     */
    InventoryInfoDTO getRemainQuality(Long goodsId);

    /**
     * 扣减库存
     */
    Boolean reduceRemainQuality(Long goodsId, Integer reduceQuality);
}

/************************* 实现 *************************/
/**
 * 库存服务实现
 */
@DubboService(version = "1.0.0", group = "product")
public class InventoryFacadeImpl implements InventoryFacade {
    @Override
    public InventoryInfoDTO getRemainQuality(Long goodsId) {
        InventoryInfoDTO dto = new InventoryInfoDTO();
        dto.setGoodsId(goodsId);
        if (goodsId == 1L) {
            dto.setRemainQuantity(100);
            dto.setInTransitQuantity(101);
        } else {
            dto.setRemainQuantity(200);
            dto.setInTransitQuantity(202);
        }

        return dto;
    }

    @Override
    public Boolean reduceRemainQuality(Long goodsId, Integer reduceQuality) {
        return true;
    }
}

/************************* 库存服务 Dubbo 接口返回值 *************************/
/**
 * 库存信息
 */
@Data
public class InventoryInfoDTO implements Serializable {
    private static final long serialVersionUID = -7542780056688475990L;
    /**
     * 商品ID
     */
    private Long goodsId;
    /**
     * 剩余库存
     */
    private Integer remainQuantity;
    /**
     * 在途库存
     */
    private Integer inTransitQuantity;
}

/************************* application.properties *************************/
dubbo.application.name=inventory
dubbo.registry.address=multicast://224.5.6.7:1234
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880

/************************* pom.xml *************************/
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.8</version>
        </dependency>

来看下在 ordercenter 引用第三方服务的姿势

第三方服务实现 io.study.order.rpc.impl.DubboInventoryAdaptorImpl

/**
 * 库存服务(Dubbo 实现)
 */
@Component("dubboInventoryAdaptor")
public class DubboInventoryAdaptorImpl implements InventoryAdaptor {
    @DubboReference(version = "1.0.0", group = "product")
    private InventoryFacade inventoryFacade;

    @Override
    public InventoryDTO getRemainQuality(Long goodsId) {
        InventoryInfoDTO inventoryInfoDTO = inventoryFacade.getRemainQuality(goodsId);
        return DubboInventoryTranslator.INSTANCE.toInventoryDTO(inventoryInfoDTO);
    }

    @Override
    public Boolean reduceRemainQuality(Long goodsId, Integer reduceQuality) {
        return inventoryFacade.reduceRemainQuality(goodsId, reduceQuality);
    }
}

第三方服务防腐对象转换器 io.study.order.rpc.impl.DubboInventoryAdaptorImpl

/**
 * 库存服务防腐对象转换器
 */
@org.mapstruct.Mapper
public interface DubboInventoryTranslator {
    DubboInventoryTranslator INSTANCE = Mappers.getMapper(DubboInventoryTranslator.class);
    InventoryDTO toInventoryDTO(InventoryInfoDTO inventoryInfoDTO);
}

基础设层包依赖

        <!-- dubbo -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
        </dependency>

服务启动模块

/************************* io.study.order.OrderApplication *************************/
/**
 * 应用启动器
 *
 * @author jigang
 */
@EnableOpenApi
@EnableDubbo // 启动 Dubbo
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

/************************* application.properties *************************/
# 数据库相关
...

# dubbo 共享
dubbo.application.name=ordercenter
dubbo.registry.address=multicast://224.5.6.7:1234

# dubbo 消费者
dubbo.consumer.check=false

这里将数据库、dubbo 的配置信息都写在了启动模块中,实际上也可以将这些配置写在他们各自使用的地方,比如可以将这些配置都写在 infrastructure 中,同时还可以将这些配置根据功能拆分成不同的配置文件,之后在启动类使用 @PropertySource 进行加载即可。

提供服务给第三方

如果仅提供 Rest 服务,那么当前的用户接口层中的 io.study.order.web.OrderController 即可。但是绝大多数情况下,需要提供类似 Dubbo 的使用方式,打成 Jar 包给第三方使用,为了避免内部逻辑泄露,以及为了打给第三方的包是一个“干净”的包,我们抽出一个单独的模块 order-client 来实现这一目的。

设计原则:

  1. 创建 order-client 模块:仅存储提供给第三方的接口和对象模型
  2. 用户接口层来实现 order-client 中的接口
  3. 领域层中需要使用 order-client 中的查询对象,所以领域层直接依赖 order-client,最终形成如下的依赖关系。
image.png

client 模块

image.png

对外接口 io.study.order.facade.OrderFacade

/**
 * 订单服务
 */
public interface OrderFacade {
    /**
     * 查询订单
     */
    List<OrderDTO> getOrderList(OrderQueryRequest request);

    /**
     * 创建订单
     */
    void createOrder(OrderDTO orderDTO);
}

对外接口返回模型 io.study.order.dto.OrderDTO

@Data
public class OrderDTO implements Serializable {
    private static final long serialVersionUID = 8642623148247246765L;
    /**
     * 订单ID
     */
    private Long id;
    /**
     * 订单名称
     */
    private String name;
    /**
     * 商品ID
     */
    private Long goodsId;
    /**
     * 购买数量
     */
    private Integer buyQuality;
}

对外接口请求参数 io.study.order.dto.OrderQueryRequest

/**
 * order 查询请求参数
 */
@Data
public class OrderQueryRequest implements Serializable {
    private static final long serialVersionUID = 3330101115728844788L;
    /**
     * 订单ID
     */
    private Long orderId;
    /**
     * 订单名称
     */
    private String orderName;
}

用户接口层

image.png
/**
 * 订单服务实现
 */
@DubboService(version = "1.0.0", group = "product")
public class OrderFacadeImpl implements OrderFacade {
    @Resource
    private OrderAppService orderAppService;
    @Resource
    private OrderRepository orderRepository;

    @Override
    public List<OrderDTO> getOrderList(OrderQueryRequest request) {
        List<Order> orderList = orderRepository.ordersOfCondition(request);
        return OrderDTOAssembler.INSTANCE.toDTOList(orderList);
    }

    @Override
    public void createOrder(OrderDTO orderDTO) {
        orderAppService.createOrder(OrderDTOAssembler.INSTANCE.fromDTO(orderDTO));
    }
}

Dubbo 服务的配置和启动与上述的库存服务相同,不在赘述,接下来着重看一下 OrderQueryRequest 对象的传输路径。

类:OrderFacade -> OrderFacadeImpl -> OrderRepository -> OrderRepositoryImpl
层:client      -> interfaces      -> domain          -> infrastructure

由于 domain 中要使用到 client 定义的对象,那么 domain 要依赖 client,乍一看,不符合 外层依赖内层 的原则,实际上,在 DDD 分层模型中,是没有 client 这个模块的;另外, 外层依赖内层 原则的目的是为了保证内层的稳定性,这个稳定怎么理解?个人理解为,模块内的代码不随外界技术的变动而变动,例如,将存储从 mysql 换成了 oracle,我们仅需要处理 infrastructure 层即可,其他内层不动;在比如,当前的都是直接穿数据库的,想使用 Cache Aside Pattern 加一层缓存,那么仅需要在 infrastructure 资源库的实现中进行修改即可,内层逻辑不应该动。但是现在如果是业务本身就发生了变化,那么内部的模型除了部分可以使用开闭设计避免变动时,大部分情况下还是要动的,不管是 application 还是 domain 层,client 被 domain 依赖就是这个道理,假设 domain 不依赖 client,那么我们需要在 domain 层也一模一样的设计一个查询模型,然后在用户接口层进行转换即可,这样也是可以实现的,但是必要性是否有,可以考虑一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容