项目重构实践

本文介绍了重构的定义,强调了重构的时机应在日常开发迭代中抓住,而非大规模的集中重构。重构的好处包括提高代码可读性和可维护性,遵循SOLID原则。实践中通过参数校验、业务抽象、远程服务模板和灰度控制四个案例详细阐述了重构方法,提倡使用java校验规范、抽象业务逻辑、消除重复代码和实现灰度控制。重构应确保单测覆盖,以防止业务逻辑错误。

重构的定义

我理解的重构是在不改变业务逻辑的基础上,修改代码实现,让其具有更好的可读性、可扩展性。要保证不改变业务逻辑是前提。有可能一次重构把正常的业务逻辑改坏了,搞成了线上故障。怎样保障这个前提呢?那就是充分的单测。有了单测的保证,采用小步快跑的方式,逐步重构。每完成一个小的修改,就运行一次单测。逐步重构可以让你快速发现问题,当一次单测失败了,你就知道是这次改动导致了bug。如果进行了多处修改,再运行单测,此时定位bug是哪处变更导致的,将花费更多的时间。这是逐步单测带来的好处,让你快速定位bug。有一句话叫做,无单测不重构。因为没有了单测的保障,你没有信心保证重构不会导致业务逻辑错误。但是,重构仅仅是为了让代码更优雅,不会带来直接的产品功能增量。甚至因为花时间重构导致占用了需求开发的时间,所以除了程序员之外其他角色往往不关心重构甚至抵制重构,这就导致了坚持重构的程序员往往成了孤勇者。

重构的时机

什么时候进行重构呢?有人认为重构与开发是冲突的。必须暂停所有进行中的需求迭代,然后投入一批人,基于某个版本进行代码重写。往往这样的重构往往是灾难性的,可以参考软件行业永远不要做的事这篇文章。恰恰相反,重构是一个可以融入日常开发迭代的事,每一次需求迭代都是重构的时机。当你面对项目的代码准备开发一个新的需求了,实现的过程中,你发现基于现有的实现很难实现本次业务需求,或者即使可以实现业务需求,但是要写很多恶心的定制代码,或者目前的代码实现刚好适用于某个设计模式的场景。使用设计模式改造后,更简洁,更容易理解,日后更好修改。当遇到上述几种场景的时候,代表着是时候进行重构了。

重构的好处

为什么要重构呢?作为写过几年代码的同学,你肯定看到过一团乱麻的代码。修改这种代码的感觉,有种如履薄冰的感觉。甚至达到了牵一发而动全身的地步。这种代码往往耦合度很高。单个类职责居多,往往既做了A事,又做了B事,还做了C事。当你想复用A逻辑的时候,你发现这段代码很难被单独执行,无法被单独复用。面向对象的语言带来的一个好处就是提高了代码的复用性,降低了开发人员的理解成本。一个组件的使用者不需要了解组件的实现细节,只需要拿到对象的引用就可以使用该对象提供的能力。当然,这种情况的前提是优良的设计。有一个普遍存在的误区,我使用了面向对象的语言,所以我写的代码就是面向对象的代码。我见过的很多开发项目,虽然使用了Java,但是本质上是面向过程编程的,属于事务脚本范畴,跟面向对象相差甚远。一个简单的检验标准是,代码实现是否使用到了接口、继承、多态、封装这些语言特征。很多实现都是业务逻辑的堆叠,缺乏合理的设计。业界有一个用于衡量软件质量的标准-SOLID原则,也就是设计原则。包括,开闭原则、单一职责原则、接口隔离、依赖倒置、里氏替换原则。符合这些标准的软件,软件质量肯定是高的。设计原则是目标,重构、设计模式、领域设计是具体的实现手段。

重构实践

说了这么多,都是比较虚的理论。在实际项目中怎么具体操作呢?下面就结合自己最近开发的项目,说下自己的理解。

项目背景

简单的讲就是目前业务功能依赖的服务A改成服务B。现状如图,后端提供了一批标准的服务,这些服务注册在网关中。基于接口定义有一批服务实现。本次项目会将业务领域层的服务实现进行重构,下面会介绍一下重构过程中的遇到的一些典型问题和自己的解决办法,提供给大家参考,期望达到抛砖引玉的作用。

