关于java:还重构就你那代码只能铲了重写

13次阅读

共计 8631 个字符,预计需要花费 22 分钟才能阅读完成。

作者:小傅哥
博客:https://bugstack.cn

一、前言

咱们不一样,就你没对象! 对,你是面向过程编程的!

我说的,绝大多数码农没日没夜被需要憋着肝进去的代码,无论有如许的吭哧瘪肚,都不可能有重构,只有从新写。为什么?因为从新写所花的工夫老本,远比重构一份曾经烂成团的代码,要节省时间。但谁又不敢保障重写完的代码,就比之前能好多少,况且还要承当着重写后的代码事变危险和简直体现不进去的 业务价值

尽管代码是给机器运行的,但同样也是给人看的,并且随着每次需要的迭代、变更、降级,都须要研发人员对同一份代码进行屡次开发和上线,那么这里就会波及到 可保护 易扩大 好交接 的特点。

而那些不合理分层实现代码逻辑、不写代码正文、不按标准提交、不做格式化、命名随便甚至把 queryBatch 写成 queryBitch 的,都会造成后续代码没法重构的问题。那么接下来咱们就别离介绍下,开发好能重构的代码,都要怎么干!

二、代码优化

1. 约定标准

# 提交:次要 type
feat:     减少新性能
fix:      修复 bug

# 提交:非凡 type
docs:     只改变了文档相干的内容
style:    不影响代码含意的改变,例如去掉空格、扭转缩进、增删分号
build:    结构工具的或者内部依赖的改变,例如 webpack,npm
refactor: 代码重构时应用
revert:   执行 git revert 打印的 message

# 提交:暂不应用 type
test:     增加测试或者批改现有测试
perf:     进步性能的改变
ci:       与 CI(继续集成服务)无关的改变
chore:    不批改 src 或者 test 的其余批改,例如构建过程或辅助工具的变动

# 正文:类正文配置
/**
* @description: 
* @author: ${USER}
* @date: ${DATE}
*/
  • 分支 :开发前提前约定好拉分支的标准,比方 日期_用户_用处,210905_xfg_updateRuleLogic
  • 提交 作者,type: desc 如:小傅哥,fix:更新规定逻辑问题 参考 Commit message 标准
  • 正文:包含类正文、办法正文、属性正文,在 IDEA 中能够设置类正文的头信息 Editor -> File and Code Templates -> File Header 举荐下载安装 IDEA P3C 插件 Alibaba Java Coding Guidelines,对立标准化编码方式。

2. 接口标准

在编写 RPC 接口的时候,返回的后果中肯定要蕴含明确的 Code 码Info 形容,否则应用方很难晓得这个接口是否调用胜利还是异样,以及是什么状况的异样。

定义 Result

public class Result implements java.io.Serializable {

    private static final long serialVersionUID = 752386055478765987L;

    /** 返回后果码 */
    private String code;

    /** 返回后果信息 */
    private String info;

    public Result() {}

    public Result(String code, String info) {
        this.code = code;
        this.info = info;
    }

    public static Result buildSuccessResult() {Result result = new Result();
        result.setCode(Constants.ResponseCode.SUCCESS.getCode());
        result.setInfo(Constants.ResponseCode.SUCCESS.getInfo());
        return result;
    }
    
    // ...get/set
}

返回后果包装:继承

public class RuleResult extends Result {

    private String ruleId;
    private String ruleDesc;

    public RuleResult(String code, String info) {super(code, info);
    }
    
    // ...get/set
}

// 应用
public RuleResult execRule(DecisionMatter request) {return new RuleResult(Constants.ResponseCode.SUCCESS.getCode(), Constants.ResponseCode.SUCCESS.getInfo());
}

返回后果包装:泛型

public class ResultData<T> implements Serializable {

    private Result result;
    private T data;

    public ResultData(Result result, T data) {
        this.result = result;
        this.data = data;
    }   
    
    // ...get/set
}  

// 应用
public ResultData<Rule> execRule(DecisionMatter request) {return new ResultData<Rule>(Result.buildSuccessResult(), new Rule());
}
  • 两种接口返回后果的包装定义,都能够标准返回后果。在这样的形式包装后,应用方就能够用对立的形式来判断 Code 码 并做出相应的解决。

3. 库表设计

三范式:是数据库的规范化的内容,所谓的数据库三范式艰深的讲就是设计数据库表所应该恪守的一套标准,如果不恪守就会造成设计的数据库不标准,呈现数据库字段冗余,数据的查问,插入等操作等问题。

数据库不仅仅只有三范式(1NF/2NF/3NF),还有 BCNF、4NF、5NF…,不过在理论的数据库设计时,恪守前三个范式就足够了。再向下就会造成设计的数据库产生过多不必要的束缚。

