Java架构

还重构?就你那代码只能铲了重写!

2021-09-15  本文已影响0人  Java架构奶思

作者:小傅哥
原文: https://mp.weixin.qq.com/s/4xzd6mC2wKhXldrX50uk2w

一、前言

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

我说的,绝大多数码农没日没夜被需求憋着肝出来的代码,无论有多么的吭哧瘪肚,都不可能有重构,只有重新写。为什么?因为重新写所花的时间成本,远比重构一份已经烂成团的代码,要节省时间。但谁又不敢保证重写完的代码,就比之前能好多少,况且还要承担着重写后的代码事故风险和几乎体现不出来的 业务价值

虽然代码是给机器运行的,但同样也是给人看的,并且随着每次需求的迭代、变更、升级,都需要研发人员对同一份代码进行多次开发和上线,那么这里就会涉及到 可维护易扩展好交接 的特点。

而那些不合理分层实现代码逻辑、不写代码注释、不按规范提交、不做格式化、命名随意甚至把 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的其余修改,例如构建过程或辅助工具的变动

注释:类注释配置

/**

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());
}

3. 库表设计

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

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

0NF

1NF

image

2NF

3NF

反三范式

三大范式是设计数据库表结构的规则约束,但是在实际开发中允许局部变通:

  1. 有时候为了便于查询,会在如订单表冗余上当时用户的快照信息,比如用户下单时候的一些设置信息。
  2. 单列列表数据汇总到总表中一个数量值,便于查询的时候可以避免列表汇总操作。
  3. 可以在设计表的时候冗余一些字段,避免因业务发展情况多变,考虑不周导致该表繁琐的问题。

4. 算法逻辑

通常在我们实际的业务功能逻辑开发中,为了能满足一些高并发的场景,是不可能对数据库表上锁扣减库存、也不能直接for循环大量轮训操作的,通常需要考虑 在这样场景怎么去中心化以及降低时间复杂度。

秒杀:去中心化

算法:反面教材

@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));
}

其实很多算法包括:散列、倒排、负载等,都是可以用到很多实际的业务场景中的,包括:人群过滤、抽奖逻辑、数据路由等等方面,这些功能的使用可以降低时间复杂度,提升系统的性能,降低接口响应时常。

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) {
        // 封装自身业务逻辑
    }

}

类图

6. 逻辑缜密

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

实际上你的线上事故基本回围绕在:数据库连接和慢查询、服务器负载和宕机、异常逻辑兜底、接口幂等性、数据防重性、MQ消费速度、RPC响应时常、工具类使用错误等等。

下面举个例子:用户积分多支付,造成批量客诉。

7. 领域聚合

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

为什么呢,因为很多研发写出来的代码都不具有领域聚合的特点,当然这并不一定非得是在DDD的结构下,哪怕是在MVC的分层里,也一样可以写出很多好的聚合逻辑,把功能实现和业务的调用分离开。

8. 服务分层

如果你想让你的系统工程代码可以支撑绝对多数的业务需求,并且能沉淀下来可以服用的功能,那么基本你就需要在做代码开发实现的时候,抽离出技术组件、功能领域和业务逻辑这样几个分层,不要把频繁变化的业务逻辑写入到各个功能领域中,应该让功能领域更具有独立性,可以被业务层串联、编排、组合实现不同业务需求。这样你的功能领域才能被逐步沉淀下来,也更易于每次需求都 扩展。

9. 并发优化

在分布式场景开发系统,要尽可能运用上分布式的能力,从程序设计上尽可能的去避免一些集中的、分布式事物的、数据库加锁的,因为这些方式的使用都可能在某些极端情况下,造成系统的负载的超标,从而引发事故。

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();
    }
}

(size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16));

三、总结

上一篇下一篇

猜你喜欢

热点阅读