网页设计代码模板领域驱动编程应该怎么编写并没有一个统一的认知?

简介:领域驱动开发最重要的其实是恰当地进行领域拆解,这个拆解工作可以在理论的指导下,结合设计者对业务的深入探讨和充分理解进行。本文假定开发前早已进行了领域划分,侧重于探究编码阶段具体怎么实践能够表现领域驱动的优势。

作者|昌夜

前言

相较于大家熟练使用的MVC分层架构,领域驱动设计更适用于复杂业务平台和必须大幅迭代的硬件平台的构架模型。关于领域驱动设计的概念及优势,可以参考的文献比较多,大多数的朋友都看过相关的书籍,所以本文不争论领域驱动概念层面的东西,而是进而从编程实践的层面,对领域驱动开发做一些简单的介绍。

加入阿里健康之后,我所在的队伍也在切实加强领域驱动设计的应用,相关老师也曾给出优秀的脚手架代码,但现在看上去落地情况并不太理想,个人见解,造成这些结果主要有四个因素。

大家更熟悉MVC的编程范式,需要迅速推动某个功能的之后,往往偏向于使用较为稳妥、熟悉的方法。大家对领域驱动编程需要怎样编写并没有一个统一的认知(AxonFramework[1]对领域驱动设计推动的十分好,但它太“重”了)。DDD落地本身就非常难,往往必须事件驱动和EventStore来完美实现网页设计代码模板,而这两者是我们不常见的。领域驱动设计是面向复杂系统的,业务发展早期看起来都非常简单,一上来就搞领域驱动设计有过于设计之嫌。这只是领域驱动设计经常在平台不得不重构的是之后才被拿出来争论的缘由。

笔者曾在开发过程中探究、实践过领域驱动编程,对领域驱动框架AxonFramework也做了深入的知道,(虽然是由于业务场景相对简单)曾经落地效果还不错。抛却架构师的角度,从一线研发同学的视角来看,基于领域驱动编程的核心优势在于:

推进面向对象的编程范式,进而推动高内聚、低耦合。在复杂业务平台的降维过程中,保证代码结构不会无限制地更加混乱,因此确保平台可大幅维护。

领域驱动开发最重要的其实是恰当地进行领域拆解,这个拆解工作可以在理论的指导下,结合设计者对业务的深入探讨和充分理解进行。本文假定开发前早已进行了领域划分,侧重于探究编码阶段具体怎么实践能够表现领域驱动的优势。

保险领域知识简介

以保险业务为例来进行编程实践,一个高度抽象的保险领域划分如图所示。通过用例分析,我们把整个业务划分成产品域、承保、核保、理赔等多个领域(Bounded-Context),每个领域又可以按照业务发展状况拆分子域。当然,完备保险业务要比图中展示的复杂很多,这里我们不成为业务知识介绍的篇章,只是为了便于后续的代码实践。

领域驱动开发的代码结构

1、领域驱动的代码分层

可以使用不同的Java项目公布不同的微服务对领域进行隔离,也可以在同一个Java项目中,使用不同module进行领域隔离。这里我们使用module进行领域隔离的谋求。但是无论采取什么方法进行领域隔离,领域之间的交互只能使用他人的二方包以及API层提供的HTTP服务,而不能直接引入其它领域的其他服务。

在每个领域内部,相对于MVC对应用三层架构的分拆,领域驱动的设计将应用组件内个别为如图示的四层。

用户接口层

负责直接面向内部用户以及平台,接收外部输入,并返回结果,例如二方包的实现类、SpringMVC中的Controller、特定的数据视图转换器等一般位于该层。在代码层面通常使用的包命名可以是interface,api,facade等。用户接口层的入参、出参类定义运用POJO风格。

用户接口层是轻的一层,不含业务逻辑。安全认证,简单的入参校验(比如使用@Valid注解),访问日志记录,统一的异常处理逻辑,统一返回值封装必须在这层完成。

用户接口层所必须的用途实现是由应用层完成,这里通常不应该进行依赖倒置。编码时,该层可以直接采用应用层中定义的接口,因而该层依赖应用层。需要切记的是,虽然理论上用户接口层可以直接使用领域层和基础设备层的素质,但此处建议你们在对这些用法熟练掌握前,最好采取严格的分层架构,即当前层只依赖其下方相邻的一层。

应用层

应用层详细实现接口层中必须功能,但该层并不谋求真正的业务规则,而是按照实际的usecase来协调调用领域层提供的能力。

消息发送、事件监听、事务控制等建议在这一层实现。在代码层面通常使用的包命名可以是application,service,manager等。它用来代替SpringMVC中service层,并把业务逻辑转移到领域层。

