工程质量之研发过程管理需要关注的点

news2024/10/4 17:20:27

一、背景

  作为程序猿,工程质量是我们逃不开的一个话题,工程质量高带来的好处多多,我在写这篇文章的时候问了一下CHATGPT,就当娱乐一下,以下是ChatGPT的回答:
1、提高产品或服务的可靠性和稳定性。高质量的系统工程能够确保产品或服务的稳定性和可靠性,使其更加符合客户的需求和期望。
2、减少成本和节约时间。高质量的系统工程可以减少错误和缺陷的出现,从而减少修复错误和缺陷所需的时间和成本。
3、提高客户满意度。高质量的系统工程可以提高客户满意度,并增加客户的忠诚度和口碑。
  除了这些,对于我们程序猿而言,如果工程质量做得好,那么线上出现问题的机率会低很多,随之而来就是运维的工作也会不断减少,这样的话我们可以将更多的精力投入到其它更重要的项目中去,所以我觉得工程质量这一块非常重要,它决定一个团队是否有更多产出的关键,所以这一篇文章主要讲讲工程质量需要关注的一些点,特别是研发过程的管理,最为重要。

二、系统发展的三个阶段

  我们的系统发展主要分为三个阶段,如下图:
在这里插入图片描述
  为了保证系统能够稳定地对外提供服务,我们需要关注系统发展的三个阶段,也就是事前、事中、事后,对于每个阶段必须有对应的措施及系统工具来支持,来帮助我们提升系统的稳定性,快速定位问题并将影响尽量控制在一定范围内。

  • 事前:所谓事前主要是做好故障预防,通过持续交付、容量评估、自动化运维等完善的代码质量管理体系和自动化测试体系相关工具,来保证我们系统质量的同时也帮助研发人员能持续集成,避免将未经过测试的代码部署到生产环境。
  • 事中:事中其实包含故障发现、故障定位、故障恢复三个部分,当我们系统上线后,在大厂通常会有较为完善的监控体系,像我们公司有sirus、mercury、logtash,让我们尽早识别系统的潜在问题,系统相关人员可以快速甄别即将发生的故障,同时还有完整的链路跟踪系统,能很好的帮助我们去定位问题。另外完善的限流、熔断、降级策略,能够尽量缩小故障的影响范围,保证即便有部分服务不稳定,也不至于导致整个系统不可用。
  • 事后:主要是故障改进,一般来说当线上出现问题后,我们事后做好复盘并做好改进措施,对于疑难杂症的故障需要在测试环境模拟,对于QPS要求较高的系统需要做好压测。

三、影响工程质量的因素&做好工程质量需要具备的条件

在这里插入图片描述

  影响工程质量的因素,主要是人为因素、软件因素和硬件因素,所谓人为因素主要是一些不合理的变更和外部攻击等,比如去年我们安全做了一个变更,将测试环境变更误变更到生产,导致了重大故障。而软件因素大部分责任应该归在我们产研上,像我们技术方案没有考虑周全,存在漏洞,还有就是代码写得不够好,线上系统经常出问题,像FULLGC、空指针异常等等。硬件因素则不可预测,只能出了问题有对应的措施来快速恢复,像双活、灾备,某台机器硬件出现问题能生成新的容器将故障转移。
  那我们要提升工程质量,让我们的系统SLA达到三个9甚至4个9,需要具备哪些条件呢,我想也是三个,分别是组织文化、研发流程管控、技术支撑三个方面,我认为组织文化和研发过程管理最为重要,而技术支撑在大厂这一块基本比较完善了,都会有CI/CD、iaas/paas。

3.1 组织文化建设

  组织文化上,我们要打造一支重视技术和质量的团队,一定要在团队营造氛围。其实在软件系统的开发和运维的过程中,我们有很多手段可以发现问题,如监控报警、会议回顾、业务反馈等等,但要从根本上解决问题其实还是比较困难,在大多数情况下是头痛医头,到最后结果就是技术债务比较重,线上故障频发。究其原因可能比较复杂,但从我的视角来看,通常是团队对待技术并没有那么严谨,对待生产环境也没有那么敬畏,对待自己的代码也并不严苛。所以要提升系统的可用性,必须要有一支重视技术的团队,自上而下崇尚技术,尊重技术的氛围,不会为了短期的业务目标而在技术上做够多的妥协,即使有也要让我们技术债务可见,事后抽时间完善这一块,其实我很不赞成经常玩短期方案,虽然能享受快速上线带来的快感,但是这种快感的背后存在不少的弊端,就是后期运维的工作极大,故障频出,所以团队必须要重视工程质量,只有大家都重视起来,我们的工程质量才能真正做上去。除了这个外,跟我们研发相关的就是要重视研发过程管理。

3.2 研发过程管理

  研发过程管理是控制质量非常重要的一环,需要考虑到从需求的产生到最终功能上线,以及后续的运维的工作甚至是故障的处理,各个环节都需要做好把关,才能构建我们的质量控制体系。

3.3 技术支撑

  技术支撑这一块我就不详细说了,一般大厂在这一块相对较为完善基础设施,比如CI/CD、iaas/paas等,一般小厂相对较差一些,但我们要做好工程质量,持续集成、持续交付,将研发过程数字化还是挺重要的。

