好好的“代码优化”是怎么一步步变成“过度设计”的

阿里云开发者 发表于 4月以前  | 总阅读数:248 次

阿里妹导读

本文记录了作者从“代码优化”到“过度设计”的典型思考过程,这过程中涉及了很多Java的语法糖及设计模式的东西,很典型,能启发思考,遂记录下来。

有一天Review师妹的代码,看到一行很难看的代码,毕竟师妹刚开始转JAVA,一些书写小习惯还是要养成,所以锱铢必较还是有必要的,于是给出了一些优化思路的建议,以及为什么要这么做。建议完后,我并没有停下”追求极致“的脚步,随着不断的思考,发现这段代码的优化慢慢变得五花八门起来了,完成了一次“代码优化”到“过度设计”的典型思考过程,这过程中涉及了很多Java的语法糖及设计模式的东西,很典型,能启发思考,遂记录下来。

一切的开始

起初是一段很简单的代码,开始仅仅是将外域的一些标识符转换为域内的标识符。

public Integer parseSaleType(String saleTypeStr){
  if(saleTypeStr == null || saleTypeStr.equals("")){
    return null;
  }
  if(saleTypeStr.equals("JX")){
    return 1;
  }
  return null;
}

逻辑上很简单,实现的逻辑看上去也没啥大问题,基本学校的老师也是这么教的。

语法规范

但是嘛,不好看也容易犯错误,鸡蛋里挑骨头也得挑,于是给出了几个写代码的建议:

有函数式方法的尽量用

//saleTypeStr == null
Objects.isNull(saleTypeStr)

首先呢,虽然由于判断null这么写不会报错,但是按照常量写==前面的要求,应该倒过来写。另外,这种有JDK原生函数式的判断方法,还是优先使用函数式的写法,一来是有方法名比较直观,另外也是方便之后熟练使用Lamada,别写出 .filter(x -> null == x) 这样的写法,还是 .filter(Objects::isNull) 更可读些。

判断字符串为空不要自己写

容易漏逻辑,尽量使用现成的方法

//if(saleTypeStr == null || saleTypeStr.equals(""))
if(StringUtils.isBlank(saleTypeStr))

虽然原方法里无论判不判断空字符或者空格字符都不会影响最终方法的表征,但是从第一行想表达的判断“字符串是不是为空”这件事来看,这行并不能判断“空格字符”存在的情况,所以词不达意,另外也趁机强化记忆下isBlank和isEmpty的区别。

org.apache.commons.lang3里有很多工具类,方法比较成熟逻辑也比较完整。

同理org.apache.commons.collections4.CollectionUtils还有一堆集合操作的工具。

equals判定,常量写前面

//if(saleTypeStr.equals("JX"))
if("JX".equals(saleTypeStr))

虽然前面判断过null,所以这里并不会报空指针,但是但凡之后书写前面漏了,这里就开始报错了。

少用魔法值,定义常量

private static final String JX_SALE_TYPE_STR = "JX";
private static final Integer JX_SALE_TYPE_INT = 1;

但凡同一个魔法值在多处用,就怕漏改,所以收束定义在常量下,至少能保证全局引用的统一性。无状态方法,可选择定义为类静态

//public Integer parseSaleType(String saleTypeStr)
public static Integer parseSaleType(String saleTypeStr)

方法本身跟所在类的实例对象状态无关,且不会诱发线程安全问题,故符合被定义为static的条件,可先于对象创建在方法区,防止每个对象创建一次的时候,堆内存创建一次。

逻辑简化

语法的问题强调完,就得再琢磨琢磨这段逻辑需不需要这么多代码来表述了,乍眼一看没问题,但其实没必要写这么多。明确主体逻辑

判断入参有效性 -> 处理核心逻辑 -> 缺省返回,其实这个方法的构建思路是非常标准且合乎常理的,思考习惯很好,只是在这个简单的方法场景不免逻辑有些冗余。其实再看这个方法,最核心的逻辑就是把字符串对应到数字上,其他不命中的情况返回null就可以了,那么简化逻辑后,为空判定其实可以去掉,直接变为:

private static final String JX_SALE_TYPE_STR = "JX";
private static final Integer JX_SALE_TYPE_INT = 1;