领域层

领域层面向对象的,它主要拿来体现和推动领域里的对象所具有的固有能力。因此,在领域驱动编程中,领域层的编程实现是不允许依赖其它内部对象的,领域层的编程是在我们对领域内的对象所具有的固有能力和它要在当前业务场景下呈现哪个样的能力有一定知道后,可以直接编码实现的。

比如我们最起初接触面向对象的编程的之后,常常会遭遇的一个例证是鸟会飞、狗会游泳,假设我们的业务域只关心那些对象的运动,我们可以做如下的实现。

public interface Moveable {
    void move();
}
public abstract class Animal implements Moveable {}
public class Bird extends Animal {
  public void move(){
    //try to fly
    System.out.println("I'am flying");
  }
}
public class Dog extends Animal {
  public void move(){
    //try to swim
    System.out.println("I'am swimming");
  }
}

基于领域驱动的编程必须这么(充血模型)去推动对象的能力,而不是像我们在MVC架构中经常使用贫血模型,把业务逻辑写在service中。

其实,即使采取了这种的编程方法,距离实现领域驱动还差的远,一些看似简单的弊端就或许给我们增添巨大的压抑感。例如复杂的对象必须怎样初始化和长久化?同样一个事物在不同领域都存在,但其关注点不同时这个事物必须分别如何抽象?不同领域的对象必须对方的信息时,应当怎么获取?

这种问题,我们也会在代码实例部分尝试给出一些参考的方案。

基础设施层

基础设施层为里面各层提供通用的科技能力,例如监听、发送消息的能力,数据库/缓存/NoSQL数据库/文件系统等仓储的CRUD能力等。021

2、小结

根据对领域驱动设计各层的进一步探讨,一个非常准确化的分层结构如下。

基于上面的分层原则,前述保险领域一个可以参考的代码结构如下,我们将在以下编码示例详细讲解每一个分包的模式和作用。

领域驱动开发的代码

理论上,DOMAIN不依赖其它层次且是业务核心,我们必须先编写领域层代码,但是一则因为我们对保险领域知识的缺乏,可能不知道保单到底有什么固有能力;其二为了方便讲解,因此我们直接通过一个用例来展示代码。

1、用例

用户在后端页面选取保险产品,选择可选的保障责任,输入投/被保人信息,选择支付手段(贷款/趸交等)并支付后递交投保请求;服务端接受投保请求->核保->出单->下发保单权益。

此处用例1是用例2的前置用例,我们假设用例1已经成功完成(用例1中完成了费率计算),只来推动用例2,并且用例2也并非大略的谋求,只要能把代码样式展示即可。

2、用户接口层编程实践

分包结构

其中client是对inusurance-client(公共二方包)个别的实现,web是rest风格接口的实现。

用例代码

@AllArgsConstructor
@RestController
@RequestMapping("/insure")
public class PolicyController {
    private final InsuranceInsureService insuranceInsureService;
    /**
     * 投保出单
     * @param request
     * @return 保单 ID
     */
    @RequestMapping(value = "/issue-policy", method = RequestMethod.POST)
    public String issuePolicy(IssuePolicyRequest request){
        return insuranceInsureService.issuePolicy(request);
    }
}

此处用到的入参和返回值的类都在应用层中定义。

3、应用层编程实践

1、分包结构

注意,在领域编程实践中,会必须特别多的种类转换,我们可以通过一些框架(比如MapStruct[2])来降低这种类型转化给我们增添的繁琐工作。

2、用例代码

@Service
@AllArgsConstructor
public class InsuranceInsureServiceImpl implements InsuranceInsureService {
    private final PolicyFactory policyFactory;
    private final StakeHolderConvertor stakeHolderConvertor;
    private final PolicyService policyService;
    /**
     * 事务控制一般在应用层
     * 但是需要注意底层存储对事务的支持特性
     * 底层是分库分表时,可能需要其他手段来保证事务,或者将非核心的操作从事务中剥离(例如数据库 ID 生成)
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String issuePolicy(IssuePolicyRequest request) {
        Policy policy = policyFactory.createPolicy(request.getProductId(),
                    stakeHolderConvertor.convert(request.getStakeHolders()));
        //出单流程控制
        policyService.issue(policy);
        PolicyIssuedMessage message = new PolicyIssuedMessage();
        message.setPolicyId(policy.getId());
        MQPublisher.publish(MQConstants.INSURANCE_TOPIC, MQConstants.POLICY_ISSUED_TAG, message);
        return policy.getId().toString();
    }
}

此处代码展示的是应用层对用例2的处理。

4、领域层编程实践

1、分包结构

此处领域层一共有5个一级分包。

此外此处工厂的核心作用是从各处拉取初始化聚合或实体所必须的内部数据。

@Service
@AllArgsConstructor
public class PolicyFactory {
      /**
     * 产品领域防腐层服务
     */
    private final ProductService productService;
    /**
     * 从各种数据来源查询直接能查到的前置数据,填充到 policy 中
     * @param productId
     * @param stakeHolders
     * @return 
     */
    public Policy createPolicy(Long productId, List stakeHolders) {
        PolicyProduct product = productService.getById(productId);
        //其他填充数据,这里调用了聚合自身的静态工厂方法
        Policy policy = Policy.create(product, stakeHolders);
        return policy;
    }
}