0NF

  • 第零范式是指没有应用任何范式,数据寄存冗余大量表字段,而且这样的表构造十分难以保护。

1NF

  • 第一范式是在第零范式冗余字段上的改良,把反复字段抽离进去,设计成一个冗余数据较少便于存储和读取的表构造。
  • 同时在第一范式中也指出,表中的所有字段都应该是原子的、不可再宰割的,例如:你不能把公司雇员表的部门名称和职责寄存到一个字段。须要确保每列放弃原子性

2NF

  • 满足 1NF 后,要求表中的列,都必须依赖主键,确保每个列都和主键列之间分割,而不能间接分割,也就是一个表只能形容一件事件。须要确保表中的每列都和主键相干。

3NF

  • 不能存在依赖关系,学号、姓名,到院系,院系到宿舍,须要确保每列都和主键列间接相干, 而不是间接相干。

反三范式

三大范式是设计数据库表构造的规定束缚,然而在理论开发中容许部分变通:

  1. 有时候为了便于查问,会在如订单表冗余上过后用户的快照信息,比方用户下单时候的一些设置信息。
  2. 单列列表数据汇总到总表中一个数量值,便于查问的时候能够防止列表汇总操作。
  3. 能够在设计表的时候冗余一些字段,防止因业务倒退状况多变,考虑不周导致该表繁琐的问题。

4. 算法逻辑

通常在咱们理论的业务性能逻辑开发中,为了能满足一些高并发的场景,是不可能对数据库表上锁扣减库存、也不能间接 for 循环大量轮训操作的,通常须要思考🤔在这样场景怎么去中心化以及升高工夫复杂度。

秒杀:去中心化

  • 背景 :这个一个商品流动秒杀的实现计划,最开始的设计是基于一个流动号 ID 进行锁定,秒杀时锁定这个 ID,用户购买完后就进行开释。但在大量用户抢购时,呈现了秒杀分布式 独占锁 后的业务逻辑解决中产生异样,开释锁失败。导致所有的用户都不能再拿到锁,也就造成了有商品但不能下单的问题。
  • 优化:优化独占竞态为分段动态,将流动 ID+ 库存编号作为动静锁标识。以后秒杀的用户如果产生锁失败那么前面的用户能够持续秒杀不受影响。而失败的锁会有 worker 进行弥补复原,那么最终会防止超卖以及不能售卖。

算法:反面教材

@Test
public void test_idx_hashMap() {Map<String, String> map = new HashMap<>(64);
    map.put("alderney", "未实现服务");
    map.put("luminance", "未实现服务");
    map.put("chorology", "未实现服务");
    map.put("carline", "未实现服务");
    map.put("fluorosis", "未实现服务");
    map.put("angora", "未实现服务");
    map.put("insititious", "未实现服务");
    map.put("insincere", "已实现服务");
    
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {map.get("insincere");
    }
    System.out.println("耗时(initialCapacity):" + (System.currentTimeMillis() - startTime));
}
  • 背景 :HashMap 数据获取工夫复杂度在 O(1) -> O(logn) -> O(n),但通过 非凡 操作,能够把这个工夫复杂度,拉到 O(n)
  • 操作 :这是一个定义HashMap 寄存业务实现 key,通过 key 调用服务的性能。但这里的 key,只有insincere 有用,其余的都是未实现服务。那你看到有啥问题了吗?

    • 这点代码乍一看没什么问题,看明确了就是代码里下砒霜!它的目标就一个,要让所有的 key 成一个链表放到 HashMap 中,而且把有用的 key 放到链表的最初,减少 get 时的耗时!
    • 首先,new HashMap<>(64);为啥默认初始化 64 个长度?因为默认长度是 8,插入元素时,当链表长度为 8 时候会进行扩容和链表树化判断,此时就会把原有的 key 散列了,不能让所有 key 形成一个工夫复杂度较高的链表。
    • 其次,所有的 key 都是刻意选出来的,因为他们在 HashMap 计算下标时,下标值都为 0,idx = (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16)),这样就能让所有 key 都散列到同一个地位进行碰撞。而且单词 insincere 的意思是;不恳切的、不真挚的
    • 最初,前 7 个 key 其实都是废 key,不起任何作用,只有最初一个 key 有服务。那么这样就能够在 HashMap 中建进去很多这样耗时的碰撞链表,当然要满足 0.75 的负载因子,不要让 HashMap 扩容。

其实很多算法包含:散列、倒排、负载等,都是能够用到很多理论的业务场景中的,包含:人群过滤、抽奖逻辑、数据路由等等方面,这些性能的应用能够升高工夫复杂度,晋升零碎的性能,升高接口响应时常。

5. 职责拆散

