一、背景
作为程序猿,工程质量是我们逃不开的一个话题,工程质量高带来的好处多多,我在写这篇文章的时候问了一下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个阶段,而我们研发重点关注开发、上线和运维三个阶段。
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<VipXstoreStore> storePage = storeRepository.listByExample(storeQuery);return StoreConverter.toStoreQueryResp(storePage);}
要想让我们的代码具备可读性、可复用性等,那么我们首先要对工程的代码进行分层,而且每层要有严格要求和规范,每层由多个模块组成,每层有自己独立的职责,多个层次协同提供完整的功能,带来的好处是:
- 高内聚:分层的设计可以简化系统设计,让不同的层专注做某一模块的事
- 低耦合:层与层之间通过接口或API来交互,依赖方不用知道被依赖方的细节
- 复用:分层之后可以做到更多的复用
- 扩展性:分层架构可以让我们更容易做横向扩展
4.4.1 传统分层
4.4.2 现有常用分层
随着软件复杂度越来越高,所以分层也越来越细,目前大部分都采用四层分层结构,具体如下:
4.5 写好代码的最佳路径
我相信每个工程师都想写出高质量的代码,不想一直写没有成长、被人吐槽的烂代码。要写好代码,最佳的路径是学习、反复练习、多总结,养成良好的习惯非常重要,我先给大家讲一个小故事,前一段时间看了一本书叫《优势成长》,里面有一章节讲到了新东方,我们都知道新东方的老师非常牛,那么他们是如何从一个普通老师变成大家都喜欢的老师,其核心就是正确的姿势重复做,在新东方有一个试讲环节,应聘老师得对着台下那些老教师们去讲,这些老师也不好好听,干什么的都有,当讲着讲着,他们还会不断打断你,然后告诉你前面40分钟讲的都不好,重来一遍。于是1个小时的课,你得反复讲,反复调整,有时候一节课,要被老师们批评半年时间,打磨半年之后才真正的讲好一堂课。其实在这个过程非常的煎熬,中途很多应聘的老师被气哭、被骂走了,一开始100人最后剩下20人都不到了。在新东方他们内部有句口号,通过把自己逼疯从而把对手逼死。所以我们看到这些新东方的老师各个出口成章,能言会道,都是在极端苛刻的条件下训练而成。经过千百次的刻意练习后形成肌肉记忆,从知道,再到熟练,进而精通。所以我们很多人都说要写好代码,知道高内聚低耦合,并不是我们讲讲口号就行,这中间还差了千百次的刻意练习才能转化成你的技能,所以学习、反复实践并总结才能成为一个代码技术达人。
4.5.1 抽像能力
抽象思维是我们工程师最重要的思维能力,因为软件技术本质上就是一门抽象的艺术。我们工程师每天都要动用抽象思维,对问题域进行分析、归纳、综合、判断、推理,从而抽象出各种概念,挖掘概念和概念之间的关系,然后通过编程语言实现业务功能,所以,我们大部分的时间并不是在写代码,而是在梳理需求,理清概念,对需求有一个全局的认知。而抽像能力让我及团队切身感受到,它给我们在编码和设计上带来的质的变化。
其实导出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) {//循环获取数据@Overridepublic 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 {@Overridepublic 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) {@Overridepublic 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 { @Overridepublic BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException {//业务逻辑实现return BizNotificationStatus.PROCESS_SUCCESS;}
}
//占用库存
public class TransferOccupyInventoryHandler implements BizNotificationHandler {@Overridepublic BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException {//业务实现}
}//在GPDC生成新条码
public class GpdcGenerateNewBarcodeHandler implements BizNotificationHandler {@Overridepublic BizNotificationStatus handleNotification(BizNotification bizNotification) throws OspException {//业务代码实现}
}
其实我们在与其它系统交互的时候,我们可以把每一个交互动作抽像成一个通知事件,每次交互的时候,写一个事件通知事件即可。
4.5.2 组合/聚合复用原则
关于组合/聚合复用原则,其实我们在项目过程会经常遇到,比如项目里会经常管理各种单据,像采购单、调拨单、收货单等,而对于每种单据都会有各种各样的较验,我们先来看一段建调拨单代码,具体如何下:
//接口定义
public interface TransferValidator {boolean validator(CreateTransferCtx ctx) throws OspException;
}
//接口实现1
public class W2sCrossPoQtyValidator implements TransferValidator {@Overridepublic boolean validator(CreateTransferCtx ctx) throws OspException {//较验器代码实现}
//接口实现2
public class W2sStoreBarcodeSaleLimitValidator implements TransferValidator {@Overridepublic 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 {@Overridepublic boolean createOrder(CreateTransferCtx ctx) throws OspException {validator(ctx)//实现业务逻辑}protected abstract boolean validator(CreateTransferCtx ctx) throws OspException;}
//店仓调拨单
public class S2wRefundCreator extends DefaultTransferCreator {//较验器自由组装@Overrideprotected boolean validator(CreateTransferCtx ctx) throws OspException {return transferValidators.newChain().qtyValidator().transferRouteCfgValidator().prodValidator().validator(ctx);}
}
通过上面的示例,其实抽像并不难,难的是我们要花时间去思考,去理解,只有自己花足够的多时间,反复训练我相信比较容易做到,最近在两个新项目,我们团队的部分成员反馈做梦都在想如何实现更合理。
五 、研发过程管理中善用工具并数字化
研发过程我们不刚要写好技术方案、写好代码,但仅做好这两点还不够,我们需要善用于一些工具,将过程管理数字化,我们平常工作中很多人只管理写好代码,但代码是否符合预期,认为是测试的事情,这个观点是错误的,所以一定要善用工具sonar、测试覆盖率、流量回放等工具,将过程管理数字化,同时要与我们的合作伙伴产品、测试、研发形成良好的互动,如果有条件的话跟我们的业务做一些沟通也是非常有必要的。
六 、总结
要想让我们的系统能够平稳运行,我们需要在抓好人、工具、预案、目标这几个点,只有抓好这些点,我们才能真正构建高性能、高可用、可降级的业务系统。