四、研发过程管理

  如果我们的过程管理不好或管理较为粗糙,往往会带来不少的问题,像FULLGC\空指针\资源使用超标告警,还有一些是我们做设计方案的时候没有考虑周全,导致业务流程阻断或数据出错,如果这种问题一多,产品和业务会有不少抱怨和投诉,我们开发就会天天加班,每天看上去很忙碌但实际开发效率低,我之前刚接手线下店就面临这种情况,像调拨、商品池一天不FULLGC就不正常,系统流单经常卡单,天天有业务投诉,前半年基本60%的时间都花在运维上面,而且天天晚上加班,白天在公司搞,晚上在家里搞,那半年团队的人都非常辛苦,我自己也瘦了10多斤,非常痛苦,所以在这几年我一直在强调团队主要骨干一定要做好研发过程管理。

4.1 研发过程包含的阶段

  那么研发过程到底包含哪些呢,无非就是需求、开发、测试、上线部署和运维5个阶段,而我们研发重点关注开发、上线和运维三个阶段。
在这里插入图片描述

  • 开发阶段:除了要做好技术方案中的总体设计,接口设计、方案评审外,最重要的是要signOff机制,也就是产品、研发、测试在方案评审结束后三方达成一致。另外开发人员写完代码后,需要做好自测的同时要做好codeReview.
  • 上线阶段: 做好上线步骤的规划,对于非常重要的业务,要做好灰度上线的准备等。
  • 运维阶段:线上出问题一定要做好事故的处理,对于影响主流程的BUG要第一时间去解决,减少业务影响,另外需要做事故的总结包括改进措施。
4.2 研发过程管理-技术方案

  如何做好技术方案,在说这个之前我们技术为什么要做技术方案,我觉得写技术方案根本目的是提高团队的沟通效率、提高研发和质量效率。
  那么沟通效率具体体现在:对于产品经理可以审查技术方案是否与产品设计有偏差,是否满足当前产品需求和后续产品设计规划,反过来我们研发通过技术方案来完善产品方案,这个是一个相补的过程。对于研发可以把控技术方案是否满足技术要求,包括技术规范,性能要求,实现复杂度,可拓展性等。对于测试同学可以掌握需求技术实现原理,改动点,影响点,再做针对性测试用例设计。另外还有一点就是后续接手项目的同学可以通过技术方案文档熟悉系统。
  通过写技术方案,把需求和实现提前梳理一遍,减少等到编码阶段返工的情况,并且写好技术方案再编码,使得编码时思维更加清晰,提高编码效率和质量。
  技术方案确实能带来不少好处,那么我们如何才能写好技术方案呢?其实好的技术方案其实就相当于一个工程施工图纸,研发人员拿到图纸就能按部就班实施,只是我们平时项目都是小步快跑,采用敏捷的方式,所以很多时候做了一些简化,目前我们大部分团队写概要设计更多一些,以下是概要设计包含部分,仅供参考。
在这里插入图片描述
  想成为优秀的开发人员,除了有过硬的技术功底外,必须要有跨界的意识,包括产品意识、测试意识和运维意识。

  • 产品意识:理解产品的具体业务流程是什么,产品为何这样设计,目前业务规划的发展方向,主动沟通产品方案中的不合理之处,提供参考解决办法。
  • 测试意识:技术方案中要考虑测试同学如何测试,提供必要的细节信息支撑测试。
  • 运维意识:对系统的日常运维、容量和系统瓶颈、系统流量增大扩容方案要有一定的认识和解决方案,保障业务正常使用。
4.3 高质量代码的评判原则和指导原则

在这里插入图片描述
  我接下来讲讲编写高质量代码的一些标准和原则,之前我们也经常讲作为程序员,衡量职业素养高低,代码就是其中之一,因为我们日常工作中接触最多就是写代码,代码的好坏就代表了个人的名片,那么如何评价代码的好坏,其实业界有了明确的原则,主要是评判原则和指导原则。
我们先来看一下评判原则,主要涉及可读性、可维护性、可扩展性、可复用性、灵活性等七个点,我重点讲我最关注的三点:

  • 1、可读性:在这七个点中,我个人认为代码的可读性应该是评价代码质量最重要的指标之一。在GOOGLE工程师需要考一个证,也就是代码“可读性“这个证书,只有拿到这个认证的人才有资格在CR的过程中批准别人提交代码,所以凸显了可读性的重要性,其实我也非常在乎这个可读性,我经常读大家的代码,一旦发现不好,基本是让大家重改。因此我们在编写代码的时候,时刻要考虑到代码是否易读、易理解,如果代码读不大懂,就很有可能因为考虑不周全,另外一个原因是代码除了给机器执行之外,更多的是给我们的开发人员去读,如果接替的开发都很难读懂,那会很容易产生BUG。
  • 2、可扩展性:就是说我们的代码要有面对未来新需求的变化能力,一般来说,开发新需求的时候,不修改原代码或很少修改,即可达到满足需求的能力,所以我们在设计的时候要预留一些功能扩展点。但实际上很多研发团队并没有做好,往往写的代码都是过程代码,既没有重用性、也不具备可扩展性。比如刚刚“主数据”的例子就是一个典型,代码既没可读性,也没有拓展性。
  • 3、可测试性/简洁性:其实可测试性、简洁性,其实比较难衡量,我最直观的想法就是要求代码越简单越好,一个方法代码行数越少越好,所以我在团队内部强制要求每个功能函数不能超过50行,到现在为止,我们团队新需求的代码基本在30行以内,一旦代码行数减下来后,单元测试就会比较容易测试,代码的CodeReview也非常容易。
      那么如何写出易扩展、可测试、高可读的高质量呢?高内聚低耦合几乎是每个程序员员都会挂在嘴边的,但这个词太过于宽泛,太过于正确,所以我们的前辈们提出了若干面向对象设计原则来衡量代码的优劣,主要是6大原则,这些理论想必大家都很熟悉了,是我们编写代码时的指导方针,按照这些原则开发的代码具有高内聚低耦合的特性。