为了能够让程序的逻辑实现更具备扩展性,通常咱们都须要应用设计模式来解决各个场景的代码实现构造。而设计模式的应用在代码开发中的体现也次要为接口的定义、抽象类的包装和继承类的实现。通过这样的形式来隔离各个性能畛域的开发,以此保障每次需要扩大时能够更加灵便的增加,而不至于让代码因需要迭代而变得更加凌乱。

案例

public interface IRuleExec {void doRuleExec(String req);

}

public class RuleConfig {protected Map<String, String> configGroup = new ConcurrentHashMap<>();

    static {// ...}

}

public class RuleDataSupport extends RuleConfig{protected String queryRuleConfig(String ruleId){return "xxx";}

}

public abstract class AbstractRuleBase extends RuleDataSupport implements IRuleExec{

    @Override
    public void doRuleExec(String req) {
        // 1. 查问配置
        String ruleConfig = super.queryRuleConfig("10001");

        // 2. 校验信息
        checkRuleConfig(ruleConfig);

        // 3. 执行规定{含业务逻辑,交给业务本人解决}
        this.doLogic(configGroup.get(ruleConfig));
    }

    /**
     * 执行规定{含业务逻辑,交给业务本人解决}
     */
    protected abstract void doLogic(String req);

    private void checkRuleConfig(String ruleConfig) {// ... 校验配置}

}

public class RuleExec extends AbstractRuleBase {

    @Override
    protected void doLogic(String req) {// 封装本身业务逻辑}

}

类图

  • 这是一种模版模式构造的定义,应用到了接口实现、抽象类继承,同时能够看到在 AbstractRuleBase 抽象类中,是负责实现整个逻辑调用的定义,并且这个抽象类把一些通用的配置和数据应用独自隔离进来,而专用的简略办法放到本身实现,最初是对于形象办法的定义和调用,而业务类 RuleExec 就能够按需实现本人的逻辑性能了。

6. 逻辑周密

你的代码出过线上事变吗?为什么出的事变,是树上有十只鸟开一枪还剩几只的问题吗?比方:枪是无声的吗、鸟聋吗、有怀孕的吗、有绑在树上的鸟吗、边上的树还有鸟吗、鸟胆怯枪声吗、有残疾的鸟吗、打鸟的人眼睛花不花,… …

实际上你的线上事变根本回围绕在:数据库连贯和慢查问、服务器负载和宕机、异样逻辑兜底、接口幂等性、数据防重性、MQ 生产速度、RPC 响应时常、工具类应用谬误等等。

上面举个例子:用户积分多领取,造成批量客诉。

  • 背景:这个产品性能的背景可能很大一部分研发都参加开发过,简略说就是满足用户应用积分抽奖的一个需要。上图左侧就是研发最开始设计的流程,通过 RPC 接口扣减用户积分,扣减胜利后进行抽奖。但因为当天 RPC 服务不稳固,造成 RPC 理论调用胜利,但返回超时失败。而调用 RPC 接口的 uuid 是每次主动生成的,不具备调用幂等性。所以造成了用户积分多领取景象。
  • 解决:事变后批改抽奖流程,学生成待抽奖的抽奖单,由抽奖单 ID 调用 RPC 接口,保障接口幂等性。在 RPC 接口失败时由定时工作弥补的形式执行抽奖。流程整改后发现,弥补工作每周产生 1~3 次,那么也就是证实了 RPC 接口的确有可用率问题,同时也阐明很久之前就有流程问题,但因为用户客诉较少,所以没有反馈。

7. 畛域聚合

不够形象、不能写死、不好扩大,是不是总是你的代码,每次都像一锤子买卖,齐全是写死的、绑定的,基本没有一点缝隙让新的需要扩大进去。

为什么呢,因为很多研发写进去的代码都不具备畛域聚合的特点,当然这并不一定非得是在 DDD 的构造下,哪怕是在 MVC 的分层里,也一样能够写出很多好的聚合逻辑,把性能实现和业务的调用分来到。

  • 依附畛域驱动设计的设计思维,通过事件风暴建设畛域模型,正当划分畛域逻辑和物理边界,建设畛域对象及服务矩阵和服务架构图,定义合乎 DDD 分层架构思维的代码构造模型,保障业务模型与代码模型的一致性。通过上述设计思维、办法和过程,领导团队依照 DDD 设计思维实现微服务设计和开发。

    • 回绝泥球小单体、回绝净化性能与服务、回绝一加性能排期一个月
    • 架构出高可用极易合乎互联网高速迭代的应用服务
    • 物料化、组装化、可编排的服务,进步人效

8. 服务分层

如果你想让你的系统工程代码能够撑持绝对多数的业务需要,并且能积淀下来能够服用的性能,那么根本你就须要在做代码开发实现的时候,抽离出技术组件、性能畛域和业务逻辑这样几个分层,不要把频繁变动的业务逻辑写入到各个性能畛域中,应该让性能畛域更具备独立性,能够被业务层串联、编排、组合实现不同业务需要。这样你的性能畛域能力被逐渐积淀下来,也更易于每次需要都 扩大。