现状

目标

参数校验

现有实现

我们在实现对外服务时,第一步往往要做参数校验。因为是对外提供的服务,调用方提供的入参五花八门,虽然会约定协议,但没法保证每个调用方都是严格按照协议传参。所以,对于入参的校验是不可或缺的。下面的参数校验是比较典型的实现方式,对必填参数就行校验,校验不通过就返回错误提示。如果这种校验逻辑比较少,是可以接受的。但是当我们提供了大量服务,每个服务都有这么一段逻辑。我们就要考虑下,是不是有统一的解决办法。

@Override
    public BaseResponse<Knowledge> getKnowledgeDetail(KnowledgeRequest request) {
        if (request == null) {
            return ResponseUtils.error(null, PaasResponseEnum.MISSING_PARAMETER, "Request");
        }
        if (StringUtils.isEmpty(request.getTenantId())) {
            return ResponseUtils.error(request.getRequestId(), PaasResponseEnum.MISSING_PARAMETER, "RobotCode");
        }
        if (request.getKnowledgeId() == null) {
            return ResponseUtils.error(request.getRequestId(), PaasResponseEnum.MISSING_PARAMETER, "KnowledgeId");
        }
        ...
     }   

解决方案

对于校验这块,java软件生态是有成熟的解决方案的。那就是java校验规范,validation-api。

jsr规范请求 JSR是Java Specification Requests的缩写,意思是Java 规范提案。 是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。

这套校验规范定义了,校验所需要的基本实体和接口。类似于servlet-api,委员会只定义规范,不提供实现。就像tomcat是基于servlet-api实现的servlet容器一样。hibernate-validator是对java参数校验规范的一种实现。

实践

在项目中引入如下依赖。如果是SpringBoot应用的话,SpringBoot检测到有hibernate-validator的依赖,就会进行自动装配,帮我们初始化好需要的bean。我们在业务代码中只要注入javax.validation.Validator实例就可以进行校验逻辑了。

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-validator</artifactId>
</dependenc

所以上面的校验实现可以实现为下面这样。

第一步声明字段应该满足什么约束。我们使用注解规定tenantId和knowledgeId必须非空,并且knowledgeId必须大于等于1。

class KnowledgeRequest {
    @NotNull
    private String tenantId;
    
    @NotNull(groups = {SingleKnowledgeGroup.class, AddSolutionGroup.class})
    @Min(groups = AddSolutionGroup.class, value = 1)
    private Long knowledgeId;
}

第二步调用框架进行校验。我们上面定义的规则叫做约束,约束是分组的。不