4.4 系统的分层
  • 为什么要分层?我们分层的目的就是想让代码易读,易维护,先看一个例子:
public StoreListPageDTO getStoreList(String companyCode, int currentPage, int pageSize, SearchInputDTO searchInputDTO) throws OspException {
       Example example = new Example(StoreInfo.class);
       Example.Criteria criteria = example.createCriteria();
       if (StringUtils.isNotBlank(companyCode)) {
           criteria.andEqualTo("companyCode", companyCode);
       }

       StoreListPageDTO storeListPageDTO = new StoreListPageDTO();
       storeListPageDTO.setPageSize(pageSize);
       storeListPageDTO.setCurrentPage(currentPage);

       if (null != searchInputDTO) {
           if (StringUtils.isNotBlank(searchInputDTO.getStoreCode())) {
               criteria.andEqualTo("storeCode", searchInputDTO.getStoreCode());
           }
           if (StringUtils.isNotBlank(searchInputDTO.getWarehouseCode())) {
               criteria.andEqualTo("warehouseCode", searchInputDTO.getWarehouseCode());
           }
           if (StringUtils.isNotBlank(searchInputDTO.getStoreName())) {
               criteria.andLike("storeName", "%" + searchInputDTO.getStoreName() + "%");
           }
           if (StringUtils.isNotBlank(searchInputDTO.getStoreLevelCode())) {
               criteria.andEqualTo("storeLevelCode", searchInputDTO.getStoreLevelCode());
           }
           if (null != searchInputDTO.getStoreClass()) {
               criteria.andEqualTo("storeClass", searchInputDTO.getStoreClass());
           }
           if (null != searchInputDTO.getStoreTypeCode()) {
               criteria.andEqualTo("storeTypeCode", searchInputDTO.getStoreTypeCode());
           }
           if (StringUtils.isNotBlank(searchInputDTO.getZoneCode())) {
               criteria.andEqualTo("zoneCode", searchInputDTO.getZoneCode());
           }
           if (StringUtils.isNotBlank(searchInputDTO.getProvince())) {
               criteria.andEqualTo("provinceCode", searchInputDTO.getProvince());
           }
           if (StringUtils.isNotBlank(searchInputDTO.getCity())) {
               criteria.andEqualTo("cityCode", searchInputDTO.getCity());
           }
           if (StringUtils.isNotBlank(searchInputDTO.getContact())) {
               criteria.andLike("contact", "%" + searchInputDTO.getContact() + "%");
           }
           if (StringUtils.isNotBlank(searchInputDTO.getMobie())) {
               criteria.andEqualTo("mobie", searchInputDTO.getMobie());
           }

           if (null != searchInputDTO.getStartOpenDate()) {
               criteria.andGreaterThanOrEqualTo("openDate", searchInputDTO.getStartOpenDate());
           }
           if (null != searchInputDTO.getEndOpenDate()) {
               criteria.andLessThanOrEqualTo("openDate", searchInputDTO.getEndOpenDate());
           }


           //城市等级如果传参了,需要先根据城市等级查询对应的城市表,再把城市codeList作为传参
           if (null != searchInputDTO.getCityLevel()) {
               List<VipXstoreCityLevelCity> cityLevelCityList = cityLevelRepository.selectByCityLevelIdSet(Sets.newHashSet(searchInputDTO.getCityLevel()));
               //如果城市等级没有关联到任何城市,则直接返回空列表
               if (CollectionUtils.isEmpty(cityLevelCityList)) {
                   storeListPageDTO.setTotalCount(0);
                   storeListPageDTO.setStoreDTOList(new ArrayList<>());
                   return storeListPageDTO;
               }
               if (CollectionUtils.isNotEmpty(cityLevelCityList)) {
                   Set<String> cityCodeSet = new HashSet<>();
                   cityLevelCityList.stream().forEach(x -> cityCodeSet.add(x.getCityCode()));
                   //如果城市等级没有关联到任何城市,则直接返回空列表
                   if (CollectionUtils.isEmpty(cityCodeSet)) {
                       storeListPageDTO.setTotalCount(0);
                       storeListPageDTO.setStoreDTOList(new ArrayList<>());
                       return storeListPageDTO;
                   }
                   criteria.andIn("cityCode", cityCodeSet);
               }
           }
           if (StringUtils.isNotBlank(searchInputDTO.getSmallZoneName())) {
               criteria.andLike("smallZoneName", "%" + searchInputDTO.getSmallZoneName() + "%");
           }
           if (CollectionUtils.isNotEmpty(searchInputDTO.getSmallZoneCodeList())) {
               Set<String> smallZoneCodeSet = searchInputDTO.getSmallZoneCodeList().stream().filter(x -> StringUtils.isNotBlank(x)).collect(Collectors.toSet());
               if (CollectionUtils.isNotEmpty(smallZoneCodeSet)) {
                   criteria.andIn("smallZoneCode", smallZoneCodeSet);
               }
           }
           if (searchInputDTO.getStoreStatus() != null) {
               criteria.andEqualTo("storeStatus", searchInputDTO.getStoreStatus());
           }
       }

       criteria.andNotEqualTo("storeStatus", StoreStatusEnum.DELETED.getValue());
       example.setOrderByClause("update_time DESC");
       BaseLogic.PageInfo<VipXstoreStore> pageInfo = storeRepository.selectPageInfoWithCnt(example, currentPage, pageSize);


       int total = (int) pageInfo.getTotal();
       storeListPageDTO.setTotalCount(total);
       List<VipXstoreStore> storeList = pageInfo.getList();
       List<StoreDTO> storeDTOList = new ArrayList<>(total);

       Map<String, SimpleUserModel> userMapModels = new HashMap<>();
       if (CollectionUtils.isNotEmpty(storeList)) {
           List<String> userIds = storeList.stream().map(s -> s.getContactUserNumber()).collect(Collectors.toList());
           userMapModels = userService.getSimpleUserByUserIds(userIds).stream().collect(Collectors.toMap(u -> u.getUserNumber(), u -> u));
       }
       for (VipXstoreStore store : storeList) {
           if (Objects.equals(store.getStreetCode(), "")) {
               store.setStreetCode(null);
               store.setStreet(null);
           }
           StoreDTO storeDTO = convert(store);
           storeDTO.setOperator(store.getUpdateBy());

           //填充城市等级
           VipXstoreCityLevel vipXstoreCityLevel = cityLevelRepository.selectByCityCode(storeDTO.getCompanyCode(), storeDTO.getCityCode());
           storeDTO.setCityLevel(vipXstoreCityLevel == null ? null : vipXstoreCityLevel.getCityLevel());
           storeDTO.setCityLevelId(vipXstoreCityLevel == null ? null : vipXstoreCityLevel.getId());
           
           if((store.getCompanyCode().equalsIgnoreCase(CompanyCodeEnum.CITYOUTLETS.getCode())
                   ||store.getCompanyCode().equalsIgnoreCase(CompanyCodeEnum.OUTLETS.getCode()))
                   &&(store.getStoreClass()==StoreClassEnum.ADD.getValue() ||store.getStoreClass()==StoreClassEnum.RENT.getValue())
           ){
               storeDTO.setContact(store.getContact());
               storeDTO.setMobie(store.getMobie());
           }else{
               SimpleUserModel user = userMapModels.get(store.getContactUserNumber());
               if (user != null) {
                   storeDTO.setContact(user.getUserName());
                   storeDTO.setMobie(user.getUserPhone());
               }
           }
           storeDTOList.add(storeDTO);


       }
       storeListPageDTO.setStoreDTOList(storeDTOList);
       return storeListPageDTO;

   }