  • 这是一个简化的分层逻辑构造,有聚合的畛域、SDK 组件、中间件和代码编排,并提供一些通用共性凝练出的服务治理性能。通过这样的分层和各个层级的实现形式,就能够更加灵便的承接需要了。

9. 并发优化

在分布式场景开发零碎,要尽可能使用上分布式的能力,从程序设计上尽可能的去防止一些集中的、分布式事物的、数据库加锁的,因为这些形式的应用都可能在某些极其状况下,造成零碎的负载的超标,从而引发事变。

  • 所以通常状况下更须要做去集中化解决,应用 MQ 打消峰,升高耦合,让数据能够最终一致性,也更要思考在 Redis 下的应用,缩小对数据库的大量锁解决。
  • 正当的使用 MQ、RPC、分布式工作、Redis、分库分表以及分布式事务只有这样的操作你才可能让本人的程序代码能够撑持起更大的业务体量。

10. 源码能力

你有理解过 HashMap 的拉链寻址数据结构吗、晓得哈希散列和扰动函数吗、懂得怎么联合 Spring 动静切换数据源吗、AOP 是怎么实现以及应用的、MyBatis 是怎么和 Spring 联合交管 Bean 对象的,等等。看似都是些面试的八股文,但在理论的开发中其实是能够解决很多问题的。

@Around("aopPoint() && @annotation(dbRouter)")
public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {String dbKey = dbRouter.key();
    if (StringUtils.isBlank(dbKey)) throw new RuntimeException("annotation DBRouter key is null!");

    // 计算路由
    String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
    int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();

    // 扰动函数
    int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));

    // 库表索引
    int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
    int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);   

    // 设置到 ThreadLocal
    DBContextHolder.setDBKey(String.format("%02d", dbIdx));
    DBContextHolder.setTBKey(String.format("%02d", tbIdx));
    logger.info("数据库路由 method:{} dbIdx:{} tbIdx:{}", getMethod(jp).getName(), dbIdx, tbIdx);
   
    // 返回后果
    try {return jp.proceed();
    } finally {DBContextHolder.clearDBKey();
        DBContextHolder.clearTBKey();}
}
  • 这是 HashMap 哈希桶数组 + 链表 + 红黑树的数据结构,通过扰动函数 (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16)); 解决数据碰撞重大的问题。
  • 但其实这样的散列算法、寻址形式都能够使用到数据库路由的设计实现中,还有整个数组 + 链表的形式其实库 + 表的形式也有类似之处。
  • 数据库路由简化的外围逻辑实现代码如上,首先咱们提取了库表乘积的数量,把它当成 HashMap 一样的长度进行应用。
  • 当 idx 计算完总长度上的一个索引地位后,还须要把这个地位折算到库表中,看看总体长度的索引因为落到哪个库哪个表。
  • 最初是把这个计算的索引信息寄存到 ThreadLocal 中,用于传递在办法调用过程中能够提取到索引信息。

三、总结

  • 讲道理,你简直不太可能把一堆曾经烂的不行的代码,通过重构的形式把他解决洁净。细了说,你要扭转代码构造分层、属性对象整合、调用逻辑封装,但任何一步的操作都可能会对原有的接口定义和调用造成危险影响,而且内部现有调用你的接口还须要随着你的改变而降级,可能你会想着在包装一层,但这一层包装仍须要较大的工夫老本和简直没有价值的适配。
  • 所以咱们在理论开发中,如果能让这些代码具备重构的可能,简直就是要实时重构,每当你在增加新的性能、新的逻辑、修复异样时,就要思考是否能够通过代码构造、实现形式、设计模式等伎俩的应用,扭转不合理的性能实现。每一次,一点的优化和扭转,也不会有那么难。
  • 当你在接需要的时候,认真思考承接这样的业务诉求,都须要建设怎么的数据结构、算法逻辑、设计模式、畛域聚合、服务编排、零碎架构等,能力更正当的搭建出良好的具备易保护、可扩大的零碎服务。如果你对这些还没有什么感觉,能够浏览设计模式和手写 Spring,这些内容能够帮忙你晋升不少的编程逻辑设计。

四、系列举荐

  • 握草,你居然在代码里下毒!
  • 一次代码评审,差点过不了试用期!
  • 谁说今天上线,这货压根不晓得开发流程!
  • 带头撸我的项目,《DDD + RPC 开发分布式架构,抽奖零碎》
  • 调研字节码插桩技术,用于系统监控设计和实现
正文完
 0