项⽬重构⽅案设计 最近接⼿到⼀个已经成型的项⽬,然后我们的任务就是对它进⾏重构,这个项⽬是⼀个功能很齐全的WPF视频播放器(附带很多其他功 能),在仔细研究了项⽬的背景和架构以后,初步做出了⼀下的重构⽅案: ⽬前现状: 虽然整个系统做得很漂亮,代码也写得不错,但仍有以下不⾜: 1. 架构有待改善。虽然看似MVC架构,却没有遵循MVC的模式,⾥⾯逻辑和UI耦合很⾼,没有清晰的规律。 2. 没有充分⽤到WPF的特性。WPF除了给我们很多炫丽的效果外,还给我们提供了诸如Binding,command等特性,这些特性可以帮我们隔开耦合, 同时减少代码量。 3. 代码和⽂件没有组织。代码、dll、样式⽂件和资源⽂件等没有统⼀的组织,到处都有,这样看起来就会很混乱。 4. 没有建⽴公⽤代码库。没有把公⽤的代码库独⽴出来,很多地⽅都是另外在写,这样既增加了代码量,同时维护和重构也带来了⿇烦。 5. 逻辑处理不应暴露在Client端。项⽬是⼀个C/S架构的系统,没有必要把所有的逻辑都暴露在Client端,应该⽤分布式把Logic放在服务器端,这样 可以更安全同时使客户端变⼩。 6. 没有单元测试。这样⼀个庞⼤的程序,没有单元测试是⾮常危险的,我们不可能做到100%的覆盖率,但是我们可以对主要的逻辑和Function做单 元测试,这样既减少了测试⼈员的⼯作量同时整个系统的安全、稳定和可维护性得到了⼤⼤的提⾼。 7. 性能不够优化。启动项⽬,通过WPF性能⼯具Perforator和Visual Profiler分析得出,程序启动和界⾯操作都导致CPU很⾼,内存也消耗⽐较多。 解决⽅案 1. 1. 针对缺陷1的"架构问题"。做法是采⽤MVP或者MVVM模式,⽬前正在对⽐和考虑。 2. 针对缺陷2的"WPF特性"。做法是充分利⽤Binding,command等特性。 3. 针对缺陷3的"代码和⽂件没有组织"。做法是建⽴⼀些单独的⼯程或者⽂件来分类和组织这些代码,并且充分隔离 耦合。 4. 针对缺陷4的"没有建⽴公⽤代码库"。做法是把⼀些公⽤的代码和常⽤的代码做成单独的Dll,并且有完整的单元测 试,这样才能提⾼效率。 5. 针对缺陷5的"逻辑处理不应暴露在Client端"。做法是⽤WCF做为中间层,把业务逻辑全部进⾏封装,通过WCF提 供统⼀的接⼝供项⽬调⽤。 6. 针对缺陷6的"没有单元测试"。做法是不管⽤MVP还是MVVM,我们起码保证对逻辑组件的代码有充分的单元测试 覆盖,同时对⼀些公⽤的组件也要有单独的单元测试代码。 7. 针对缺陷7的"性能不够优化"。这个我会单独做⼀个性能优化列表出来,针对耗资源的操作和其他有损害性能的操 作,我们应该避免。 8. 那么我们就可以结合实际情况搭建如下的结构 9. 10. 因为使⽤了MVVM模式,所以UI结构图就做如下调整 11. 12. 由于整个项⽬客户部希望我们引⽤第三⽅的组件或者⼯具,所以很多功能都只能通过企业库实现,⽐如AOP和 IOC,log和exception对项⽬特征做了定制化,数据访问通过企业库重写实现局部ORM,对性能要求⽐较⾼的应⽤仍 然实现存储过程。对所有事务操作都转移到数据库,邮件使⽤JOB进⾏发送。⼤型数据和客户要求较⾼的实时操 作,⽤MSMQ和SSB相结合的⽅式。层次依赖关系 UI: 功能模块使⽤时候,都会⾸先通过UI层次Security模块的安全验证(验证是通过Components模块⾥⾯的⾃定义的⽤于页⾯功能以及功能 点验证的控件触发), Security模块会通过服务层获取⽤户⾝份数据,⽤于页⾯验证. 功能模块的实际功能实现,如果需要数据库⽀持,那么依然会通过服务层进⾏数据操作.整个架构基于MVVM模式。 Service:通过WCF做中间服务,使应⽤隔离开来,这样有利于扩展和维护,同事提⾼了整个应⽤程序的伸缩性。 Business Logic: 服务层内部之间的组合关系,主要体现再依赖和调⽤,由上往下调⽤,逐级依赖,最后Service底层边界Data Access模块将调 ⽤Framework中的Data模块,Data模块将调⽤MS.EntLib3中的Data,向数据服务器发送数据操作命令和数据. Framework: 该层次提供许多基础的功能模块(七⼤块),分别提供给UI,Service层⾥⾯的模块直接或者间接的调⽤,同时也可以看到 Framework层次内部各模块之间再运⾏时也有互相依赖调⽤的关系存在.该层次的部分模块会依赖和调⽤Ms.EntLib3中的模块,⼀般是按 照两个层次⾥⾯的模块名称,产⽣关系的. MS.EntLib3: 该层次的各个模块是整个系统框架中最底层的,只会在运⾏时被更⾼层次的模块依赖和调⽤,同时该层次内部各个模块之间也 存在依赖和运⾏时调⽤关系.  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值