分层后代码优化:

public StoreQueryResp getStoreInfo(StoreQueryRequest req) throws OspException {
        StoreQuery storeQuery = StoreConverter.toStoreQuery(req);
        matchZoneByZoneName(req, storeQuery);
        matchSmallZoneByZoneName(req, storeQuery); 
        
        PageInfo<StoreInfo> storePage = storeRepo.listByExample(storeQuery);
        
        return StoreConverter.toStoreQueryResp(storePage);
    }    

  要想让我们的代码具备可读性、可复用性等,那么我们首先要对工程的代码进行分层,而且每层要有严格要求和规范,每层由多个模块组成,每层有自己独立的职责,多个层次协同提供完整的功能,带来的好处是:

  • 高内聚:分层的设计可以简化系统设计,让不同的层专注做某一模块的事
  • 低耦合:层与层之间通过接口或API来交互,依赖方不用知道被依赖方的细节
  • 复用:分层之后可以做到更多的复用
  • 扩展性:分层架构可以让我们更容易做横向扩展
    在这里插入图片描述
4.4.1 传统分层

在这里插入图片描述

4.4.2 现有常用分层

  随着软件复杂度越来越高,所以分层也越来越细,目前大部分都采用四层分层结构,具体如下:
在这里插入图片描述

4.5 写好代码的最佳路径