以下是领域内核心的聚合Policy的样例代码。

    @Getter
public class Policy {
    private Long id;
    private PolicyProduct product;
    private List stakeHolders;
    private Date issueTime;
    /**
     * 工厂方法
     * @param product
     * @param stakeHolders
     * @return
     */
    public static Policy create(PolicyProduct product, List stakeHolders){
        Policy policy = new Policy();
        policy.product = product;
        policy.stakeHolders = stakeHolders;
        return policy;
    }
    /**
     * 保单出单
     */
    public void issue(Long id) {
        this.id = id;
        this.issueTime = new Date();
    }
}

2、用例代码

@Service
@AllArgsConstructor
public class PolicyService {
    private final InsureUnderwriteService insureUnderwriteService;
    private final PolicyRepository policyRepository;
    public void issue(Policy policy) {
        if(!insureUnderwriteService.underwrite(policy)){
            throw new BizException("核保失败");
        }
        policy.issue(IdGenerator.generate());
        //保存信息
        //policyRepository.save(policy);
        policyRepository.create(policy);
    }
}

此处注意我们注掉了一行policyRepository.save(policy);,那么为什么要区分save和create呢?

save是领域驱动设计中最恰当的做法:我的聚合以及实体有变动,仓储不用关心是新建还是升级,帮我保存起来就好了。听进去很幸福,但对关系型数据库存储却是很不友好的。因此,在我们的画面里,需要违背一下书上所谓的最佳实践,我们告诉仓储是要新建还是升级,甚至即使是更新的话更新的是什么列。

此外领域驱动的最佳实践是基于事件驱动的,AxonFramework对其有完美的推动,应用层发出一个IssuePolicyCommand指令,领域层接收该指令,完成保单创建后发出PolicyIssuedEvent,该event会被监听仍然持久化到eventstore中。这种方法至今看上去在我们这儿落地的或许性不大,不做更多介绍。

5、基础设备层编程实践

1、分包结构

此处只展现了repository的实现,但实际上此处也有RPC调用的二方包推动类注入等诸多内容。上文说到领域层不关心仓储的推动,交由基础设备层负责。基础设施层可以按照需要使用关系型数据库、缓存以及NoSQL,领域层是无感知的。这里我们以关系型数据库为例来,dao和dataobject等都可以使用诸如mybatisgenerator等软件生成,领域对象和dataobject之间的转化由convertor负责。

2、用例代码

@Repository
@AllArgsConstructor
public class PolicyRepositoryImpl implements PolicyRepository {
    private final PolicyDAO policyDAO;
    private final StakeHolderDAO stakeHolderDAO;
    private final PolicyConvertor policyConvertor;
    private final StakeHolderConvertor stakeHolderConvertor;
    @Override
    public String save(Policy policy) {
        throw new UnsupportedOperationException();
    }
    @Override
    public String create(Policy policy) {
        policyDAO.insert(policyConvertor.convert(policy));
        stakeHolderDAO.insertBatch(stakeHolderConvertor.convert(policy));
          //...其它数据入库
        return policy.getId().toString();
    }
    @Override
    public void updatePolicyStatus(String newStatus) {
    }
}

这部分代码相当简单,无需赘言。

结语

关于领域驱动,笔者仍进入初学者阶段,再好的设计,随着业务的演进,代码也常常显得混乱,这个过程中网页设计代码模板,每个参加者都有责任。最后,总结一下我们保持代码初心的一些方法,和你们分享。

[1]

[2]

原文链接:301MovedPermanently

添加微信

转载原创文章请注明,转载自设计培训_平面设计_品牌设计_美工学习_视觉设计_小白UI设计师,原文地址:http://www.zfbbb.com/?id=6036

上一篇:网页设计代码模板一下无敌低代码平台演示页面95的功能都是自动生成的

下一篇:报纸服装设计福建石狮有近5000家服装企业年工业产值超600亿元(图)