public static Integer parseSaleType(String saleTypeStr){
  if(JX_SALE_TYPE_STR.equals(saleTypeStr)){
    return JX_SALE_TYPE_INT;
  }
  return null;
}

语法简化:三元运算符

再仔细看下场景有没有成熟的范式,【布尔式+返回值+非此即彼】,三元运算符可堪一用。

public static Integer parseSaleType(String saleTypeStr){
  return JX_SALE_TYPE_STR.equals(saleTypeStr) ? JX_SALE_TYPE_INT : null;
}

语法简化:Optional

这个场景范式也满足,【可能为空,有后续处理,有条件,有缺省值】,Optional也算完美契合。

public static Integer parseSaleType(String saleTypeStr){
  Optional.ofNullable(saleTypeStr).filter(JX_SALE_TYPE_STR::equals).map(o -> JX_SALE_TYPE_INT).orElse(null);
}

方法独立存在的必要性讨论

其实语法简化到三元运算符和Optional这一步,如果一个方法体内只有这一行,这个方法独立存在的必要性的就开始存疑了,如果所有的转换流程都能收束在工程中的某个环节上,且保证这个方法的引用仅存在一处,那么这一行代码其实放在主干代码上更好,防止来回跳转的代码阅读障碍,当然这也仅仅是在现状下的讨论,如果存在且不仅限于以下几种状况时还得独立出来:

  • 未来除了一种逻辑分支外,还会扩展其他分支,并且有被扩展的可能;
  • 虽然还是一种逻辑分支,但是判断的内容变长了,跟上下文和调用状态有关;
  • 虽然还是一种逻辑分支,但是逻辑总在调整;
  • 一处定义,多点引用;

继续拓展:定义枚举

“如无必要,勿增实体”假如这个传入的字符其实还有很多种,返回的映射也有很多种的时候,其实在这里继续写一堆常量定义就很不理智了。值枚举构建

考虑继续将入参的所有可能和出参的所有可能,可以构建为两组枚举值,这样所有的同簇常量就被放到一起了。

public enum SaleTypeStrEnum{
  JX,
  // OTHERS
  ;
}

@AllArgsConstructor
@Getter
public enum SaleTypeIntEnum{
  JX(1),
  // OTHERS
  ;
  private Integer code;
}

但是这个枚举功能并不完善,因为从入参映射为SaleTypeStrEnum,依然需要一段转换的逻辑,需要用到 SaleTypeStrEnum::name 来判定传参命中了哪个,所以这个逻辑不应该放在枚举外,继续补充:

public enum SaleTypeStrEnum{
  JX,
  // OTHERS 
  ;
  public static SaleTypeStrEnum getByName(String saleTypeStr){
    for (SaleTypeStrEnum value : SaleTypeStrEnum.values()) {
      if(value.name().equals(saleTypeStr)){
        return value;
      }
    }
    return null;
  }
}

方法有了,但是每次传进来值都要遍历整个枚举,O(n)效率太低了,还是老规矩,空间换时间。

public enum SaleTypeStrEnum{
  JX,
  // OTHERS
  ;

  /**
    * 预热转换关系到内存
    */
  private static Map<String, SaleTypeStrEnum> NAME_MAP = Arrays.stream(SaleTypeStrEnum.values()).collect(Collectors.toMap(SaleTypeStrEnum::name, Function.identity()));

  public static SaleTypeStrEnum getByName(String saleTypeStr){
    return NAME_MAP.get(saleTypeStr);
  }
}

这样每次检索就是O(1)了,那么最终方法体内也能使用switch管理原本的if-else

public static Integer parseSaleType(String saleTypeStr){
  switch(SaleTypeStrEnum.getByName(saleTypeStr)){
    case JX:return SaleTypeIntEnum.JX.getCode();
    // OTHERS
    default:return null;
  }
}

关系枚举构建

再仔细思考下,其实这里在描述的内容,无论是哪个枚举描述的内容都是同一件事物,方法本身就是描述两个不同编码的转换关系,且转换关系本身就是单向的,且映射路径极度简单,所以简单化一点,可以直接构建转换关系枚举。

@Getter
@AllArgsConstructor
public enum SaleTypeRelEnum {
  // 不在分别定义两类变量,而是直接定义变量映射关系
  JX("JX", 1),
  // OTHERS
  ;
  private String fromCode;
  private Integer toCode;