在这里插入图片描述
  我相信每个工程师都想写出高质量的代码,不想一直写没有成长、被人吐槽的烂代码。要写好代码,最佳的路径是学习、反复练习、多总结,养成良好的习惯非常重要,我先给大家讲一个小故事,前一段时间看了一本书叫《优势成长》,里面有一章节讲到了新东方,我们都知道新东方的老师非常牛,那么他们是如何从一个普通老师变成大家都喜欢的老师,其核心就是正确的姿势重复做,在新东方有一个试讲环节,应聘老师得对着台下那些老教师们去讲,这些老师也不好好听,干什么的都有,当讲着讲着,他们还会不断打断你,然后告诉你前面40分钟讲的都不好,重来一遍。于是1个小时的课,你得反复讲,反复调整,有时候一节课,要被老师们批评半年时间,打磨半年之后才真正的讲好一堂课。其实在这个过程非常的煎熬,中途很多应聘的老师被气哭、被骂走了,一开始100人最后剩下20人都不到了。在新东方他们内部有句口号,通过把自己逼疯从而把对手逼死。所以我们看到这些新东方的老师各个出口成章,能言会道,都是在极端苛刻的条件下训练而成。经过千百次的刻意练习后形成肌肉记忆,从知道,再到熟练,进而精通。所以我们很多人都说要写好代码,知道高内聚低耦合,并不是我们讲讲口号就行,这中间还差了千百次的刻意练习才能转化成你的技能,所以学习、反复实践并总结才能成为一个代码技术达人。

4.5.1 代码实战
抽像能力

  抽象思维是我们工程师最重要的思维能力,因为软件技术本质上就是一门抽象的艺术。我们工程师每天都要动用抽象思维,对问题域进行分析、归纳、综合、判断、推理,从而抽象出各种概念,挖掘概念和概念之间的关系,然后通过编程语言实现业务功能,所以,我们大部分的时间并不是在写代码,而是在梳理需求,理清概念,对需求有一个全局的认知。而抽像能力让我及团队切身感受到,它给我们在编码和设计上带来的质的变化。

  • 案例一:异步Excel导出

其实导出Excel功能在我们工程里随处可见,特别是咱们的运营希望一次性导出越多数据越好,为了不给我们系统带来太大压力,对于大数据量的导出一般异步进行,针对于这样一个简单的功能,那么应该如何抽像呢?

普通的写法:
public String exportXXX(参数) throws Exception {
	//业务实现
}

public String exportXXX2(参数) throws Exception {
	//业务实现
}
抽像写法:

我们其实可以把每个异步导出看作是一个异步任务,而每个任务可导出的内容是不一样的,因此完全可以把导出抽像一个方法,由每个具体实现类去实现导出不同的内容,具体如下:

