网易首页 > 网易号 > 正文 申请入驻

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

0
分享至

阿里妹导读

本文记录了作者从“代码优化”到“过度设计”的典型思考过程,这过程中涉及了很多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 == nullObjects.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@Getterpublic enum SaleTypeIntEnum{JX(1),// OTHERSprivate Integer code;

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

public enum SaleTypeStrEnum{JX,// OTHERSpublic 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 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();// OTHERSdefault:return null;

关系枚举构建

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



@Getter@AllArgsConstructorpublic enum SaleTypeRelEnum {// 不在分别定义两类变量,而是直接定义变量映射关系JX("JX", 1),// OTHERSprivate String fromCode;private Integer toCode;
private static Map 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{@Overridepublic Integer parse(SaleTypeParseContext saleTypeParseContext) {return SaleTypeIntEnum.JX.getCode();
* 调用上下文@Datapublic 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本身可以继续扩展内容和其他属性继续丰富参数,策略实现中也可以继续针对更多参数扩充逻辑。

策略工厂-手动容器

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



@Componentpublic static class SaleTypeParseStrategyContainer{public final static Map STRATEGY_MAP = new HashMap<>();
@PostConstructpublic 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();
@Componentpublic class JxSaleTypeParseStrategy implements SaleTypeParseStrategy{@Overridepublic Integer parse(SaleTypeParseContext saleTypeParseContext) {return SaleTypeIntEnum.JX.getCode();@Overridepublic SaleTypeStrEnum support() {return SaleTypeStrEnum.JX;
@Componentpublic static class SaleTypeParseStrategyContainer{public final static Map STRATEGY_MAP = new HashMap<>();@Autowiredprivate List parseStrategyList;@PostConstructpublic 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);
@Componentpublic class JxSaleTypeParseStrategy implements SaleTypeParseStrategy{@Overridepublic Integer parse(SaleTypeParseContext saleTypeParseContext) {return SaleTypeIntEnum.JX.getCode();@Overridepublic boolean support(SaleTypeParseContext saleTypeParseContext) {return SaleTypeStrEnum.JX.equals(saleTypeParseContext.getSaleTypeStr());
@Componentpublic static class SaleTypeParseStrategyContainer{@Autowiredprivate List 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走分布式调用思路;

  • 借助一些成熟的网关走服务路由的的调用思路;

就不再此再过多展开了。

总结

笔记向的内容帖子,用于活跃思维打开思路,没啥高科技~

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
米兰这一天,41岁胡歌撞上53岁胡兵,男神与男模的差距一目了然

米兰这一天,41岁胡歌撞上53岁胡兵,男神与男模的差距一目了然

白宸侃片
2024-06-19 16:28:34
武汉最新人事消息

武汉最新人事消息

林子说事
2024-06-20 11:00:06
4年1.895亿!西亚卡姆将顶薪续约步行者 今年率队打进东决

4年1.895亿!西亚卡姆将顶薪续约步行者 今年率队打进东决

醉卧浮生
2024-06-19 21:38:14
女孩子咪咪的这8个秘密,连女生都不知道

女孩子咪咪的这8个秘密,连女生都不知道

喜马拉雅主播暮霭
2024-06-20 08:51:43
北方人出生就懂,怎么南方人死活学不会?

北方人出生就懂,怎么南方人死活学不会?

风味人间
2024-06-19 12:42:54
俄罗斯的核威胁正在失去效力

俄罗斯的核威胁正在失去效力

不死好鸟
2024-06-19 23:52:35
缪昌文,重返江苏履新

缪昌文,重返江苏履新

鲁中晨报
2024-06-19 22:56:03
官方:因行为不当,欧足联对阿尔巴尼亚和塞尔维亚足协进行处罚

官方:因行为不当,欧足联对阿尔巴尼亚和塞尔维亚足协进行处罚

直播吧
2024-06-20 09:44:19
年产140亿份:全球最大方便面生产基地,在河北一个农业县

年产140亿份:全球最大方便面生产基地,在河北一个农业县

正解局
2024-06-19 12:41:52
胡锡进:得饶人处且饶人,但余琦自己造成的后果得慢慢承受

胡锡进:得饶人处且饶人,但余琦自己造成的后果得慢慢承受

映射生活的身影
2024-06-19 13:49:12
陈水扁天安门前留影,蔡英文参加汪辜会谈:台独分子多是变色龙

陈水扁天安门前留影,蔡英文参加汪辜会谈:台独分子多是变色龙

黄娜老师
2024-06-19 09:46:24
柔宇破产,一地鸡毛

柔宇破产,一地鸡毛

搜狐科技
2024-06-19 18:15:08
李娜:嫁给教练姜山,婚后不做饭不叫公婆,为何仍16年恩爱如初?

李娜:嫁给教练姜山,婚后不做饭不叫公婆,为何仍16年恩爱如初?

卡索
2024-06-20 09:20:05
张琳艳签约阿迪达斯:从小看贝叔穿着猎鹰踢球,今天我也穿上了

张琳艳签约阿迪达斯:从小看贝叔穿着猎鹰踢球,今天我也穿上了

直播吧
2024-06-20 09:12:13
连场策动进球!34岁克罗斯大师级斜塞,2大数据全场最高,获8.1分

连场策动进球!34岁克罗斯大师级斜塞,2大数据全场最高,获8.1分

我爱英超
2024-06-20 02:16:21
上交数院研究生力邀姜萍访问交大直播解题,自掏腰包报销一切费用

上交数院研究生力邀姜萍访问交大直播解题,自掏腰包报销一切费用

趣笔谈
2024-06-19 10:58:20
中国隐形的4大家族,每一个都“富可敌国”,你知道几个?

中国隐形的4大家族,每一个都“富可敌国”,你知道几个?

我不叫阿哏
2024-06-19 07:30:12
广东佛山,某银行以一名女客户的贷款到期未还为由,直接把该女子在该行办的另一张卡里的钱划走了

广东佛山,某银行以一名女客户的贷款到期未还为由,直接把该女子在该行办的另一张卡里的钱划走了

美人茶话会
2024-06-19 07:35:13
挺突然!一知名房企发布声明,将收回福州中加学校办学场所!

挺突然!一知名房企发布声明,将收回福州中加学校办学场所!

好房福州
2024-06-20 11:54:30
简直堪称变态:长沙中考的数学压轴题,把整个长沙都筐瓢了!

简直堪称变态:长沙中考的数学压轴题,把整个长沙都筐瓢了!

天气观察站
2024-06-20 01:21:43
2024-06-20 13:24:49
开源中国
开源中国
每天为开发者推送最新技术资讯
6335文章数 34226关注度
往期回顾 全部

科技要闻

苹果回应AI仅限iPhone15Pro:不是为卖新机

头条要闻

乌媒:乌军遭受一系列惨痛失败 乌军总司令或将被解职

头条要闻

乌媒:乌军遭受一系列惨痛失败 乌军总司令或将被解职

体育要闻

绿军的真老大,开始备战下赛季了

娱乐要闻

离谱!24岁女偶像参加涉毒男星生日聚会,坐在桌边陪赌

财经要闻

茅台大跌,谁的锅?

汽车要闻

售价11.79-14.39万元 新一代哈弗H6正式上市

态度原创

艺术
亲子
家居
游戏
房产

艺术要闻

穿越时空的艺术:《马可·波罗》AI沉浸影片探索人类文明

亲子要闻

女儿生妈妈气坐路边戳手指,被妈妈问到生谁气时,孩子低头指认,小手一指,委屈巴巴太可爱了

家居要闻

自然开放 实现灵动可变空间

WE助教Zoom放话,“Uzi疑似复出”秒登热搜;Faker再曝伤病隐患

房产要闻

海棠湾!一所重量级国际学校真的来了!

无障碍浏览 进入关怀版