  private static Map<String, SaleTypeRelEnum> FROM_CODE_MAP = Arrays.stream(SaleTypeRelEnum.values()).collect(Collectors.toMap(SaleTypeRelEnum::getFromCode, Function.identity()));

  public static SaleTypeRelEnum get(String saleTypeStr){
    return FROM_CODE_MAP.get(saleTypeStr);
  }

  public static Integer parseCode(String saleTypeStr){
    return Optional.ofNullable(SaleTypeRelEnum.get(saleTypeStr)).map(SaleTypeRelEnum::getToCode).orElse(null);
  }
}

如果将转关系作为枚举,那么从职责上划分,转换这个动作应该是封闭在枚举内的固有行为,而不该暴露在外,故原来对方法的引用其实应该转为对关系枚举中 SaleTypeEnum::parseCode 方法的引用,O(1)检索且封闭性良好,同时支持更多简单单向映射关系的管理,要是以后出现的新场景都是这种关系,那够扛很久嘞。

继续拓展:设计模式

枚举的前提还是基于无状态前提,如果转换的的映射关系不再单纯,变得复杂,枚举的简单映射管理就不work了。

“万事不决,上设计模式”

哎~就是玩儿~策略模式-简单实现

首先,依然将传入的字符串作为路由依据,但是传入的内容为了防止有未来扩展,所以构造一个上下文,策略本身基于上下文来处理,借助上文定义的值枚举做策略路由。

/**
  * 定义策略接口
  */
public interface SaleTypeParseStrategy{
  Integer parse(SaleTypeParseContext saleTypeParseContext);
}

/**
  * 策略实现
  */
public class JxSaleTypeParseStrategy implements SaleTypeParseStrategy{
  @Override
  public Integer parse(SaleTypeParseContext saleTypeParseContext) {
    return SaleTypeIntEnum.JX.getCode();
  }
}

/**
  * 调用上下文
  */
@Data
public class SaleTypeParseContext{
  private SaleTypeStrEnum saleTypeStr;

  private SaleTypeParseStrategy parseStrategy;

  public Integer pasre(){
    return parseStrategy.parse(this);
  }
}

public static Integer parseSaleType(String saleTypeStr){
  SaleTypeStrEnum saleTypeEnum = SaleTypeStrEnum.getByName(saleTypeStr);
  SaleTypeParseContext context = new SaleTypeParseContext();
  context.setSaleTypeStr(saleTypeEnum);
  switch(saleTypeStr){
    // 策略路由
    case JX:context.setParseStrategy(new JxSaleTypeParseStrategy());break;
    // 继续扩展
    default:return null;
  }
  return context.parse();
}

当然,如果是这种没有上下文强依赖的策略,无论是静态单例还是Spring单例都会是一个不错的选择。SaleTypeParseContext本身可以继续扩展内容和其他属性继续丰富参数,策略实现中也可以继续针对更多参数扩充逻辑。策略工厂-手动容器

策略是个好东西,但是简单实现下,这里依然将策略实现的路由过程交给了调用方来做,那么每增加一种实现,调用点还要继续改,要是恰好有若干调用点就完犊子了,并不优雅,所以搞个中间层容器工厂,解耦一下依赖。

@Component
public static class SaleTypeParseStrategyContainer{
  public final static Map<SaleTypeStrEnum, SaleTypeParseStrategy> STRATEGY_MAP = new HashMap<>();

  @PostConstruct
  public void init(){
    STRATEGY_MAP.put(SaleTypeStrEnum.JX, new JxSaleTypeParseStrategy());
    // 继续拓展
  }

  public Integer parse(SaleTypeParseContext saleTypeParseContext){
    return Optional.ofNullable(STRATEGY_MAP.get(saleTypeParseContext.getSaleTypeStr())).map(strategy-> strategy.parse(saleTypeParseContext)).orElse(null);
  }
}

容器内手动创建各个策略的实现的单例后进行托管,那调用方只需要去构建上下文就好了,实际调用的方法更换为 SaleTypeParseStrategyContainer::parse,那后续无论策略如何丰富,调用方都不需要再感知这部分变化。后续出现了新的策略实现,则在工厂内继续追加路由表即可。

注册与发现&策略工厂-Spring容器