// export excel 
public interface IExcelExportTask {
    String export(BizCommonExportTask exportTask) throws Exception;
}
//样例实现类
XXXXExportTask implements IExcelExportTask {
	String export(BizCommonExportTask exportTask) throws Exception{
    	public String export(BizCommonExportTask exportTask) throws Exception {
    	//组织数据筛选条件
        TestReq queryReq = GsonUtils.toObject(exportTask.getInputParams(),TestReq.class);
        String fileName = String.format("%s%s%s", exportTask.getUploadFileName(),System.currentTimeMillis(),".xlsx");

        String downUrl = excelService.uploadExcel(fileName, null, new Fetcher<PreOccupyModel>(PreOccupyModel.class) {
        	//循环获取数据
            @Override
            public List<TestModel> fetch(int pageNo, int pageSize) throws OspException{
                TestQueryResp resp = testFethchLogic.fetchRecord(queryReq);
                return pageNo > resp.getPageNum() ? Collections.emptyList() :toExcelModel(resp);
            }
        });
        return downUrl;
    }
}

public class XXXXExportTask1 implements IExcelExportTask {
    @Override
    public String export(BizCommonExportTask exportTask) throws OspException {
        TestQuery query = GsonUtils.toObject(exportTask.getInputParams(), TestQuery .class);
        String fileName = String.format("%s%s%s", exportTask.getUploadFileName(), System.currentTimeMillis(), ".xlsx");

        return excelService.uploadExcel(fileName, null, new Fetcher<ExportItemModel>(TestModel.class) {
            @Override
            public List<TestModel> fetch(int pageNo, int pageSize) throws OspException {
                return XXXXLogic.queryExportItem(query, pageNo, pageSize);
            }
        });
    }
}
//导出任务分发器
public class ExcelTaskDispacther extends ApplicationObjectSupport {
	public boolean dispacthTask(Long taskId) throws OspException {
        
        updateTaskStatus(exportTask,CommonExportStatus.CREATING,TransferExportStatus.CREATING,StringUtils.EMPTY);
        try {
            String beanName =  getBeanName();
            ExportTaskHandler exportTaskHandler = getApplicationContext().getBean(beanName , IExcelExportTask .class);
            if(exportTaskHandler == null) {
                log.warn(String.format("任务ID[%s]写入配置错误!", taskId));
                return false;
            }
            
            updateTaskStatus(exportTask,CommonExportStatus.CREATE_SUCCESS,TransferExportStatus.CREATE_SUCCESS,StringUtils.EMPTY);
            log.info(String.format("任务ID[%s]RFID为[%s]处理成功", exportTask.getId(),rfid));
            return true;
        } catch(BusiException ex) {
            log.info("任务ID[{}]失败,原因:{}", exportTask.getId(),ex.getMessage(),ex);
            updateTaskResult();
        } catch(Exception ex) {
            log.info("任务ID[{}]失败,原因:{}", exportTask.getId(),ex.getMessage(),ex);
            updateTaskResult();
        }
        return false;
    }
}
  • 案例二:系统通知

  在微服务化流行的今天,为了提升系统吞吐量,系统职责越来越细,各系统模块需要频繁交互数据,那么对于复杂的数据交互场景,比如我们调拨单,调拨单在扭转的过程中需要与很多系统交互,跟门店、仓库、库存模块有非常多的交互,我们又该如何抽像呢,以下是调拨与各系统交互的代码示例

//接口定义
public interface BizNotificationHandler {
    /**
     * 抛异常会当失败处理
     * 是否需要重试由BizNotificationStatus返回状态来决定
     * @param bizNotification
     * @return
     * @throws OspException
     */
    BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException;
}

//推送调拨差异数据给库存系统
public class SyncDiffToSimsAndBackQuotaHandler implements BizNotificationHandler {    
    @Override
    public BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException {
        //业务逻辑实现
        
        return BizNotificationStatus.PROCESS_SUCCESS;
    }
}
//占用库存
public class TransferOccupyInventoryHandler implements BizNotificationHandler {
    @Override
    public BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException {
        //业务实现
    }
}

//在GPDC生成新条码
public class GpdcGenerateNewBarcodeHandler implements BizNotificationHandler {
    @Override
    public BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException {
        //业务代码实现
    }

其实我们在与其它系统交互的时候,我们可以把每一个交互动作抽像成一个通知事件,每次交互的时候,写一个事件通知事件即可。

4.5.2 组合/聚合复用原则

  关于组合/聚合复用原则,其实我们在项目过程会经常遇到,比如项目里会经常管理各种单据,像采购单、调拨单、收货单等,而对于每种单据都会有各种各样的较验,我们先来看一段建调拨单代码,具体如何下:

//接口定义
public interface TransferValidator {
    boolean validator(CreateTransferCtx ctx) throws OspException;
}
//接口实现1
public class W2sCrossPoQtyValidator implements TransferValidator {
    @Override
    public boolean validator(CreateTransferCtx ctx) throws OspException {
        //较验器代码实现
    }
//接口实现2
public class W2sStoreBarcodeSaleLimitValidator implements TransferValidator {
    @Override
    public boolean validator(CreateTransferCtx ctx) throws OspException {
        //较验器代码实现
    }
}

//较验器组装
public class TransferValidators {

    public ValidatorChain newChain() {
        return new ValidatorChain();
    }

    public class ValidatorChain {
        private final List<TransferValidator> validators = new ArrayList<>();

        public ValidatorChain qtyValidator() {
            validators.add(qtyValidator);
            return this;
        }

        public ValidatorChain transferRouteCfgValidator() {
            validators.add(transferRouteCfgValidator);
            return this;
        }

        public ValidatorChain prodValidator() {
            validators.add(prodValidator);
            return this;
        }

        public ValidatorChain w2sWarehouseStoreValidator() {
            validators.add(w2sWarehouseStoreValidator);
            return this;
        }

        public ValidatorChain w2sStoreBarcodeSaleLimitValidator() {
            validators.add(w2sStoreBarcodeSaleLimitValidator);
            return this;
        }

        public ValidatorChain w2sAssignPoValidator() {
            validators.add(w2sAssignPoValidator);
            return this;
        }

        public ValidatorChain w2sCrossPoValidator() {
            validators.add(w2sCrossPoValidator);
            return this;
        }

        public ValidatorChain w2sCrossPoQtyValidator() {
            validators.add(w2sCrossPoQtyValidator);
            return this;
        }

        public ValidatorChain w2sCross4XupValidator() {
            validators.add(w2sCross4XupValidator);
            return this;
        }

        public ValidatorChain repeatLineValidator() {
            validators.add(repeatLineValidator);
            return this;
        }

        public ValidatorChain sstradeBarcodeValidator() {
            validators.add(sstradeBarcodeValidator);
            return this;
        }

        public ValidatorChain s2wWarehouseStoreValidator() {
            validators.add(s2wWarehouseStoreValidator);
            return this;
        }

        public boolean validator(CreateTransferCtx ctx) throws OspException {
            for (TransferValidator validator : validators) {
                if (!validator.validator(ctx)) {
                    return false;
                }
            }
            return true;
        }
    }
}

//业务代码使用
public interface TransferCreator {
    boolean createOrder(CreateTransferCtx ctx) throws OspException;
}

public abstract class DefaultTransferCreator implements TransferCreator {
     @Override
    public boolean createOrder(CreateTransferCtx ctx) throws OspException {
        validator(ctx)
        //实现业务逻辑
    }

    protected abstract boolean validator(CreateTransferCtx ctx) throws OspException;
 }
//店仓调拨单 
public class S2wRefundCreator extends DefaultTransferCreator {
	//较验器自由组装
    @Override
    protected boolean validator(CreateTransferCtx ctx) throws OspException {
        return transferValidators.newChain()
                .qtyValidator()
                .transferRouteCfgValidator()
                .prodValidator()
                .validator(ctx);
    }
}

  通过上面的示例,其实抽像并不难,难的是我们要花时间去思考,去理解,只有自己花足够的多时间,反复训练我相信比较容易做到。
  写出满足这些评价标准的高质量代码,我们需要掌握一些更加细化、更加能落地的编程方法论,包括面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。而所有这些编程方法论的最终目的都是为了编写出高质量的代码。比如,面向对象中的继承、多态能让我们写出可复用的代码;编码规范能让我们写出可读性好的代码;设计原则中的单一职责、DRY、基于接口而非实现、里式替换原则等,可以让我们写出可复用、灵活、可读性好、易扩展、易维护的代码;设计模式可以让我们写出易扩展的代码;持续重构可以时刻保持代码的可维护性等等。

五 、研发过程管理中善用工具并数字化

  研发过程我们不刚要写好技术方案、写好代码,但仅做好这两点还不够,我们需要善用于一些工具,将过程管理数字化,我们平常工作中很多人只管理写好代码,但代码是否符合预期,认为是测试的事情,这个观点是错误的,所以一定要善用工具sonar、测试覆盖率、流量回放等工具,将过程管理数字化,同时要与我们的合作伙伴产品、测试、研发形成良好的互动,如果有条件的话跟我们的业务做一些沟通也是非常有必要的。
在这里插入图片描述

六 、总结

在这里插入图片描述
  要想让我们的系统能够平稳运行,我们需要在抓好人、工具、预案、目标这几个点,只有抓好这些点,我们才能真正构建高性能、高可用、可降级的业务系统。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/421745.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

光时域反射仪那个品牌的好用

光时域反射仪 哪个品牌好用 光时域反射仪要怎么选到合适自己的&#xff0c;这些问题 可能一直在困扰这一线的工作人员&#xff0c;下面小编就为大家一一解答下 首先光时域域反射仪是一款检测光纤线路的损耗 长度 以及 事件点的一款设备&#xff0c;在诊断 光纤线路 故障点的情…

从零开始学架构——CAP理论

CAP定理 CAP 定理&#xff08;CAP theorem&#xff09;又被称作布鲁尔定理&#xff08;Brewer’s theorem&#xff09;&#xff0c;是加州大学伯克利分校的计算机科学家埃里克布鲁尔&#xff08;Eric Brewer&#xff09;在 2000 年的 ACM PODC 上提出的一个猜想。2002 年&…

Web前端 HTML、CSS

HTML与CSSHTML、CSS思维导图一、HTML1.1、HTML基础文本标签1.2、图片、音频、视频标签1.3、超链接、表格标签1.4、布局1.5、表单标签1.6、表单项标签综合使用1.7、HTML小结二、CSS&#xff08;简介&#xff09;2.1、引入方式2.2、选择器2.3、CSS属性Web前端开发总览 Html&…

案例拆解丨ChatGPT+塔罗牌,批量起号、暴利引流,小白也能轻松月入10000+

ChatGPT 的出现&#xff0c;大大拉低了很多行业的门槛&#xff0c;比如客服、教育、翻译、自媒体……而塔罗牌占卜&#xff0c;肯定也是其中之一。 塔罗牌是一种占卜工具&#xff0c;由78张牌组成。可以用于占卜、灵性探索、个人成长和自我发现。 这是一个相对小众&#xff0c…

LinuxGUI自动化测试框架搭建(十三)-创建工具集目录tools并封装文件复制方法cpoyFile.py

(十三)-创建工具集目录tools并封装文件复制方法cpoyFile.py 1 tools的作用2 创建tools目录3 创建文件复制方法cpoyFile.py4 设计cpoyFile.py4.1 安装shutil4.2 导入模块4.3 脚本设计5 目前框架目录1 tools的作用 为了存放框架需要用到的一些常用工具或方法,比如文件复制功能…

OJ系统刷题 第九篇(难篇)

13441 - 求小数的某一位&#xff08;难题&#xff0c;二刷、三刷&#xff01;&#xff09; 时间限制 : 1 秒 内存限制 : 128 MB 分数\tfrac {a}{b}ba​化为小数后&#xff0c;小数点后第n位的数字是多少&#xff1f; 输入 三个正整数a&#xff0c;b&#xff0c;n&#xff0…

使用jni-rs实现Rust与Android代码互相调用

本篇主要是介绍如何使用jni-rs。有关jni-rs内容基于版本0.20.0&#xff0c;新版本写法有所不同。 入门用法 在Rust库交叉编译以及在Android与iOS中使用中我简单说明了jni-rs及demo代码&#xff0c;现在接着补充一些详细内容。 首先贴上之前的示例代码&#xff1a; use std:…

嘉靖王朝最大的一出闹剧和惨剧——大礼仪之争

大礼仪之争 大礼议是指发生在正德十六年&#xff08;1521年&#xff09;到嘉靖三年&#xff08;1524年&#xff09;间的一场皇统问题上的政治争论。 原因是明世宗以地方藩王入主皇位&#xff0c;为其改换父母的问题所引起&#xff0c;是明朝历史第二次小宗入大宗的事件。 “…

罗丹明荧光染料标记叶酸,FA-PEG-RB,叶酸-聚乙二醇-罗丹明;Folic acid-PEG-RB

FA-PEG-RB,叶酸-聚乙二醇-罗丹明 中文名称&#xff1a;叶酸-聚乙二醇-罗丹明 英文名称&#xff1a;FA-PEG-RB, Folic acid-PEG-RB 性状&#xff1a;粉红色固体或液体&#xff0c;取决于分子量 溶剂&#xff1a;溶于水和DMSO、DMF等常规性有机溶剂 保存条件&#xff1a;-2…

【Python】【进阶篇】二十一、Python爬虫的多线程爬虫

目录二十一、Python爬虫的多线程爬虫21.1 多线程使用流程21.2 Queue队列模型21.3 多线程爬虫案例1) 案例分析​2) 完整程序二十一、Python爬虫的多线程爬虫 网络爬虫程序是一种 IO 密集型程序&#xff0c;程序中涉及了很多网络 和 本地磁盘的 IO 操作&#xff0c;这会消耗大量…

自绘 MFC 控件 CComboBox

运行效果: 第一步:在窗口中拖拽一个CComboBox控件,设置如下属性: 类型,设置为:下拉列表包含字符串,设置为:True所有者描述,设置为:Variable 注意: 包含字符串,不设置为True,则使用GetLBText等函数无法获取到Item的text;所有者描述,设置为No,不执行DrawItem、M…

实战大数据项目

存储日志数据集&#xff08;HDFS&#xff09; 数据仓库构建&#xff08;Hive&#xff09; 数据分区表构建 数据预处理 &#xff08;Spark计算引擎&#xff09;-使用Zeppelin进行写SQL 订单指标分析 Sqoop数据导出到传统数据库&#xff08;Mysql&#xff09; Superset数据…

Vue2_02_指令

模板语法 — Vue.js (vuejs.org) 指令 (Directives) 是带有 v- 前缀的特殊 attribute 参数 一些指令能够接收一个“参数”&#xff0c;在指令名称之后以冒号表示 <a v-bind:href"url">...</a> 动态参数 可以用方括号括起来的 JavaScript 表达式作为一…

企业消费管理迈向数字化,助力员工满意度提升,解决行政·财务·采购等部门痛点 | 爱分析调研

调研&#xff1a;李进宝 撰写&#xff1a;李进宝 某市场巡视人员&#xff1a;每次出差都要垫钱&#xff0c;每月还只能报销一次&#xff0c;这不是自费上班吗&#xff1b; 某软件研发人员&#xff1a;我们每天都要加班到很晚&#xff0c;公司提供晚餐&#xff0c;但高油高盐&…

ERP系统应用场景,API接口接入

ERP订单管理系统能为企业带来什么 1、ERP订单管理系统可以对工厂物料采购的时间进行提示&#xff0c;根据产品目前的库存情况来进行确定&#xff0c;并比较物料采购时所需要支付的费用是多少&#xff0c;什么时候采购价格相对更加便宜。 2、在产品的生产方面&#xff0c;涉及…

Linux远程连接虚拟机超时,且ip地址找不到问题解决

ip地址虚拟机自动更改&#xff1a; 原因&#xff1a;Linux没有正常关机 解决&#xff1a;从虚拟机在自己电脑上的文件地址中bin目录下&#xff0c;前面几个以.lck的文件全部删除 Linux远程连接虚拟机超时&#xff1a; 原因可能跟上面是一样的&#xff0c;IP地址自动修改之后自…

华为 ADS 2.0 发布,城区智驾之战「白热化」

作者 | 马波编辑 | 德新虽然上海车展还未正式拉开帷幕&#xff0c;但今天的华为却通过一系列新品的发布为今年的汽车盛会进行了预热。就在今天上午&#xff0c;华为车BU正式发布了一系列新品&#xff0c;同时也对部分现有产品进行了升级。其内容之多、升级的幅度之大&#xf…

Maven项目中的依赖出现版本冲突,最终发现是对Dependency Scope理解有误

再来个文章目录 文章目录背景疑问排查过程问题存在的原因总结示例依赖版本说明本文记录一下遇到maven依赖版本冲突后的排查过程说明以及问题原因说明 下面还有投票&#xff0c;帮忙投个票&#x1f44d; 背景 最近加入了 Apache Dubbo 开源社区&#xff0c;成为了一名Dubbo Con…

【K8S系列】深入解析Pod对象(一)

目录 序言 1.问题引入 1.1 问题描述 2 问题解答 2.1 pod 属性 2.1.1 NodeSelector 2.1.2 HostAliases 2.1.3 shareProcessNamespace 2.1.4 NodeName 2.1.5 其他pod属性 2.2 容器属性 2.2.1 ImagePullPolicy 2.2.2 Lifecycle 3 总结 4. 投票 序言 任何一件事情&am…

Zabbix代理服务器

Zabbix代理服务器一、部署 zabbix 代理服务器1、设置 zabbix 的下载源&#xff0c;按照 zabbix-proxy2、安装zabbix所需数据库3、添加数据库用户&#xff0c;以及 zabbix 所需的数据库信息4、导入数据库信息5、修改配置文件6、配置 agent 使用 proxy二、设置 zabbix-snmp 监控1…