如果考虑到策略会依赖Spring的bean和其他有状态对象,那么这里也可以改成Spring的注入模式,同时继续将“支持哪种情况”由托管方容器移动至策略内部,改成由策略实现自身去注册到容器中。

public interface SaleTypeParseStrategy{
  Integer parse(SaleTypeParseContext saleTypeParseContext);
  // 所支持的情况
  SaleTypeStrEnum support();
}

@Component
public class JxSaleTypeParseStrategy implements SaleTypeParseStrategy{
  @Override
  public Integer parse(SaleTypeParseContext saleTypeParseContext) {
    return SaleTypeIntEnum.JX.getCode();
  }
  @Override
  public SaleTypeStrEnum support() {
    return SaleTypeStrEnum.JX;
  }
}

@Component
public static class SaleTypeParseStrategyContainer{
  public final static Map<SaleTypeStrEnum, SaleTypeParseStrategy> STRATEGY_MAP = new HashMap<>();
  @Autowired
  private List<SaleTypeParseStrategy> parseStrategyList;

  @PostConstruct
  public void init(){
    parseStrategyList.stream().forEach(strategy-> STRATEGY_MAP.put(strategy.support(), strategy));
  }
  public Integer parse(SaleTypeParseContext saleTypeParseContext){
    return Optional.ofNullable(STRATEGY_MAP.get(saleTypeParseContext.getSaleTypeStr())).map(strategy-> strategy.parse(saleTypeParseContext)).orElse(null);
  }
}

这样的话,连容器都不用改了,追加策略实现的改动只与当前策略有关,调用方和容器类都不需要感知了,但是缺点就在于如果有俩策略支持的情况相同,取到的是哪个就听天由命了~注册与发现&责任链

当然如果不能事先知道“支持哪种情况”,只能在运行时判断“是否支持”,将事前判定改为运行时判定,广义责任链会是一个不错的选择,把所有策略排成一排,谁举手说自己能处理就谁处理。

public interface SaleTypeParseStrategy{
  Integer parse(SaleTypeParseContext saleTypeParseContext);
  // 用于判断是否支持
  boolean support(SaleTypeParseContext saleTypeParseContext);
}

@Component
public class JxSaleTypeParseStrategy implements SaleTypeParseStrategy{
  @Override
  public Integer parse(SaleTypeParseContext saleTypeParseContext) {
    return SaleTypeIntEnum.JX.getCode();
  }
  @Override
  public boolean support(SaleTypeParseContext saleTypeParseContext) {
    return SaleTypeStrEnum.JX.equals(saleTypeParseContext.getSaleTypeStr());
  }
}

@Component
public static class SaleTypeParseStrategyContainer{
  @Autowired
  private List<SaleTypeParseStrategy> parseStrategyList;

  public Integer parse(SaleTypeParseContext saleTypeParseContext){
    return parseStrategyList.stream()
        .filter(strategy->strategy.support(saleTypeParseContext))
        .findAny()
        .map(strategy->strategy.parse(saleTypeParseContext))
        .orElse(null);
  }
}

这样的实现,依然可以将改动收束在策略本体上,修改相对集中,可以无耦地进行扩展。其他拓展

以上还只是在JAVA语言内去玩一些花样,在当前这种场景下肯定是有过度设计的嫌疑,7行代码可以缩到1行,也可以扩充到70行,所以说嘛:“用代码行数来考量一个程序员是不太合适滴!~”当然了,也还可以继续借助其他的中间件搞花样,包括但不限于:- 植入Diamond走走动态配置开关的思路;

  • 植入QLExpress搞搞逻辑表达式的思路;
  • 把策略实现改成HsfProvider走分布式调用思路;
  • 借助一些成熟的网关走服务路由的的调用思路;

就不再此再过多展开了。

本文由微信公众号阿里云开发者原创,哈喽比特收录。
文章来源:https://mp.weixin.qq.com/s/Vt_mdLicWwkZ8phD1rH6UQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:8月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:8月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:8月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:8月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:8月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:8月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:8月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:8月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:8月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:8月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:8月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:8月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:8月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:8月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:8月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:8月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:8月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:8月以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  236889次阅读
vscode超好用的代码书签插件Bookmarks 1年以前  |  6992次阅读
 目录