关于重构:重构改善既有代码的设计

最近正在重构我的项目,并且正在看《重构》,在实际的同时总结了一些点,或者能给你一些重构或者写代码上的一些思考。 我始终认为代码构造是一个因人而异的事件,很多时候咱们其实判断一个代码的好坏往往是通过主观判断,比方同样是实现一个性能,100 行的代码并非肯定比 50 行的差;咱们没有一个正当的标杆去评判。 然而,最近我的想法变了,发现有些代码肯定是毒药,早点发现他们,往往会对于咱们当前需要的批改有莫大的帮忙。 命名如果把整个我的项目代码比作是屋宇建造,命名就是砖头,命名的好坏间接决定了你代码 50% 的可读性。绝大部分的状况下,读者应该能够通过你函数的命名,间接理解到你这个函数的性能。要求命名要形容具体用意而非含糊的操作性能命名不要呈现技术名词整个我的项目对立命名命名要形容具体用意不好的比方:process、modify...通常咱们须要应用一个动词+宾语,比方 modifyUsername,processFile而一个类(对象)的命名,通常应用名词 性能命名不要呈现技术名词不好的比方:UserRoleMap显然 Map 是一个技术名词,而这个对象的类型其实往往曾经能够分明的标识这个是个 map 类型。这里想要示意的是一个用户角色名称的映射关系,集体习惯会通常命名为: UserRoleMapping 了解为映射关系 整个我的项目对立命名最好在我的项目建设数据库的时候就对立命名,特地是针对一些专有名词的命名,能够建设一个表格。 并且很多英语单词的十分讲究的,小技巧:当你应用翻译软件翻译成一个英文单词之后,将这个英文单词再放到搜索引擎外面再去搜一遍,或者搜寻对应图片,就能晓得这个单词是否真的是你想要的函数管制函数长度书本上有一句话常常被人提到就是:写的代码越少,bug 越少,所以要缩小函数长度这句话我是不认可的,有的时候代码极具的缩小,可能会带来一些意外的操作,因为长度的缩小有些时候并不是 等值替换 特地是 python 这种常常能够一行搞定的状况。 然而我认可要管制函数长度,函数长度越短,性能点越集中,浏览代码速度越快。很多函数我看一眼命名就晓得要实现的性能是什么,而后测试的时候,只有输入没问题,则这个函数就能够间接跳过不看,如果函数长,那么我必须一行行的去看到底是哪一个中央呈现了问题。 不同的语言不同,函数长度管制限度不同,比方 python 往往就会短一些,java 就会长一些,因为 golang 常常还会写一些 if err != nil 会更加拖长一些。 PS:集体个别会尽量管制在 35 以内,但未严格执行 lint。 不足封装管制函数长度之后很容易导致的一个问题就是不足封装。你必定会奇怪了,我都把原来一个 200 行函数拆成 5 个函数了,为什么你还说不足封装呢?案例: function test() { a = testA() b = a.testB() c = b.testC() ... // 或者写为 c = a.testB().testC()}我也常常会写这样的代码,然而其实暗藏一些细节,才是封装的精华。提供一个常常在重构应用的思路: 将一个函数分为三段:前置条件查看,根本逻辑解决,后置返回值解决我往往将一个函数分好之后,就会发现,函数中的几个调用尽管起源不同,然而都是在做同一个事件,职责雷同,暗藏其中的细节会对函数有更好的封装。 管制函数参数长度之前在 java 的 阿里标准外面提到 函数的参数数量的管制,超过肯定数量就须要封装成一个类,这个没有问题,很多人也都能做到。然而,千万不要成心把所有的参数封装为一个对象,特地是业务属性原本就是不同的,有的时候封装成两个对象会更加复用或更加满足职责繁多的要求。 ...

August 18, 2023 · 1 min · jiezi

关于重构:减少80存储风控名单服务重构剖析

引言小小的 Redis 大大的不简略,本文将联合风控名单服务在应用 Redis 存储数据时的数据结构设计及优化,并详细分析 redis 底层实现对数据结构选型的重要性。 背景先来交代下应用场景,在风控场景下,名单服务每时每刻都须要接受海量数据查问。 名单检索内容波及维度十分广:用户业务标识(UID)、手机号、身份证号、设施号、IMEI(International Mobile Equipment Identity, 国内挪动设施识别码)、Wifi Mac、IP 等等。用户的一次业务申请,在风控的中会扩散到多个名单维度,同时还须要在 RT(Response-time) 上满足业务场景诉求。 这就导致名单服务的构建须要接受住如下挑战: 海量数据存储:维度多,存储内容尚可(是否命中),依照 X 个用户,Y 个维度,Z 个业务线(隔离),量级十分大大流量、高并发:业务场景下任何存在危险敞口的点都须要评估过风控,每天决策峰值 TPS 过万极低耗时:留给名单服务的工夫不多了,如果整体业务零碎给风控决策的耗时是 200 ms,名单服务必须要在 30 ~ 50 ms 就得失去后果,否则将极大影响后续规定引擎的运算执行进度如上零碎要求其实在大数据系统架构下都是实用的,只是名单服务要的更极致而已。 在上一篇 《风控外围子域——名单服务构建及挑战》 文章中曾经介绍了名单服务设计,选用了 Redis 作为存储,目前也只能是 Redis 能满足名单服务场景的高性能诉求。同时也介绍了抉择用 Redis 中遇到的数据异样及高可用设计架构,忘了或者感兴趣的敌人能够再回顾一遍。 名单数据的存储构造选用的是 Hash 存储,构造如下: 在此我提出几个疑难(不晓得读者看完后是否也有~): 为何应用 Hash? 应用 set key-value 构造能够么?过期工夫如何保护?set key-val 能够间接基于 expire 设置, hash 构造内过期的数据是如何删除的?以后设计架构,对 Redis 的内存耗费大略在什么水位?可预感的将来可能满足业务的增长需要么?如果你也有这些疑难,那么本篇文章将为你解惑,心愿能有播种。 Redis 是如何存储数据的?工欲善其事必先利其器,咱们先将罕用的 Redis 构造底层实现摸透,能力在应用上熟能生巧,因为本文在用的 redis 构造只会波及到 string 和 hash,笔者仅剖析这两种,其它的读者们感兴趣能够自行搜寻。 字符串存储string 是 redis 中最罕用的存储构造,redis 实现是是基于 C 语言,此处的字符串并不是间接应用 c 中的字符串,而是本人实现了一套 “SDS”(简略动静字符串)。 ...

March 7, 2023 · 3 min · jiezi

关于重构:整洁架构和商家前端的重构之路

1. 背景团队归属于前方业务撑持部门,组内的我的项目都以pc中后盾利用为主。比照挪动端利用,代码库比拟宏大,业务逻辑也绝对简单。在继续的迭代过程中,咱们发现以后的代码仓库依然有不少能够优化的点: 能够削弱对ui框架的依赖21年前端平台决定技术栈对立迁徙到React生态,后续平台的根底建设也都围绕React开展,这就使得商家应用Vue生态做开发的零碎面临技术栈迁徙的难题,将业务逻辑和UI框架节藕变得异样重要。 代码格调能够更加对立随着代码量和团队成员的减少,利用里格调迥异的代码也越来越多。为了可能继续迅速的进行迭代,团队急需一套对立的顶层代码架构设计计划。 能够集成自动化测试用例随着业务变得越来越简单,在迅速的迭代过程中团队须要频繁地对性能进行回归,因而咱们对于自动化单测用例的诉求也变的越来越强烈。 为了实现以上的优化,四组对现有的利用架构做了一次重构,而重构的外围就是整洁架构。 2. 整洁架构(The Clean Architecture)整洁架构(The clean architecture)是由 Robert C. Martin (Uncle Bob)在2012年提出的一套代码组织的理念,其外围次要是根据各局部代码作用的不同将其拆分成不同的档次,在各层次间制订了明确的依赖准则,以达到以下目标: 与框架无关:无论是前端代码还是服务端代码,其逻辑自身都应该是独立的,不应该依赖于某一个第三方框架或工具库。一套独立的代码能够把第三方框架等作为工具应用。可测试:代码中的业务逻辑能够在不依赖ui、数据库、服务器的状况下进行测试。和ui无关:代码中的业务逻辑不应该和ui做强绑定。比方把一个web利用切换成桌面利用,业务逻辑不应该受到影响。和数据库无关:无论数据库用的是mysql还是mongodb,无论其怎么变,都不该影响到业务逻辑。和内部服务无关:无论内部服务怎么变,都不影响到应用该服务的业务逻辑。为了实现以上目标,整洁架构把利用划分成了entities、use cases、interface adapters(MVC、MVP等)、Web/DB等至多四层。这套架构除了分层之外,在层与层之间还有一个十分明确的依赖关系,外层的逻辑依赖内层的逻辑。 Entityentities封装了企业级的业务逻辑和规定。entities没有什么固定的模式,无论是一个对象也好,是一堆函数的汇合也好,惟一的规范就是可能被企业的各个利用所复用。 Use Caseentities封装了企业里最通用的一部分逻辑,而利用各自的业务逻辑就都封装在use case外面。日常开发中最常见的对于某个模型的crud操作就属于usecase这一层。 Interface Adapter这一层相似于胶水层,须要负责内圈的entity和use case同外圈的external interfaces之间的数据转化。须要把外层服务的数据转化成内层entity和usecase能够生产的数据,反之亦然。如下面图上画的,这一层有时候可能很简略(一个转化函数), 有时候可能简单到蕴含一整个MVC/MVP的架构。 External Interfaces咱们须要依赖的内部服务,第三方框架,以及须要糊的页面UI都归属在这一层。这一层齐全不感知内圈的任何逻辑,所以无论这一层怎么变(ui变动),都不应该影响到内圈的应用层逻辑(usecase)和企业级逻辑(entity)。 依赖准则在整洁架构的原始设计中,并不是强制肯定只能写这么四层,依据业务的须要还能够拆分的更细。不过无论怎么拆,都须要恪守后面提到的从外至内的依赖准则。即entity作为企业级的通用逻辑,不能依赖任何模块。而外层的ui等则能够应用usecase、entity。 3. 重构后面介绍了以后代码库目前的一些具体问题,而整洁架构的理念正好能够帮忙咱们优化代码可维护性。 作为前端,咱们的业务逻辑不应该依赖视图层(ui框架及其生态),同时该当保障业务逻辑的独立性和可复用性(usecase & entity)。最初,作为数据驱动的端利用,要保障利用视图渲染和业务逻辑等不受数据变动的影响(adapter & entity)。 依据以上的思考,咱们对“整洁架构”做了如下落地。 Entities对于前端利用来说,在entity层咱们只须要将服务端的生数据做一层简略的形象,生成一个贫血对象给后续的渲染和交互逻辑应用。 以上是商家后盾订单模型的entity工厂函数,工厂次要负责对服务端返回的生数据进行加工解决,让其满足渲染层和逻辑层的要求。除了形象数据之外,能够看到在entity工厂还对数据进行了校验,将脏数据、不合乎预期的数据全副解决掉或者进行兜底(具体操作要看业务场景)。 有一点须要留神的是,在设计entity的时候(尤其是根底entity)须要思考复用性。举个例子,在下面orderEntity的根底上,咱们通过简略的组合就能够生成一个虚构商品订单entity: 如此一来,咱们就通过entity层达到了2个目标: 把前端的逻辑和服务端接口数据隔离开,无论服务端怎么变,前端后续的渲染、业务代码不须要变,咱们只须要变更entitiy工厂函数;并且通过entity层解决过后,所有流入后续渲染&交互逻辑的数据都是牢靠的;对于局部异样数据,前端利用能够第一工夫发现并报警。通过对业务模型进行形象,实现了模块间的组合、复用。另外,形象出的entity对代码的维护性也有十分大的帮忙,开发者能够十分直观的晓得所应用的entity所蕴含的所有字段。Usecaseusecase这一层即是围绕entity开展的一系列crud操作,以及为了页面渲染做的一些联动(通过ui store实现)。因为以后架构的起因(没有bff层),usecase还可能承当局部微服务串联的工作。 举个例子,商家后盾订单页面在渲染前有一堆筹备逻辑: 依据route的query参数以及一些商家类型参数来决定默认选中哪个tab依据是国内商家还是境外商家,调用对应的供应商接口来更新供应商下拉框当初大抵的实现是:咱们能看到7-15、24-125行对this.subType进行了赋值。但因为咱们无奈确定20行的函数是否也对this.subType进行了赋值,所以光凭mounted函数的代码咱们并不能齐全确定subType的值到底是什么,须要跳转到getAllLogisticsCarrier函数确认。这段代码在这里曾经做了简化,理论的代码像getAllLogisticsCarrier这样的调用还有好几个,要想搞清楚逻辑就得把所有函数全看一遍,代码的可读性个别。同时,因为函数都封装在ui组件里,因而要想给函数笼罩单测的话也须要一些革新。为了解决问题,咱们将这部分逻辑都拆分到usecase层: 首先,能够看到所有usecase肯定是一个纯函数,不会存在副作用的问题。 其次,prepareOrderPage usecase专门为订单页定制,拆分后一眼就能看进去订单页的筹备工作须要干决定选中的tab和拉取供应商列表两件事件。而另一个拆分进去的queryLogisticsCarriers则是封装了商家后盾跨境、国内两种逻辑,后续无论跨境还是国内的逻辑如何变更,其影响范畴被限度在了queryLogisticsCarriers函数,咱们须要对其进行性能回归;而对于prepareOrderPage来说,queryLogisticsCarriers只是() => Promise<{ carriers: ICarrires }>的一个实现而已,其外部调用queryLogisticsCarriers的逻辑齐全不受影响,不须要进行回归。 最初,而因为咱们做了依赖倒置,咱们能够非常容易的给usecase笼罩单测: 单测除了进行性能回归之外,它的形容(demo里应用了Given-When-Then的格局,因为篇幅的起因,对于单测的细节在后续的文章再进行介绍)对于理解代码的逻辑十分十分十分有帮忙。因为单测和代码逻辑强行绑定的缘故,咱们甚至能够将单测形容当成一份实时更新的业务文档。 除了不便写单测之外,在通过usecase拆分实现之后,ui组件真正成为了只负责“ui”和监听用户交互行为的组件,这为咱们后续的React技术栈迁徙奠定了根底;通过usecase咱们也实现了很不错的模块化,对于应用比拟多的一些entity,他的crud操作能够通过独立的usecase具备了在多个页面甚至利用间复用的能力。 Adapter下面usecase例子中的fetchAllLogisticsCarrier就是一个adapter,这一层起到的作用是将内部零碎返回的数据转化成entity,并以一种对立的数据格式返回回来。 这一层很外围的一点即是能够依赖entity的工厂函数,将接口返回的数据转化成前端本人设计的模型数据,保障流入usecase和ui层的数据都是通过解决的“洁净数据”。除此之外,通常在这一层咱们会用一种固定的数据格式返回数据,比方例子中的 {success: boolean, data?: any}。这样做次要是为了抹平对接多个零碎带来的差异性,同时缩小多人合作时的沟通老本。 通过Adapter + entity的组合,咱们根本造成了前端利用和后端服务之间的防腐层,使得前端能够在齐全不分明接口定义的状况下实现ui渲染、usecase等逻辑的开发。在服务端产出定义后,前端只须要将理论接口返回适配到本人定义的模型(通过entity)即可。这一点对前端的测试周提效十分十分十分重要,因为防腐层的存在,咱们能够在测试周实现需要评审之后依据prd的内容设计出业务模型,并以此实现需要开发,在真正进入研发周后只须要和服务端对接实现adapter这一层的适配即可。 在实际过程中,咱们发现在对接同一个零碎的时候(对商家来说就是stark服务)各个adapter对于异样的解决简直截然不同(上述的11-15行),咱们能够通过Proxy对其进行抽离实现复用。当然,后续咱们也齐全有机会依据接口定义来主动生成adapter。 ...

July 6, 2022 · 1 min · jiezi

关于重构:重构知识的供给模式-数据平台从思考到落地

简介:如何去建设一套 “高度自动化&体系化的常识管理系统,重构常识的供应模式”。是不是看不懂?而且有点冲?是不是谜语人附体?别急,本文作者将会做具体的阐明。 作者 | 七惜起源 | 阿里技术公众号 一 前言咱们想尝试去建设一套 “高度自动化&体系化的常识管理系统,重构常识的供应模式”。 是不是看不懂?而且有点冲?是不是谜语人附体?别急,上面我会具体的阐明我想做啥和曾经做了啥。 1 平台现状阶段剖析 孵化一个Idea,到产品最终简略易用,通常会经验三个阶段。 阶段一:做通做对 阶段意义:对idea和计划的有效性与合理性进行验证摸索。这个阶段个别资源很少,也比拟孤单。不过如果顺利解决了外围问题,那零碎将初具业务价值。 阶段产品:小程序数据平台 (DONE 交付500+指标) 阶段二:做大做深 阶段意义:开始在初版的根底上,去做边界的摸索。通过接入更多的场景,更大范畴的解决业务问题,来打磨计划,拓宽能力边界并摸索积淀下最优实际。 阶段产品:Foundry根底数据平台 ING 阶段三:做精做好 阶段意义:这是做减法和重构的过程,通过后面的摸索,清晰的定义下零碎的边界,并对交互和性能等方面做更深的耕耘。 阶段产品:业务数据平台 Prepare 阶段成绩 目前Idea正经验第二阶段,在手淘进行更大范畴的摸索与落地。 业务撑持:撑持手淘4个域9个模块的229个指标的数据产出(全链路AB试验,apm启动性能,广告大盘,购物车,首页坑位,搜寻后果页,手淘稳定性等)。同时也迁徙生产了生态凋谢小程序,小部件相干的数据。 能力建设:在《小程序数据平台》的根底上,进一步针对自动化构建能力进行了补强;数据资产治理方面裁减了多租户,资产隔离,文件治理等能力,不便咱们更好的治理指标; 同时也进行了一些数据利用的摸索,如数据开发服务,即席查问能力等。 2 整体架构 3 页面概览 二 数据平台到底要做个啥?所以建设高度自动化&体系化的常识管理系统,重构常识的供应模式,到底是啥意思? 解释分明这个指标,只须要解释分明如下两个问题: “数据”是如何影响“业务决策”的?数据”影响“决策”的过程中,有哪些问题和机会?问题一:“数据”如何影响“业务决策” ? 数据生产生产生命周期 事实世界中,咱们能够把数据的生命周期形象成5个局部:“事实->信息->常识->智慧->决策&口头->回到 事实”。上面给出我集体了解的每个局部的含意: 事实:代表数据被如实的记录(ODS),事实是庞杂冗余无意义的。只有通过分类和荡涤能力失去对人有意义的信息。信息:代表事实中是有意义的局部(DWD + DIM),信息是对一类事实状况的形容。而当信息通过业务的定义与提炼加工,就能生产出有用的常识。常识:代表信息加工出的有用的局部我称之为常识(ADS)。比方巴菲特是股神这是信息。而买qqq对与普通人来说整体收益不从不错,能够思考月供qqq,这是常识。智慧:不同的常识互相碰撞,演绎,推导能产生新的常识,咱们称这种为智慧,智慧是能预测将来的。借用我的好友@骨玉(zherui.lzr)的总结:常识是有用的,而智慧是能预测将来的!决策/口头:通过智慧,理解未知,研判将来,做出决策,口头落地,从而产生新的事实后果,进入下一轮循环。举个例子 吾有一友,名叫老王,不住隔壁。 老王有座山,山上有野花,野草,鸡,苹果等各种动植物(事实)。 其中鸡和苹果比拟有价值,于是老王就把他们圈起来养殖(从事实中梳理出有价值的信息)。并定时喂食施肥除虫,起初鸡和苹果都顺利长大成熟,成为了能吃,能卖的农产品(信息加工成了有用的常识)。 起初老王又发现鸡比苹果利润高很多,如果只养鸡能多赚50%(常识推演出可预测将来的智慧)。于是第二年他决定只养鸡(决策/口头)。起初禽流感来袭,山头只剩野花了,老王血本无归,一盘算还是出租稳当,于是老王把山一租,又回来写代码了。(第二轮数据的生产生产闭环) 这个故事中: 老王山头上的各种动植物就是事实:事实的外围要求是全面实在,而外围行为是采集记录。动植物中的鸡和苹果就是信息:信息的外围要求是有意义,而外围行为上是梳理和荡涤。把鸡和苹果养殖大就是常识:常识的外围要求是有价值有用,而外围行为上是加工和提炼。能够本人吃转化成身材的营养,也能够卖钱投资再生产。这是对老王有用的。 在数据中就是指标了。老王发现养鸡更赚钱就是智慧:智慧的外围要求是可预测未知,而外围行为是应用常识进行演绎推导。最终只养鸡就是决策/口头:决策和口头将产生新的事实,进入下一轮循环。 那咱们来试着答复一下第一个问题:“数据”如何影响“业务决策” ? 答:首先咱们通过埋点采集失去原始的事实(实时数据),从事实中梳理荡涤失去信息(明细),随后通过定义和加工交融各类维度(维度),能失去对应的常识(业务指标)。而用户通过各类路径取得到指标后,通过演绎推导等办法,预测业务的倒退,而后并做出下一步的决策。 问题二:“数据”影响“决策”的过程中,有哪些问题和机会? 咱们简化一下: 咱们把事实梳理成信息,信息加工成常识的整个过程,称为常识生产。 通过智慧预测将来,影响业务决策的过程,称为业务决策。 而常识治理,积淀,运输,供应等中间环节,称之为常识供应和常识获取。 这外面的每个局部,其实都存在问题,也蕴含了很多的机会。 常识生产:不足标准化&自动化的工程体系来生产指标 问题: 1、不足标准化协定 ...

March 8, 2022 · 1 min · jiezi

关于重构:浅谈订单系统重构之路线上真实案例分享

前言最近负责了订单重构我的项目,从技术方案设计,到人员安顿,到最终落地,我的项目圆满完成。尽管重构我的项目做了很屡次,每次都是在挑战极限,在工夫紧工作重的状况下,井井有条的推动。最终提测品质高,安稳上线,此文章记录一下。 背景原订单单库单表,数据量大,已达到性能瓶颈,且无奈程度扩容。订单增长迅速,重构火烧眉毛。指标订单分库分表,不便前期程度扩大订单流程革新,并且平滑过渡到新流程。重构计划注: 所谓重构计划,肯定是基于特定场景的,没有对立计划,但核心思想是一样的。 场景: 网约车场景,下单量大,然而大部分订单会派不上司机勾销。 计划: 减少派前订单表,派前订单表分库分表, 派上司机后,订单进入派后表(老表) 此计划劣势: 不影响派后订单流程(例如,司机端流程,计费流程),压力集中在派前表,同时派前表数据无需长时间保留(例如7天归档)。派前订单表数据量极小,查问写入效率高。 分库分表计划: 灰度计划按流量灰度,分为5个阶段,平滑过渡到新流程 一阶段二阶段三阶段四阶段五阶段十万分之一千分之一10%50%100%工夫安顿 开发人力: 4人7个工作日 测试人力: 6人3个工作日 灰度工夫: 2周 注: 尽管工夫紧,工作重,该有的流程不能漠视 重构收益本次重构如期上线,并且测试阶段bug极少,能够说超预期。 1) 资源不变状况下,下单接口性能晋升N倍(N>4)。 2) 重构后下单接口RT<50ms, 根底服务(dubbo服务)下单接口 RT < 5ms。 3) 订单分库分表,分了256张表,8个库,目前在一个数据库集群,最多反对8个数据库集群程度扩大。 4)订单架构分层,分为业务层和数据层,订单外部通信改为RPC通信。 舒适提醒欢送关注“浅谈架构” 公众号,不定期分享原创文章。

February 12, 2022 · 1 min · jiezi

关于重构:架构团队如何重构内部系统

前端团队不免须要保护一些外部零碎,有些外部零碎因为开始的架构设计不合理,随着业务复杂度的减少,“坏滋味”代码也越来越多,从而导致认知和沟通成本上升,甚至问题频出,此时,重构就天然成了一个抉择。但重构不是一时衰亡,也不是欲速不达的,须要认真的剖析和有序的施行,以试验平台为例,介绍一下智联大前端的重构教训。 试验平台是智联招聘自主研发的A/B试验生态,依靠于数据平台,并联合公司的业务和技术特点量身定制,提供了丰盛的试验能力,迷信的试验机制和残缺的流程治理。 Web端是应用了基于 Vue 实现的 Ant Design Vue组件库开发实现;API层是基于Node.js开发,事后解决、组合、封装后端微服务所返回的原始数据,无效升高UI与后端接口的耦合,实现并行开发和接口变更。 现状UI排版和布局的整体设计不对立,前端交互简单,性能冗余,“坏滋味”代码一直增多更是加大了开发与保护的难度;Api层没有遵循支流 RESTful Web API 规范,只负责了后端接口的转发,逻辑全放在Web层实现,没有无效升高UI与Api层接口的耦合,减轻了Web层的累赘; 基于以上起因,咱们决定对试验平台零碎进行重构,进一步提高其易用性、内聚性和可维护性。 剖析首先一一页面剖析一下试验平台性能及应用状况,以不便对接下来重构工作有初步的理解: 概览页:次要展示自试验平台上线以来应用状况的统计信息,为了更好展示统计内容,以及日后不便对数据结构的保护,咱们决定数据不再由后端接口提供,改由本人的Api层计算;试验列表页:列表页次要用于展现用户关注的试验的要害信息,所以尽可能的精简展现字段以及优化信息主次排版;同时,提供疾速跳转入口(中转统计、中转调试),优化用户体验;增加可搜寻试验名和创建人,优化搜寻体验;对于试验状态,有些状态不再须要(如申请公布、批准公布、已公布、归档),同时还须要兼容旧的试验状态,为此咱们对试验状态做了新的调整: 草稿:新建调试:调试状态运行:运行、申请公布、批准公布进行:放弃、进行、已公布、归档世界概览页:基本功能不变,按准则重构代码即可变量页:通过考量,变量没有必要再执行开释、或者复原等操作,所以只须要展现正在“运行”试验的变量列表;设置页:次要目标是展现和增加管理员,所以没有必要显示所有的用户,所以能够简化为删除和增加管理员即可;根本信息页:此页面基本功能不变,优化页面布局排版,对立用户体验,编辑权限由Api层对立管制;统计分析页:此页面基本功能不变,为了便于保护,统计数据全副由Api层计算生成;另外,经剖析大盘指标页面实时性能能够去掉;优化页面整体布局排版及重构代码;操作记录页:须要增加克隆试验id的信息,优化用户体验;管制页:此页面基本功能不变,对立用户体验,编辑权限由Api层对立管制,优化页面布局排版及重构代码;总结页:用于总结试验后果,这个页面通过剖析,曾经不再须要; 准则至此,依据之前的剖析,咱们曾经对试验平台的现状有了初步的意识。接下来,总结一下造成一些有用的领导准则: 分层,Web层和Api层应各司其职: Web层只负责UI的交互和展现;API层遵循Restful Web API规范,采纳强类型查看的Typescript开发,负责所有的性能逻辑解决及权限管制;布局,整体布局放弃设计统一: 布局自然化,晋升可维护性;版块规范化,放弃设计对立;各版块的上下左右间距统一,版块间对齐;模块,放弃职责繁多,不便保护准则: 依照职责拆分模块,并互相解耦;尽最大可能不保护状态(尤其是全局状态),而是通过与其余模块交互实现;逻辑去地方化,扩散到各性能组件;组件化,放弃组件职责繁多;未复用的组件均置放于父容器组件的目录之下;开发标准,遵循智联前端开发标准及自定义准则: 款式规范化,升高更新老本;对立输入输出标准;禁止所有魔法数字,而是通过变量实现;禁止所有内联款式,而是通过更加通用的Class实现;尽最大可能不应用相对定位和浮动,而是通过a-layout组件、规范文档流或Flex实现;流程,采纳渐进式重构形式: 渐进式重构形式,分阶段进行重构,每一阶段都不毁坏现有性能,具备独自公布的能力;阶段接下来,咱们将重构周期划分几个不同的阶段进行有序施行。 第一步:js迁徙到ts家喻户晓,JS是一门动静语言,运行时动静解决类型,应用非常灵活,这就是动静语言的魅力所在,然而,灵便的语言有一个弊病就是没有固定数据类型,短少动态类型查看,这就导致多人开发时乱赋值的景象,这样就很难在编译阶段排除更多的问题,因而,对于须要长期迭代保护以及泛滥开发者参加的我的项目,选一门类型严格的语言、能够在编译期发现错误是十分有必要的,而TypeScript采纳强类型束缚和动态查看以及智能IDE的提醒,能够无效的升高软件腐化的速度,晋升代码的可读性可维护性。 所以,这次重构工作首先从js迁徙到ts开始,为后续模型梳理奠定语言根底。 ts仅限于API工程的node层,因为,前端应用Vue2对ts反对不太敌对,所以还放弃应用原有js。 第二步:梳理数据模型这个步骤比较简单,次要是梳理现有的API接口申请的输出和输入的信息,对后续梳理数据实体打好根底。首先,整顿出试验平台零碎所有的页面,如下所示: 设置变量世界概览试验列表创立试验查看根本信息编辑根本信息查看操作记录查看统计管制而后,别离对每个页面波及的Api接口进行进一步统计,例如,设置页:获取用户列表、新增和删除用户,设置用户角色等API接口。其次,依据上一步整顿的后果,对每个API接口申请输入输出信息进行演绎整顿,例如,关上试验根本信息页,找到浏览器的开发工具并切换到【NetWork】,鼠标右击申请接口找到【Copy as fetch】复制申请后果,如下图所示: 如下展现了API接口申请输入输出信息的代码构造: 【示例】// [分组]: 获取试验分组信息列表fetch( "https://example.com/api/exp/groups?trialId=538", { credentials: "include", headers: { accept: "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin" }, referrer: "https://example.com/exps/538", referrerPolicy: "no-referrer-when-downgrade", body: null, method: "GET", mode: "cors" });const response = { code: 200, data: { groups: [ { desp: "c_app_default_baselinev", flow: 20, groupId: 1368, imageUrl: "", type: "A,对照组", vars: ["c_app_default_baselinev"] }, { desp: "c_App_flowControl_baseline", flow: 20, groupId: 1369, imageUrl: "", type: "B", vars: ["c_app_flowControl_baseline"] } ], varName: ["gray_router_prapi_deliver"] }, time: "2019-12-20 17:25:37", message: "胜利", taskId: "5f4419ea73d8437e9b851a0915232ff4"};同样的,咱们依照以上流程顺次对所有页面的对应API接口申请的输入输出别离进行整顿,最初,失去如下文件列表:接下来,剖析每个接口的返回值,提取UI层交互会用到的字段,从而定义根本的数据模型,数据模型要可能直观的展现数据的根本组成构造,如下所示: ...

December 2, 2021 · 3 min · jiezi

关于前端:重构把重构后的代码稳定搞上线

代码重构有两大难点,一个是「考古」,也就是如何疾速梳理出代码的原有逻辑,还有一点就是「公布」,如何让新的代码能够稳固的公布到线上,而不产生故障。上面咱们就聊聊我一个敌人的故事,看看他是怎么把代码稳固搞上线的。为了表白更为亲切,你当初就是我那个敌人。重构代码对很多人来说,相对是一件脏活、累活。没有能够大幅度提效的办法,难以积淀无效的体系化的可复用的技术抓手,对业务来说没有显著的增量,精力和工夫耗费微小,没有测试用例,也不肯定能失去测试的反对,自测很难做到充沛,最初开发完了很难上线,次要起因是胆怯!当然并不是咱们不自信,是真的恐怖。 一、你为什么不敢发代码?通过代码还原过后残缺的产品逻辑太难了你重构的代码是谁的?鬼晓得是谁的!能让你重构的代码大概率不是你写的代码,而且是远古代码,用的是一种过期的技术栈。当然个别状况下,当年的开发、测试、甚至产品早已不见了形迹,只能在正文的代码里看见了了数语。语言中走漏着无奈,用一个程序员的良心揭示着起初人,「小心后面的脏东西」。看了这些话,你只能发出口中马上要吐出的芳香,默默来到工位,倒点热水。 从此你会发现,正文不仅可能帮你读懂代码,还能有警示作用,通知你重构代码的同时,记得把 bug 一并改了。你想要通过正文来梳理出原始需要的欲望宣告失败,接下来你只能死磕了,祷告千万不要漏掉业务逻辑。 没有自测用例别以为大公司制度欠缺,测试都有残缺的测试用例,事实会狠狠的夹你脑门。频繁的迭代,性能早已面目全非,老的用例基本不可用,更何况基本找不到老的测试用例。没有用例怎么自测呢?全靠集体设想。 没有测试同学跟进多一个人多一分力量,让一个有教训的测试参加到性能回归中来,无疑会给你的重构事业吃上定心丸,但实在的状况是,测试同学基本不想参加这种脏活累活。他本人手里的需要还测不过去,怎么会把工夫奉献给一个前端发动的重构工作上呢。无增量,无抓手,纯膂力,他们同样心知肚明。 没有稳固公布计划在没有上述保障的前提下,如果你还能硬着头皮上线,就会遇到更大的难题,如何上线?间接全量替换吗?如果线上出问题怎么办?好在前端的回滚是十分迅速的,然而即便再迅速的回滚,从公布实现到发现问题回滚,在揭示用户从新刷新页面,这个过程也足以造成难以估计的结果,尤其是那些高频应用,且极易产生脏数据的场景。这就是没有一个无效的公布计划所导致的常见结果,这个结果还有可能导致你背上故障,这一年加过的班,熬过的夜,掉的头发,什么也换不来,只能催生你换个中央重新做人的念头。 综上因素间接导致开发者极度不足安全感,一个不敢上线本人代码的程序员,就像中午被本人一个月大孩子的哭声吵醒,那时那刻你只想装死摸鱼。更何况你的工作往往不是只有重构这一件事,写写新需要他不香吗?就这样你眼看着一个页面重构了两个星期,迟迟不能收尾,你变得越来越不自信,越来越胆怯了起来,不敢面对那些重构了一半的代码,开始恐怖老板的问题:「重构搞的怎么样了?」,你几乎不像个程序员。 终于到了年底,你的重构事业还未实现,更可怕的是,这件事还被打上了「承诺型」OKR 的标,于是你痛定思痛,做了个梦。 工夫回到年初你刚刚接到重构工作的时候。二、寻求组织保障你的重构工作是把 177 个 jQuery 页面用 React 重写一遍。你立马想到,本人一个人一年工夫,肯定是做不完的,此时此刻,切记不要满口答应,肯定捕风捉影,甚至向着最坏的方向想,让老板充分认识到这项工作的艰巨性,不要抱有太高的冀望。最重要的是保障人力的投入,必须有更多的同学一起参加进来,无效的分工才有可能实现这项艰巨的工作。有人参加进来,也只是根底,因为他们极有可能会像下面形容的一样,从兴高采烈到气宇轩昂,因而肯定要确保工夫的投入,必要时把老板也拉进来跟你一起做,老板一旦参加进来,就会更有体感,能领会到大家的不易。接下来,就应了那就老话,「别忘了,你是一个 owner!」做好基础设施建设,让每个同学有趁手的工具,有平安的保障,去除他们的后顾之忧至关重要。因而,你要做上面几件事。 三、划分重构页面优先级你通过粗疏的钻研发现,这些页面中,有 77 个页面是用户应用较多的页面,也是绝对比较复杂的页面,剩下的 100 个页面,大部分是给开发用的增删改查页面,用户的应用频率不高。于是你做了如下划分:优先级划分好优先级当前,就要对不同优先级的页面应用不同的稳固公布策略。 简单高频页面:重兵压上,粗疏还原原始需要,抠代码,拉测试同学一起整顿测试用例,依照测试用例自测,测试同学回归所有性能。但其实这部分页面中,也能够分为两种页面: 编辑页面:这样的页面是危险最高的页面,一旦因为后端接口没有做残缺的数据校验,就会编辑出脏数据,或者谬误的数据被保留,导致线上运行异样,这种结果将是不堪设想的,即便十分短的工夫内回滚,也会造成难以挽回的故障,因而必须要像新需要一样测试到位。展现页面:这样的页面不会影响运行时,不会产生脏数据,是危险绝对低一点点的页面,本着不麻烦合作方的准则,毕竟资源无限,能够让测试帮你出残缺的用例,而后你本人自测,或者多找几个同学帮你自测。高频简略页面:自测,当然最好是能绑架几个常常用这个性能的开发,来帮你点点,然而本人测总是会有可能会有脱漏,因而就须要上面的步骤来保障了。低频运维页面:选择性重构,因为很多页面基本上不会有迭代,且应用频率较低,基本上不须要重构,即便是有新的需要,也能够在做新需要的时候顺便重构下,认为并不能占用太多工夫。将页面划分结束后,你会发现重构的工作量升高了很多,因为本着「无需要,勿变更」的准则,很多页面都能够不须要重构。且上述重构完的页面都必须做灰度公布。 四、单测前端不太喜爱写单测,你大略总结了一下,次要有上面几方面的起因: 当下的收益不高。相比后端接口的单测,前端单测写起来绝对简单。前端更多是面向 UI 的编程,但 UI 变动大,难以使用 TDD (测试驱动开发) 的开发模式。没有写单测的习惯,可能是因为单测减少了工作量,且没有写纯函数的意识,不利于测试。单测的工具难学又难用。你发现前端不喜爱写单测,有各种各样的起因,然而当你重构那些简单页面,尤其是 jQuery 技术栈重构为 React 技术栈的时候,单测真的十分有用。比方这里有一个编辑页面,蕴含两局部:根本信息和运行逻辑,在重构运行逻辑时候,你首先要保障的是重构过后的页面在保留的时候,保留的数据结构必须跟之前的接口参数必须统一,所以在重构运行逻辑这个组件的时候就会有很多数据转换逻辑。能够看到为了保障你的新组件不影响放弃原有性能,就要保障原始数据通过新组件的一顿操作最终保留了原来的构造,此时你就能够写单测来保障这个过程。 describe('utils', () => { it('流程图:转换为提交的数据 transformForm', () => { const result = transformForm(canvasData); expect(result).toEqual(settingData); }); it('流程图:转换为须要的数据 parseRuleSetData', () => { const [result] = parseRuleSetData(settingData, rules); expect(result).toEqual(canvasData); }); it('流程图:重复转换 transformForm - parseRuleSetData', () => { const [result] = parseRuleSetData(visualSettings, rulesData); const newResult = transformForm(result); expect(newResult).toEqual(visualSettings); });});前端单元测试写起来简单,其实只是 UI 的单测简单而已,如果你把代码做好了足够的拆分,拆出更多函数,更多 hooks ,单测就是轻而易举了。 ...

April 15, 2021 · 1 min · jiezi

关于重构:有哪些可以提高代码质量的书籍推荐

这篇文章的内容其实很早就写了,并且,我也曾经同步在了我的 Github 的一个仓库中(仓库内容还在持续欠缺中),地址:https://github.com/CodingDocs/awesome-cs 。对应的 Gitee地址:https://gitee.com/SnailClimb/awesome-cs (Github无法访问或者访问速度比较慢的小伙伴能够看码云上的对应内容)。 思考到还未发过相似的文章,所以,明天早晨就来一篇!上面举荐都是我看过并且我感觉值得举荐的书籍。 不过,这些书籍都比拟偏实践,只能帮忙你建设一个写优良代码的意识规范。如果你想要编写更高质量的代码、更高质量的软件,还是应该多去看优良的源码,多去学习优良的代码实际(比方设计模式、设计准则) 代码整洁之道《重构》 必看书籍!无需多言。编程书籍畛域的珍宝。 世界顶级、国宝级别的 Martin Fowler 的书籍,能够说是软件开发畛域最经典的几本书之一。目前曾经出了第二版。 这是一本值得你看很多遍的书籍。 《Clean Code》 《Clean Code》是 Bob 大叔的一本经典著作,强烈建议小伙伴们肯定要看看。 Bob 大叔将本人对整洁代码的了解稀释在了这本书中,真堪称是对后生的一大馈赠。 《代码大全》 其实,《代码大全(第 2 版)》这本书我自身是不太想举荐给大家了。然而,看在它的豆瓣评分这么高的份上,还是拿出来说说吧! 这也是一本十分经典的书籍,第二版对第一版进行了重写。 我简略地浏览过全书的内容,感觉内容总体比拟虚,对于大部分程序员的作用其实不大。如果你想要切实地进步本人的代码品质,《Clean Code》和 《编写可读代码的艺术》我感觉都要比《代码大全》这本书更好。 不过,最重要的还是要多看优良的源码,多学习优良的代码实际。 《编写可读代码的艺术》 《编写可读代码的艺术》这本书要表白的意思和《Clean Code》很像,你看它俩的目录就可以看进去了。 在我看来,如果你看过 《Clean Code》 的话,就不须要再看这本书了。当然,如果你有工夫和精力,也能够疾速过一遍。 另外,我这里还要举荐一个叫做 write-readable-code 的仓库。这个仓库的作者收费分享了一系列基于《编写可读代码的艺术》这本书的视频。这一系列视频会基于 Java 语言来教你如何优化咱们的代码。 在实践中学习的成果必定会更好!举荐小伙伴们都放松学起来啊! 《Effective java 》 Java 程序员必看! 又是一本 Java 畛域国宝级别的书,十分经典。这本书次要介绍了在 Java 编程中很多极具实用价值的教训规定,这些教训规定涵盖了大多数开发人员每天所面临的问题的解决方案。这篇文章可能十分理论地帮忙你写出更加清晰、强壮和高效的代码。本书中的每条规定都以简短、独立的小文章模式呈现,并通过例子代码加以进一步阐明。 程序员职业素养《 The Clean Coder》 ...

April 6, 2021 · 1 min · jiezi

关于重构:程序员重构入门指南

文章首发于公众号「架构师指南」及集体博客 shuyi.tech,欢送关注拜访。 对于刚入门的编程者来说,《重构》是一本不错的读物。它能给你带来一些重构思维上的扭转,通知你为什么要重构,应该怎么做重构。本文基于《重构》一书,整顿重构所需的「思维」与「技巧」上的筹备。 思维篇指的是对于重构的意识,了解这些思维可能让你更好地做好重构。而技巧篇指的是具体重构时的一些技巧,可能让你晓得怎么写出更好的代码。 思维篇重构之前先建设测试用例重构的第一步,是为行将批改的代码建设一组牢靠的测试用例。预感建设好的测试用例,是你的平安绳,它能通知你工作是否实现了,是否存在可能的缺点。 重构的价值重构能够改良软件的设计。就像在一直整顿代码一样,经常性的重构能够帮忙代码维持本人该有的状态。重构使得软件更容易了解,不要让几个月之后其他人(甚至本人)也读不懂你的代码,清晰易懂的代码能让你更快了解代码的用意。重构能帮忙找到bug,因为重构是小步快跑的,每一步都有一个猎手(测试用例)帮你抓到猎物(bug)。 好的重构,最终能帮你进步编程速度,进步编程带来的愉悦感。 什么时候重构?什么时候重构?第一次只管去做,第二次会恶感,第三次应该重构。事不过三,三则重构。 专门拨出工夫重构是不可能的,咱们须要在日常工作中一直地重构。然而还没开始有反复的性能,就想着重构,那太可笑了。然而反复的代码或者代码有问题,超过三次之后还不入手,那么就有点偷懒了。 什么时候不重构?当现有代码基本不能失常运作的时候,你应该重写,而不是重构。 重构应该是一个习惯重构应该是一种工作习惯,在日常工作中一点点重构,而不是妄想有专门的工夫重构。咱们已经进行的一些大型重构,须要数月甚至数年的工夫。如果须要给一个运行中的零碎增加性能,你不可能让零碎进行2个月去重构。你只能一点点地做你的工作,明天一点点,今天一点点。 如何测试?咱们的工夫总是无限的,测试你最放心出错的局部,这样你能失去最大的收益。测试的时候,寻找边界条件,集中火力测试那里! 什么时候勾销重构?如果你感觉到重构失控了,那么最好的方法是勾销重构,回到你的安全区去。等你从新能掌控的时候,再来做重构。 重构的团队意识进行大规模重构时,有必要为整个开发团队建设共识。整个团队都必须意识到:有一个大型重构正在进行,每个人都应该相应地安顿本人的口头。 设计模式帮忙你重构学习设计模式能够很好地帮忙你重构,它能在适当的场合帮忙你承载简单的业务。但你不应该简略地理解,而是要多比照各个设计模式之间的区别,它们解决了什么问题,实用于什么场合。 技巧篇不要呈现反复代码当呈现反复代码时,你应该提取出公共办法。我想这个说得曾经足够分明了,当呈现反复代码的时候就须要想想:我是否须要抽离出反复的代码? 不要呈现过长、过短的函数当函数过长,你应该依据业务逻辑提炼出多个函数。那一个函数多少行算是长呢?按我集体了解,一个函数在 20-50 行是比拟适合的。但这也只是一个经验值,最基本的判断规范是:他人浏览你的代码的时候,是否能很清晰、很不便地读懂。 如果你写得很长,然而他人读得时候很难受,那么也能够。 要留神函数过短也会带来浏览的艰难,他会让你屡次跳转,打断你的浏览思路。所以如果一个函数内容过短,你须要思考是否去掉这个函数。简略地说,你还是应该依据业务逻辑结构化,将每块业务逻辑放到适合的函数中。 不要呈现过大的类当类过大,你应该思考是否能拆分出多个类。或者你应该思考,你的类形象体系是否呈现了问题。一个过大的类与过长的函数一样,会让人感觉到压抑、难于读懂。 不要让参数过长当参数列过长,你应该应用对象参数。 提炼发散式变动因为一个变动,而须要批改多个中央,这阐明呈现了发散式变动,你须要思考将变动的代码合并在一起。 提炼对象总是绑在一起呈现的数据,须要把他们提炼到一个独立对象中。 引入解释性变量不要让你的变量或表达式没有语义,必要时引入解释性变量。很多人会习惯性地用 flag 去承载一个表达式的值,但这并不是一个好的习惯。变量命名还是应该更加语义化,这样咱们能更加清晰地明确这个变量的作用。 搬移函数一个函数被另一个类调用得很频繁,那你可能得思考把这个函数搬移到另一个类中。 搬移字段一个字段被另一个类用得很频繁,或者你改思考把这个字段搬移到另一个类中。 提炼类、简化类某个类做了应该由两个类做的事件,此时应该提炼出一个新类,而后用组合关系组合起来。这其实与 SOLID 准则想符合,一个类应该是繁多职责的,如果某个类做了两个类的事件,那阐明其承当的职责就简单了,因而须要抽离出一个新类进去。 而如果一个类并没有太多内容,这时候就应该思考是否去掉这个类,优化整个类构造。 参考资料除旧迎新,试试《零碎重构与迁徙指南》 - 知乎五个简略的准则,带你写出整洁代码 - 知乎GitHub - phodal/migration: 《零碎重构与迁徙指南》手把手教你剖析、评估现有零碎、制订重构策略、摸索可行重构计划、搭建测试防护网、进行零碎架构重构、服务架构重构、模块重构、代码重构、数据库重构、重构后的架构守护技术债治理的四条准则 - ThoughtWorks 洞见31 天重构指南 - InfoQVIP!!!!Refactoring Day 1 : Encapsulate Collection · Los Techies不要让 “Clean Code” 更难保护,请应用 “Rule of Three”-InfoQ

February 25, 2021 · 1 min · jiezi

记一次代码重构

单一职责功能单一功能单一是SRP最基本要求,也就是你一个类的功能职责要单一,这样内聚性才高。 比如,下面这个参数类,是用来查询网站Buyer信息的,按照SRP,里面就应该放置查询相关的Field就好了。 @Datapublic class BuyerInfoParam { // Required Param private Long buyerCompanyId; private Long buyerAccountId; private Long callerCompanyId; private Long callerAccountId; private String tenantId; private String bizCode; private String channel; //这个Channel在查询中不起任何作用,不应该放在这里}可是呢? 事实并不是这样,下面的三个参数其实查询时根本用不到,而是在组装查询结果的时候用到,这给我阅读代码带来了很大的困惑,因为我一直以为这个channel(客户来源渠道)是一个查询需要的一个重要信息。 那么如果和查询无关,为什么要把它放到查询param里面呢,问了才知道,只是为了组装查询结果时拿到数据而已。 所以我重构的时候,果断把查询没用到的参数从BuyerInfoParam中移除了,这样就消除了理解上的歧义。 Tips:不要为了图方便,而破坏SOLID原则,方便的后果就是代码腐化,看不懂,往后要付出的代价更高。 功能内聚在类的职责单一基础之上,我们还要识别出是不是有功能相似的类或者组件,如果有,是不是要整合起来,而不要让功能类似的代码散落在多处。 比如,代码中我们有一个TenantContext,而build这个Context统一是在ContextPreInterceptor中做的,其中Operator的值一开始只有crmId,但是随着业务的变化operator的值在不同的场景值会不一样,可能是aliId,也可能是accountId。 这样就需要其它id要转换成crmId的工作,重构前这个转换工作是分散在多个地方,不满足SRP。 //在BaseMtopServiceImpl中有crmId转换的逻辑 public String getCrmUserId(Long userId){ AccountInfoDO accountInfoDO = accountQueryTunnel.getAccountDetailByAccountId(userId.toString(), AccountTypeEnum.INTL.getType(), false); if(accountInfoDO != null){ return accountInfoDO.getCrmUserId(); } return StringUtils.EMPTY; } //在ContextUtilServiceImpl中有crmId转换的逻辑 public String getCrmUserIdByMemberSeq(String memberSeq) { if(StringUtil.isBlank(memberSeq)){ return null; } MemberMappingDO mappingDO = memberMappingQueryTunnel.getMappingByAccountId(Long.valueOf(memberSeq)); if(mappingDO == null || mappingDO.getAliId() == null){ return null; } }重构的做法是将build context的逻辑,包括前序的crmId的转换逻辑,全部收拢到ContextPreInterceptor,因为它的职责就是build context,这样代码的内聚性,可复用性和可读性都会好很多。 ...

August 7, 2019 · 4 min · jiezi

记一次前端项目重构要点总结

不知不觉已是2019年的7月,恍惚之间已工作四年。懵懵懂懂的成长,间歇性努力,实话说,对现在自己取得的成果不大满意。不过,好在时不时顿悟,知道适时作出改变。 此后发文会适当记录一些心路历程,与君共勉。 欢迎Star和订阅我的博客。本文要点: 什么项目,为何会重构?怎么重构的?重构前后对比什么项目,为何会重构?项目是公司主打业务产品之一的可视化子项目,与其他子项目几乎没有耦合,所以可以单独拎出来重构。 具体业务不作描述。技术主要用的是Vue2系列和JavaScript,还有一个自研的可视化工具库。第一个重构原因就是没有引入静态类型,导致查看一个对象结构需要翻来覆去在多个文件中查找。第二是因为之前新增代码模式一般为:“来一个需求加一段代码”,长期积累导致代码结构混乱,可读性差。第三是各个状态模块耦合度高,加大了代码维护难度。 怎么重构的?一、在JavaScript中使用TypeScript。“什么?在JS中使用TS? 闻所未闻。 ” 在看到TS官网手册最后一条"Type Checking JavaScript File"之前,我也这样想。其实,TS和VSCode(一款IDE)结合,也可以实现静态类型检测,只不过使用注释形式,一样支持tsconfig.json和自定义Typing。 type TypeApple = { name: string, count: number }/** @type {TypeApple} */const apple = { name: 'foo', count: 100 }二、细化模块分类。一般情况下,模块都会有耦合。但如果耦合度过高,往往是因为模块没有细分到位。如果细化模块?举例,假如有一个模块叫Operation,里面既包含操作相关逻辑,也有操作面板逻辑。随着业务发展,操作面板逻辑越来越多。我们完全可以将操作面板逻辑单独抽成一个模块OperationPanel。 三、解耦可视化库和Vue/Vuex。写业务的时候,很容易因为方便,在Vue组件或Vuex模块中代码越写越长,越来越难维护。这个项目也不列外。所以重构的时候,单独将可视化库喜爱那个管逻辑抽成模块,并使用类Vuex写法(state, getters, mutations, actions)进行管理。 class Counter { // # state /** @type {number} */ count = 0 // # getters get countText() { return `Count is: ${ this.count }` } // # mutations /** @param {number} count*/ SET_COUNT = count => { this.count = count } // # actions /** @param {number} count*/ logCount = ( count ) => { this.SET_COUNT( count ) console.log( this.countText ) }}四、最后一条,编写可维护性高的代码。这里说两个方法。 ...

July 4, 2019 · 2 min · jiezi

指尖前端重构React技术调研分析

一、为什么选择ReactReact是当前前端应用最广泛的框架。三大SPA框架 Angular、React、Vue比较。 Angular出现最早,但其在原理上并没有React创新的性能优化,且自身相对来说显得笨重。Vue出现最晚,其核心原理学习了React,只是语法形式的变化,关系上来说React是开拓者,Vue是学习者。React社区有强大活力与创新能力,不断涌现革命性的创新产品,其中包括可以使用JS操作原生控件的React Native,Vue后来跟进学习出了类似的Weex,但两者成熟度差很多。目前来看React的生态系统要比Vue大的多,在github、stackoverflow等最大的技术社区搜索两者,React的搜索结果是Vue的十倍左右,另外据近期统计使用React的站点是Vue的几百倍以上。更大的生态意味着更多可用的资源,以及遇到问题可以得到更多的有效参考与帮助,这也是除了性能之外选择React的核心原因。 选择React之后,应用会在以下几个方面有提升。 第一,原先的html间跳转会有短暂的白屏现象,这一点在安卓性能较差的机器上尤为明显,而React作为单页应用没有这个问题。第二,React 提供的虚拟DOM包含Diff算法,即将原dom copy一份,与改动后的dom对比,只渲染不同的dom节点,实现最小代价渲染,vdom创新的性能优化方式对性能的提升毋庸置疑。第三,React中核心组件化技术,更加容易的绑定事件行为,动态更新特定的dom,代码更加模块化,重用代码更容易,结构清晰易维护。二、在移动端使用React三大框架在移动端分别有自己的东西。Angular的ionic,React的React Native,Vue的Weex。其中ionic 是基于cordova技术,依然是浏览器应用。而后两者已上升到操作原生控件的层面,做出来的是原生界面,其中React Native的成熟度远高于Weex,已经被很多公司使用,而Weex使用者很少。 综合来看选择React 生态明显最佳,由当前的cordova过渡为cordova+Reactjs,然后可以平滑地过渡到React Native,媲美原生性能的最优混合开发方式。之所以说平滑是因为React Native中近90%的代码(JS)可以在IOS和Android端使用,剩余的涉及原生的代码也基本可以找到可用的资源,就像cordova 的插件一样。毕竟如果需要同时掌握JS, OC(或swift),java(或kotlin)才能开发React Native的话,那这门技术不会有人用;当然反过来如果有原生开发知识的话会对开发React Native有一定帮助。 直接转型为React native的话涉及了应用底层架构的变动,有比较大的跨度,而转为cordova+Reactjs相对容易,而由cordova+Reactjs到React Native同样容易不少,因为其中大部分Reactjs代码可以重用。 三、Reactjs开发工具的选择首先开发脚手架官方出了Create-react-app,集成了webpack-当前最流行的打包工具,babel-提高js版本兼容性的转码器,以及ESLint-代码检测工具和其它一些常用工具,同时对这些工具进行了比较优的配置。值得一提的是该脚手架将这些工具的配置文件进行了隐藏,本意是让使用者专注于编码即可,但实际使用时通常会有自己配置的需求,此时执行npm run eject即可出现被隐藏配置文件。 React-router 是官方推荐的路由管理工具,由于是单页应用区别于原先的html界面间跳转,跳转实质是在组件间进行,所以需要有路由管理工具来统一化管理。这里值得一提的是,React-router配合webpack可以实现代码的按需加载。 一般来说,webpack打包后会在生成一个压缩的js文件,在单页应用打开会整体加载这个文件,由于该js文件包含之前所有的js代码,虽然进行了压缩,一般仍至少有几百kb,当应用稍微复杂点,打包后文件会相应变大。而加载的时候,不管那些代码有没有执行到,都会下载下来并进行加载,造成性能浪费,这一点在显然在web端很重要,而在cordova中是将js代码直接打包在本地,等于跳过了下载步骤但仍然会有加载过程。通过在router中写require.ensure代码并在webpack中相应地修改配置即可将js分成多个文件,在需要时加载对应的js文件,实现按需加载。 Redux 是应用最广泛的第三方状态管理工具,其作用是当应用中多数据状态交互时,可以更有方便且代码结构清晰地统一管理状态,下图给出了形象的阐释。由于在实际开发中一般是分人员/分功能模块独立开发,考虑引入redux的成本(redux本身略复杂),可以在没有多数据交互的模块不使用redux,而在类似涉及增删改查的表单以及即时通讯websocket等契合redux的模块使用。 为项目选取合适UI组件库,一定程度上简化UI样式的开发并且考虑使用其提供的过渡动画效果。这方面有比较多的选择,Google Material Design 风格的Material-UI在github上最受欢迎,但其设计语言与我们当前APP截然不同,腾讯的weui和阿里的antd-mobile 较为相近,其中antd-mobile与create-react-app脚手架配合使用时配置项比较繁杂,因为阿里本意是用来配合自己的脚手架dva(封装了react-router和redux),因此暂时选择weui,后期开发有特定组件需求可结合其他ui库使用。 至于页面跳转时的过渡动画,有些UI库给出了一些过渡样式,比如touchstone。但该库已不再维护,文档不佳,且与新版本的react-router配合使用有不兼容情况。后来浏览官方文档发现官方有动画库react-addons-css-transition-group,使用该库结合css3的动画三件套animation,transition,transform即可实现各种动画效果包括基本的过渡效果,比如渐进平移等。 另外关于css,因为是单页应用,所以如果不加处理,直接import css文件的话最终打包生成一个css文件会导致样式应用到全局,造成同类名样式相互污染影响。解决这个问题有很多种方案。Facebook积极探索css in js方式,但直接写内联样式代码可读性太差。目前解决方案中应用最广泛的是css-modules,即在webpack配置中开启module选项,使用styles对象来写样式。 解决的原理是将css类名在打包后编译成哈希字符串,保持其唯一性。但当想要使用全局样式时要再配置,稍显繁杂,且它类名编写的方式为对象的方式,需要整体修改,另外在使用它时,发现不支持-横线的类命名方式,支持下划线方式,推荐驼峰式,而我们之前html中的样式类名大多是横线命名,这意味着原html和css中的类名都要对应修改,考虑到样式类名非常多,这一方式舍弃。 另外有基于css-modules使用高阶组件的react-css-modules使用人数也比较多,允许横线命名方式且全局本地样式区分简单,但有benchmark测试表明其会较大程度拖累性能,所以也舍弃。解决这个问题要最大程度兼容原先css的写法,即改动最小,因为之前的css类样式数量庞大。 Webpack css-loader 有个属性 :local 加上之后类会变成局部作用域,即webpack会对该类型的类进行自动哈希转码处理,但显然类名一个个加:local 是有些呆板的工作,于是想到可以利用scss的嵌套属性将:local在一个css文件中统一加到类名前。这里涉及到在脚手架create-react-app 添加对scss的支持,在命令行执行安装,并在package.json的scripts中添加watch-css指令,将原css文件改为scss文件,然后在最外层添加:local,执行watch-css命令,即可在scss文件旁自动产生css文件,且类名前自动添加:local 前缀,这种方法实践中发现并非所有类的样式都与:local 兼容良好,相应的可以使用文件名代替:local,要做的就是保持文件名的唯一性,这一点原工程下的文件名已满足。这样原先文件中引入css的方式,全局css引入的方式都不需要变化,做到最小代价。 scss 是 sass 3 引入新的语法,其语法完全兼容 css3,并且继承了 sass 的强大功能,sass和less是前端扩充css常用的方式,添加了嵌套,变量,继承等语法,但需要编译成css来最终使用(稳定性考虑)。 四、Reactjs 和cordova结合有哪些需要注意的开发Reactjs使用官方提供的脚手架Create-react-app,最终通过npm run build生成一个单页网页应用,放入cordova的www目录下即可。由于这两部分开发时独立,而原先开发是在含www目录的cordova工程目录下直接开发,这种不同会产生一些问题。比如cordova中某些插件安装后export函数或者变量供引入使用,因为一开始是分离的,在create-react-app中并找不到这些变量,就造成在build的时候产生变量undefined的错误,尽管最终放到cordova工程中后可以找到变量并正常运行,但在第一步react开发时控制台报一堆error会妨碍调试,影响开发体验。 在github上有一些react cordova 库,但实质上它们都需要通过npm run build来打包,所以并没有解决引入插件变量的问题,且会与create-react-app 有相斥的地方。所以要想办法使插件提供的变量在React中不报错,这里在不影响ESLint 检错机制的情况下可以采取迂回的方式。Build时控制台报错仅针对src文件夹下的代码,而在public文件夹下还有个index.html这个文件会最终被打包放到www目录下,因此可以在这个文件中deviceready时添加全局的插件变量(注意该类全局变量的唯一性,可以添加plugin前缀或使用命名空间等方式保证),并将值传给src目录下的代码中,这样即可绕过控制台build以及调试时的报错。 ...

June 26, 2019 · 1 min · jiezi

重构的基本认知

本文属于原创文章,转载请注明--来自桃源小盼的博客 代码不可能在第一次就写得完美,这是一个持续修改的过程,那么应该怎么来进行呢?以下内容来自《重构-改善既有代码的设计》 是什么好代码的检验标准就是人们是否能轻而易举地修改它。由于预先做出良好的设计非常困难,想要既体面又快速地开发功能,重构必不可少。重构的意义就在于:你永远不必说对不起,只要把出问题的地方修补好就行了。重构过程的精髓所在:小步修改,每次修改后就运行测试。重构的最佳时机就在添加新功能之前。我不专门安排一段时间来重构,而是在添加功能或修复bug的同时顺便重构。与其猜测未来需要哪些灵活性,需要什么机制来提供灵活性,我更愿意只根据当前的需求来构造软件。做什么数据结构才是一个健壮程序的根基。消除重复。函数是我们将程序拆分成小块的主要方式。根据一个函数的意图(做什么)来对它命名。只要改名能够提升代码的可读性,那就应该毫不犹豫去做。如果某些数据和某些函数总是一起出现,某些数据经常同时变化甚至彼此相依,这就表示你应该将它们分离出去(比如提炼类)。一个好的模块化的设计,"封装"是最关键的特征之一。"封装"意味着每个模块都应该尽可能少了解系统的其他部分。尽量遵循命令与查询分离原则。大部分条件逻辑只用到了基本的条件语句,但如果发现复杂条件逻辑,多态是改善这种情况的有力工具。合理的继承关系是在程序演化的过程中才浮现出来的:我发现了一些共同元素,希望把它们抽取到一处,于是就有了继承关系。注意什么数据被使用得越广,就越是值得花精力给它一个体面的封装。如果可访问范围变大,重构的难度就会随之增大,这也是说全局数据是大麻烦的原因。将一个值用于多个不同的用途,这就是催生混乱和bug的温床。如果你不知道该做什么,这才是注释的良好运用时机。重构过程的性能问题:大多数情况下可以忽略它,如果有性能损耗,先完成重构,再做性能优化。如果重写比重构还容易,就别重构了。一直在追问自己要不要总结一篇这样偏理论性的文章。其实大部头的书不太容易静下心来读,那么我就把一些基本的知识晒出来,让看到的人产生思考,然后能去读原书。就算不读这本421页的《重构》也能对重构有一个基本的了解,方便在日后遇到问题时,知道去哪里寻找答案。 原书总结了常用的上百种重构手法,如果真正理解了什么是重构,自己也可以创造一些手法,实际的业务场景才是重构的最好战场。

June 15, 2019 · 1 min · jiezi

重构一项常常被忽略的基本功

讲真,我不是在本书上架的第一时间就入手的(你个假粉丝)。本月初的时候朋友和我说《重构》出第 2 版了,我才兴冲冲地下单,花了一个礼拜时间一口气把它读完后,才有了这篇书评。掩卷沉思,我无比赞同豆瓣网友“天心一”的评论: 这本书虽然很流行,但是应该看它而没有看的人,还是太多太多了。一个老读者的自白作为一个开发者,2012年初识本书的时候,我在写 Java;2019年本书再版,我在写 JavaScript。真是应了那句老话儿:“凡是可以用 JavaScript 来写的应用,最终都会用 JavaScript 来写。” JavaScript 特别适合重构,因为它很容易写的无法维护。  当然这只是个玩笑,实际上作者也解释过:重构背后的理念和架构适用于任何编程语言,选择 JavaScript 只是因为它应用的比较广泛。无论使用哪种编程语言都可以写出优秀的或者糟糕的代码,同样也都可以以本书的思路和技巧进行重构。 使用 JavaScript 展示代码范例,并不意味这本书中介绍的技巧只适用于JavaScript。 对比新旧两版,作者“重构”了这本书:前几章有所扩展,后几章结构调整较大,移除了原来的 12-14 章。总的来说,重构后的第 2 版更接地气、更适应时代:不再有“大型重构”,更多地聚焦操作的细节。 “Fowler 先生不仅没有拔高,反而把功夫做得更扎实了。”——摘自译者序虽然本书的副标题是“改善既有代码的设计”,但通读全书之后,我觉得这本书对于设计新系统时如何避免“坏味道”也是很有指导意义的。 重构和敏捷开发是一对亲兄弟提重构就不能不提敏捷开发,马丁·福勒本身就是敏捷开发的发起者之一。敏捷作为“当红炸子鸡”,与重构有着很多相似的地方。 一是,这两者都容易成为“挂羊头,卖狗肉”中的“羊头”,很多情况下,所谓的重构就是抽出时间来重写现有的几乎无法维护的代码,就如同很多“敏捷”只做到了“不拒绝需求变更”而没有真正做到响应变化;二是,它们实现起来都是一定难度且它们的实践过程可以是交叉的——它们都着眼于具体细节而不是空架子,都欢迎变化,都强调小步快走、持续改进;三是,敏捷开发很重要的两个环节就是设计与重构,两者相辅相成,彼此互补,在实践的过程中保持较强的适应力。 重构的技巧可以说,我在重构过程中遇到的问题大多都能在本书中找到答案。 我们看看作者对重构的定义: 重构(名词):对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。重构(动词):使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构。 为何重构、如何重构、重构的原则与手法,都可以在这本书中找到。从第 5 章起作者提供了多达 300 页的重构名录、60 余项重构的具体技巧(老版本是 70 多项,新版本移除了大规模项目的重构)。我觉得这一份非常详尽的重构手法清单更接近于字典,适合粗读之后在用到的时候再具体查阅。 至于什么时候能够用到这份名录,作者在第 3 章也有介绍:当代码有了“坏味道”就可以着手进行重构了。所谓“坏味道”,我认为并非是一程不变的准则,而是需要根据团队、项目、采用的技术栈等各方面综合得出的一种无法定量描述的经验。所以,作者用了“味道”这样一种体验来代指需要重构的地方。在作者列出的每种“坏味道”中,都给出了对应的重构手法。虽然作者罗列的 20 多种“坏味道”覆盖面很广,但是你和你的团队仍然可以总结出自己的经验来指导重构。实际上,与第 1 版相比,第 2 版中的“坏味道”增加了“神秘命名”“全局数据”“循环语句”,删除了“不完美的库类”。 我认为本书最重要也最容易被忽略的章节就是第 4 章——构筑测试体系。在第 4 章中,作者通过一个生产计划的示例一步一步的构建了一个完整的单元测试体系。显然,掌握单元测试是有一定成本的,这就导致有些开发者(尤其是前端领域)完全不注重单元测试。他们认为测试是QA的职责,自己只需要保证冒烟测试通过即可。然而反直觉的是,良好的单元测试不但是重构的先决条件和好帮手,而且能帮我们整理设计的思路,从而更好的写出优秀的代码。因为在写单元测试的时候,我们会假设自己是一个“代码破坏者”,思考如何破坏代码的运行、寻找那些可能出错的边界条件。单元测试的编写和运行可以在写完代码后进行,也可以在写代码之前动手。先写单元测试再写代码的技巧叫作测试驱动开发(TDD),也是敏捷开发的基石之一。关于TDD的技艺,作者的好友 Kent Beck 专门写了一本书,即《测试驱动开发》。 作者在第 1 章的示例中提到:“小步快走,代码永远处于可工作状态。”而且作者特意强调:“每当我要进行重构的时候,第一个步骤永远相同:我得确保即将修改的代码拥有一组可靠的测试。” 对于单元测试,我有一点小小的心得可以与大家分享:尽量编写纯函数。纯函数是没有副作用的函数,给出同样的参数值,纯函数总是返回同样的结果,它不依赖于参数以外的值。显然,纯函数更便于单元测试。 当然单元测试也不是万能的,它不可能检出所有的bug,而且单元测试集的覆盖率也是一个见仁见智的指标,具体需要写多少单元测试,覆盖多少代码,都是需要我们在开发中结合实际情况自己权衡的。无论如何,单元测试一直是一中非常重要却常常被忽视的技能。 另外,我在开发实践中坚持一个“432”的原则,供大家参考: 一个类包括注释代码不要超过400行;一个纯函数最好不要超过30行;函数内循环嵌套最多2层。重构的现状有些朋友对“重构”是不支持甚至是深恶痛绝的。 一部分开发者不愿意把精力“浪费”在重构上他们觉得重构是“给飞行中的飞机修引擎”,有可能出现很多问题却带不来多少拿得出手的成绩;重构总是会在“不经意间”破坏原有功能,带来的麻烦很多,投入与收益完全不成比例,也很少会是面试的重点,花精力在这上面实在是费力不讨好。 许多leader反对盲目重构在创业公司里基本不会有重构的呼声,原因无须赘言;而在一些大企业里,leader们也不是都喜欢重构,因为花时间重构意味着占用了开发新功能的时间,在代码还能跑起来甚至看起来跑得还不错的时候去重构无疑是画蛇添足;与重构带来的风险相比,重构带来的好处就不是那么有说服力了。 大部分QA对重构持谨慎的质疑态度代码的变动意味着需要进行回归测试,而敏捷当道的时代,每个迭代中QA的关注重点都在新功能上,能够分配给回归测试的精力很有限,而在测试通过后的重构极有可能导致此次变更对QA不透明,无形中增加了上线的风险。 我认为以上几种反对重构的场景都是不恰当的重构导致的。 ...

June 4, 2019 · 1 min · jiezi

重构你可能不知道的重构场景

什么是重构?“重构”一词想必你已经听腻了,就是整理代码呗,不不不,重构旨在不改变调用者行为的前提下,对内部逻辑进行调整优化,提高其理解性,降低其修改成本,它是一门艺术,是程序员至高无上的荣耀…… 何时重构?怎么重构?经常听到周边的人抱怨没有时间重构,重构并不是单独抽出时间集中处理的,而是当你想要做某个功能时,随手把需要重构的地方安排了。 逻辑重复重复代码是最核心常见的预警信息,如果有两个及以上的重复逻辑,就应该考虑将其合并。比如同一类或不同类中的函数存在相同逻辑的部分,就应该把相同部分抽象为独立函数或类。 长函数应该有很多同学经手过别人数百行甚至上千行的代码,让人质疑人生。为方便理解,最好的方式是把长函数分解为若干小函数,搭配上易理解的函数名,便可以像自然语言一样理解代码。 参数过多有一种习惯非常不好,就是把所有要用到的变量当做函数的参数,这样会加剧代码的理解难度,拓展极其困难,当需要更多数据时,不得不修改所有函数的参数,牵一发动全身。如果把对象作为参数,需要用到的数据都放进对象里,就可以有效解决参数过长的问题。 函数出轨你要是发现一个函数频繁的调用某一个类,它很可能给你戴了绿帽子,不如忍痛割爱,放其自由吧,把函数归并到它喜欢的类,也许他们在一起生活更为合适,你一定会找到一个适合的人。 变化扩散如果新加入一个业务类型(例如支付渠道、数据库类型等)时,需要改动很多地方才能实现,这就意味着还有改进的空间,可以将引起变化的原因抽出来做为配置,并将变化的函数放置到一个类中,这样不仅可以做到修改一处就应对变化,还可以很清晰的知道哪些函数会受到影响。 工具小助手一款语言包含很多基本类型与内置函数,但不能满足所有需求,比如金额单位转换、时间数组格式转换、UUID生成等简单又容易忽略的小功能,如果这些功能出现的频率很高,规则改变会带来一连串的修改,这时可以考虑将这些小功能抽象为工具函数,并将这些函数组合为工具类。 意淫的功能有些逻辑以为将来会有一些变化,于是安插了很多钩子函数应对非必要的特殊情况,这样往往提高了系统复杂性和理解成本,如果安插的钩子都能被用到且有价值,那么就使用,否则还是不要放在代码里阻碍视线了。 switch过多假如现在要做一个支持微信、支付宝、招行等渠道的支付平台,需要对接不同渠道,因为不同渠道对接方式不同,就需要用switch来根据类型选择对应渠道的对接方式,但是很多地方都可能用到这个switch,一旦新渠道加入就要满世界的找哪里用到了switch。 可以将switch语句移植为独立的函数,将这些函数组成基类,case语句调用子类对应的函数,具体实现让子类去完成,这样支付渠道的增加和变更只需要修改一个类即可。 多余的类创建的每一个类,对于其他人来讲都是有理解成本的,如果曾经为某个变化所添加的类,在实际场景中并没有发生变化,那么就把这个类去掉吧,我们需要真正有价值、理解成本低的系统。 让人犯晕的变量一个类会设置一些为特殊情况设置的变量,这些变量不一定都会被使用,经手你代码的人还要猜测当时设置这些变量的目的,非常让人头大,不如把这些变量和相关函数单独放在一个类中,屏蔽具体细节,需要的功能通过函数来表达,会使功能扩展更高效。 幽灵类项目中偶尔会出现一些“幽灵类”,这些类没有做什么实际工作,只是负责调用其它的类,不如把这个“中间人”去掉,让实际要调用的那个类与调用者发生关系。 雷同的类如果两个类,其中某几个函数作用相同,名称不同,那就可以通过修改名称或移植函数的方式将两个相似的类保持一致,然后把两个类抽象出基类,以便扩展。 过多的注释注释多并不是一件坏事,它是重构的领路人,当你感觉需要为某段代码写上注释时,这意味着你认为这段代码不容易被他人理解,也侧面证明了这就是重构发出的预警信号,所以当想要写注释时,就先重构,争取让注释都变得多余。 如果你喜欢本文,可以关注微信公众号“关爱程序员社区”(icoder_club),干货更不停

May 13, 2019 · 1 min · jiezi

模块融合中的一些思考

合久必分,分久必合。模块拆分通常并非架构RD因重构而自发的,新的业务需求或产品线要求快速上线,从现有成熟模块迁移过来,稍加改动就能实现目标,这常常皆大欢喜。然而,随着不同产品线架构的迭代和调整,两个同源模块的差异越来越大,但各个模块内部似乎仍能保持良好的框架和模块。也许是RD固有的处女座自虐倾向,或许是对架构统一的偏执,在经过各自发展后的两个模块现在提出融合要求,不管是对人力成本的考虑还是对形成僵尸代码的担忧,总之,一旦融合提上日程,这对两个模块以及负责融合的RD来讲,都是一场灾难。融合是一项吃力不讨好的工作,容易背锅,没有“收益”。本文是这个悲惨的RD在融合过程中的一些思考和总结(待融合模块10w+代码行)。1、融合需要对模块有极为细致的理解。融合需要对两个模块都有较为深入的理解,融合通常由其中一个模块的RD负责,对另一模块并不一定十分了解,那么就需要特别注重融合前期的准备工作。一方面,对模块的架构模型、关键流程、实现方式进行理解和分析(全局);另一方面,对模块的实现细节、内部结构、流程分支进行梳理和记录(细节)。这个过程是十分耗时的,但是对后续融合工作的开展提供了坚实的基础。2、融合的原则是什么?融合的原则总结为以下几点:1)能够复用的结构、功能和流程尽量复用,避免添加特有逻辑破坏模块架构的统一性;2)相对独立的流程尽量保持代码的独立性,避免模块后续调整带来的高耦合问题;3)合理屏蔽融合过程中不需要的流程和函数,避免添加一堆if/else,合理利用现有条件空转过滤。3、推荐的开发方式。融合常常是一个漫长的过程,同时,在融合过程中模块还会不断进行架构的迭代开发,因为,融合工作推荐采用快速迭代开发方式。开发、测试、上线、开发……,合理划分出每个阶段的边界,不管是对开发者、评审人、QA来说都更加清晰,上线风险也能够有效管控。而被融合的模块在融合工作完成后,再由QA进行全面的系统、性能测试,保证两者的功能均能达到预期。4、融合对模块的影响。首先,融合后最基本的要求是原有功能不受影响,相互独立实现各自功能;需要格外关注的是,待融合的两个模块的性能变化。一般情况下,融合后由于模块中处理流程更加复杂,处理时延会有明显增加;另外,内存使用量、CPU也将会有性能上的损失,一方面要和融合前独立模块时的性能进行比较,另一方面,要考虑如果部署集群也统一运维,集群在接入新流量后是否还能扛住。

February 27, 2019 · 1 min · jiezi

代码重构那些事儿

大家好,这是我今天演讲的目录,分Java,JavaScript,ABAP三门编程语言来讲述。Java•JAD•javap•Java Decompiler•Source Monitor•Visual VM•Refactor Menu in EclipseABAP•Code inspector•Refactor feature in AIE•Code coverageJavaScript•ESLint for Fiori Apps•Check Jenkins build log•JSlint for Sublime Text 2•Code check in WebIDE•Profile in Chrome在方法里引入一个布尔类型的参数控制方法的行为,这种做法正确吗?看看stackoverflow上是怎么说的。Java里定义常量的最佳实践:http://developer.51cto.com/ar…Java里这两种定义常量的方法,哪种更好?package one;public interface Constants { String NAME = “孙悟空”; int BP = 10000;}或package two;public class Constants { public static final String NAME = “贝吉塔”; public static final int BP = 9000;}为什么我们不应该在Java 接口中使用Array:https://eclipsesource.com/blo…避免Array的原因之一:Array若使用不当,会造成性能问题避免Array的原因之一:Array若使用不当,会造成性能问题避免Array的原因之二:Array是面向过程编程领域的概念,使用Java面向对象的集合类,比如List,而不是Array看个具体例子:String[] array = { “乔布斯”, “张小龙” };List list = Arrays.asList( array );System.out.println( list );// 打印输出 [乔布斯, 张小龙]System.out.println( array );// -> [Ljava.lang.String;@6f548414list.equals( Arrays.asList( “乔布斯”, “张小龙” ) )// -> truearray.equals( new String[] { “乔布斯”, “张小龙” } )// -> false看出差距了吧?Arrays不是类型安全的!下面的代码能通过编译,但是运行时会报ArrayStoreException的异常:Number[] numbers = new Integer[10];numbers[0] = Long.valueOf( 0 ); 而使用JDK的集合类比如List,就能在编译器即检测出这类错误。Javascript里有趣的逗号function a() { console.log(“I was called!”); return “Jerry”;}var b = a(), a;然后执行下面的代码:console.log(b);会打印出Jerry再看这段代码:var d = (function c(){ return a(),a;})();console.log(d);会打印出:I was called!function a() { console.log(“I was called!”); return “Jerry”;}再看这段代码呢?(function() { var e = f = 1;})();直接报错:Uncaught ReferenceError: f is not definedJavaScript里有趣的分号var b = function(para) { return { doSomething: function() { console.log(“hello: " + para); return para; } }}var a = 1, x = 3, y = 4, ss = a + b(x + y).doSomething() // 打印出 hello: 7console.log(s) // 打印出 8function test(i){ var result = i++; return result}console.log(“test: " + test(3)) // 打印出undefined继续看这段代码s = function(x){ console.log(“called: " + x ); return x}(1 + 2).toString()s = function(x){ console.log(“called: " + x ); return x}(1 + 2).toString()// 打印出 called: 3小技巧 - 如何把您自己增强逻辑植入到legacy遗留代码中var bigFunction = function() { // big logic console.log(“big logic”); // 这句话模拟我们在一段很冗长的遗留代码里植入自己的新逻辑}// 下面这种解决方案不会直接修改遗留函数本身,显得比较优雅var _old = bigFunction;bigFunction = function() { if ( _old ) { _old(); } console.log(“our own enhancement”);}bigFunction();// 第三种解决方案采用了面向切片编程思想,显得更加高级var bigFunction = function() { // big logic console.log(“big logic”);}bigFunction = ( bigFunction || function() {} ).after( function() { console.log(“our own logic”);});bigFunction();如何优雅的在一个函数里增添性能测试统计的工具代码var append_doms = function() { var d = new Date(); // dirty code - nothing to do with application logic!!! for( var i = 0; i < 100000; i++) { var div = document.createElement( “div”); document.body.appendChild(div); } // dirty code - nothing to do with application logic!!! console.log(” time consumed: " + ( new Date() - d));};function test() { append_doms();}传统方案:在充满了业务逻辑的函数体里强行加入红色标准的搜集性能测试的工具代码,这个实现显得很丑陋:再看看采用面向切片编程思路的解决方案:AOP - Aspect Oriented Programmingvar append_doms = function() { for( var i = 0; i < 100000; i++) { var div = document.createElement( “div”); document.body.appendChild(div); }};var log_time = function( func, log_name) { return func = ( function() { var d; return func.before( function(){ d = new Date(); }).after( function(){ console.log( log_name + ( new Date() - d)); }); })(); };function test() { log_time(append_doms, “consumed time: “)();}如何避免代码中大量的IF - ELSE 检查在调用真正的OData API之前,系统有大量的IF ELSE对API的输入参宿进行检查:var send = function() { var value = input.value; if( value.length === ’’ ) { return false; } else if( value.length > MAX_LENGTH) { return false; } … // lots of else else { // call OData API }}更优雅的解决方案:把这些不同的检查规则封装到一个个JavaScript函数里,再把这些函数作为一个规则对象的属性:var valid_rules = { not_empty: function( value ) { return value.length !== ‘’; }, max_length: function( value ) { return value.length <= MAX_LENGTH ; } }实现一个新的检查函数,变量检查对象的属性,执行校验逻辑:var valid_check = function() { for( var i in valid_rules ) { if ( vali_rules[i].apply( this, arguments) === false ) { return false; } }}现在的OData调用函数非常优雅了:var send = function( value ) { if ( valid_check( value ) === false ) { return; } // call OData API}通过这种方式消除了IF ELSE。另一种通过职责链 Chain of Responsibility 的设计模式 design pattern消除IF ELSE分支的代码重构方式:先看传统方式的实现:// Priority: ActiveX > HTML5 > Flash > Form(default)function isActiveXSupported(){ //… return false;}function isHTML5Supported(){ //… return false;}function isFlashSupported(){ //… return false;}好多的IF -ELSE啊:var uploadAPI;if ( isActiveXSupported()) { // lots of initialization work uploadAPI = { “name”: “ActiveX”};}else if( isHTML5Supported()) { // lots of initialization work uploadAPI = { “name”: “HTML5”};}else if( isFlashSupported()) { // lots of initialization work uploadAPI = { “name”: “Flash”};}else { // lots of initialization work uploadAPI = { “name”: “Form”};}console.log(uploadAPI);再看职责链设计模式的实现:Chain of Responsibilityvar getActiveX = function() { try { // lots of initialization work return { “name”: “ActiveX”}; } catch (e) { return null; }}var getHTML5 = function() { try { // lots of initialization work return { “name”: “HTML5”}; } catch (e) { return null; }}代码整洁优雅:var uploadAPI = getActiveX.after(getHTML5).after(getFlash).after(getForm)();console.log(uploadAPI);Java中的Stringpublic class stringTest {public static void main(String[] args) { String userName = “Jerry”; String skill = “JS”; String job = “Developer”; String info = userName + skill + job; System.out.println(info);}}用javap将上面的Hello World程序反编译出来学习:要获取更多Jerry的原创文章,请关注公众号"汪子熙”: ...

February 3, 2019 · 4 min · jiezi

编写可维护的代码

编写可维护的代码前言我们在修改他人代码的时候,阅读他人代码所花的时间经常比实现功能的时间还要更多如果程序结构不清晰,代码混乱 。牵一发而动全身。那维护起来就更难维护了可读性可理解性:他人可以接手代码并理解它直观性 : 代码逻辑清晰可调试性 :出错时,方便定位问题所在如何提高可读性代码格式化适当添加注释函数与方法大段代码注释需有意义如何优化代码找出代码的坏味道使用重构手法将其解决掉代码的坏味道在我们的程序中,可以闻到很多的坏味道。主要有以下这些点命名不规范或无意义命名存在使用缩写、不规范、无意义例子:var a = xxx,b = xxx重复代码相同(或相似)的代码在项目中出现了多次,如果需求发生更改,则需要同时修改多个地方过长函数程序越长越难理解,一个函数应该只完成一个功能过长的类一个类的职责过多,一个类应该是一个独立的整体。过长参数列表太长的参数列表难以理解,不易使用。当需要修改的时候,会更加容易出错数据泥团有些数据项总是成群结队的待在一起。例如两个类中相同的字段、许多函数签名相同的参数。这些都应该提炼到一个对象中,将很多参数列缩短,简化函数调用类似的函数整体上实现的功能差不多,但是由于有一点点区别。所以写成了多个函数重构手法提炼函数针对一个比较长的函数,提炼成一个个完成特定功能的函数。例子// 提炼前function test11() { var day = $(‘day’); var yearVal = ‘2016’; var monthVal = ‘10’; var dayVal = ‘10’; day.val(dayVal); switch (monthVal) { case 4: case 6: case 9: case 11: if (dayVal > 30) { day.val(30); } break; case 2: if ( yearVal % 4 == 0 && (yearVal % 100 != 0 || yearVal % 400 == 0) && monthVal == 2 ) { if (dayVal > 29) { day.val(29); } } else { if (dayVal > 28) { day.val(28); } } break; default: if (dayVal > 31) { day.val(31); } }}// 提炼后function test12() { var day = $(‘day’); var yearVal = ‘2016’; var monthVal = ‘10’; var dayVal = ‘10’; var maxDay = getMaxDay(yearVal, monthVal); if (dayVal > maxDay) { day.val(maxDay); } else { day.val(dayVal); }}function getMaxDay(year, month) { var maxDay = 0; switch (month) { case 4: case 6: case 9: case 11: maxDay = 30; break; case 2: if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) { maxDay = 29; } else { maxDay = 28; } break; default: maxDay = 31; } return maxDay;}例子中,提炼前的代码,需要很费劲的看完整个函数,才会明白做了什么处理,提炼后的代码。只需要稍微看一下,就知道 getMaxDay 是获取当前月份的最大天数优点:如果每个函数的粒度都很小,那么函数被复用的机会就更大;这会使高层函数读起来就想一系列注释;如果函数都是细粒度,那么函数的覆写也会更容易些内联函数有时候,一个函数的本体与函数名一样简单易懂,就要用到这种手法。这种手法用于处理优化过度的问题举个例子:function biggerThanZero(num) { return num > 0;}function test() { var num = 10; if (biggerThanZero(num)) { //do something }}//内联后function test() { var num = 10; if (num > 0) { //do something }}引入解释性变量当表达式比较复杂难以阅读的时候,就可以通过临时变量来帮助你将表达式分解为容易管理的形式有些时候,运用提炼函数会更好一点举两个简单的例子:// 例子 1// beforefunction test2() { if ( platform.toUpperCase().indexOf(‘MAC’) > -1 && browser.toUpperCase().indexOf(‘IE’) > -1 && wasInitialized() && resize > 0 ) { // do something }}// afterfunction test2() { var isMacOs = platform.toUpperCase().indexOf(‘MAC’) > -1; var isIEBrowser = browser.toUpperCase().indexOf(‘IE’) > -1; var wasResized = resize > 0; if (isMacOs && isIEBrowser && wasInitialized() && wasResized) { // do something }}// ————————————————–// 例子2// beforefunction caluPrice(quantity, itemPrice) { return ( quantity * itemPrice - Math.max(0, quantity - 500) * itemPrice * 0.05 + Math.min(quantity * itemPrice * 0.1, 100) );}// afterfunction caluPrice(quantity, itemPrice) { var basePrice = quantity * itemPrice; var discount = Math.max(0, quantity - 500) * itemPrice * 0.05; var shiping = Math.min(basePrice * 0.1, 100); return basePrice - discount + shiping;}在两个例子中,引入解释性的变量之后,可读性大大增加。函数的意图就比较明显,单看变量命名就已经能大概知道具体的实现分解临时变量除了 for 循环里用来收集结果的变量,其他的临时变量都应该只被赋值一次。因为被赋值超过一次,就意味着他在函数中承担了多个责任。一个变量承担多个责任。会令代码看起来容易迷惑举个例子:// 分解临时变量// beforefunction test3() { var temp = 2 * (width + height); console.log(temp); // do something temp = height * width; // do something console.log(temp);}// afterfunction test4() { var perimeter = 2 * (width + height); console.log(perimeter); // do something var area = height * width; // do something console.log(area);}在这个例子中,temp 分别被赋予了两次,如果代码块较长的情况,会增加风险,因为你不知道他在哪里被改掉了替换算法当你重构的时候,发现实现同样的功能有一个更清晰的方式,就应该将原有的算法替换成你的算法。举个例子:// 替换算法// beforefunction getWeekDay() { var weekStr = ‘’; switch (date.format(’d’)) { case 0: weekStr = ‘日’; break; case 1: weekStr = ‘一’; break; case 2: weekStr = ‘二’; break; case 3: weekStr = ‘三’; break; case 4: weekStr = ‘四’; break; case 5: weekStr = ‘五’; break; case 6: weekStr = ‘六’; break; } return weekStr;}// afterfunction getWeekDay() { var weekDays = [‘日’, ‘一’, ‘二’, ‘三’, ‘四’, ‘五’, ‘六’]; return weekDays[date.format(’d’)];}以字面常量取代魔法数(eg:状态码)在计算机科学中,魔法数是历史最悠久的不良现象之一。魔法数是指程序中莫名其妙的数字。拥有特殊意义,却又不能明确表现出这种意义的数字举个例子:// beforefunction test5(x) { if (x == 1) { console.log(‘完成’); } else if (x == 2) { console.log(‘上传中’); } else if (x == 3) { console.log(‘上传失败’); } else { console.log(‘未知的错误’); }}function test6(x) { if (x == 3) { // do something }}// aftervar UploadStatus = { START: 0, UPLOADING: 1, SUCCESS: 2, ERROR: 3, UNKNOWN: 4};function test7(x) { if (x == UploadStatus.START) { console.log(‘未开始’); } else if (x == UploadStatus.UPLOADING) { console.log(‘上传中’); } else if (x == UploadStatus.SUCCESS) { console.log(‘上传成功’); } else if (x == UploadStatus.ERROR) { console.log(‘上传失败’); } else { console.log(‘未知的错误’); }}function test8(x) { if (x == UploadStatus.ERROR) { // do something }}对于魔法数,应该用一个枚举对象或一个常量来赋予其可见的意义。这样,你在用到的时候,就能够明确的知道它代表的是什么意思而且,当需求变化的时候,只需要改变一个地方即可分解条件表达式复杂的条件逻辑是导致复杂度上升的地点之一。因为必须编写代码来处理不同的分支,很容易就写出一个相当长的函数将每个分支条件分解成新函数可以突出条件逻辑,更清楚表明每个分支的作用以及原因举个例子:// 分解条件表达式// 商品在冬季和夏季单价不一样// beforevar SUMMER_START = ‘06-01’;var SUMMER_END = ‘09-01’;function test9() { var quantity = 2; var winterRate = 0.5; var winterServiceCharge = 9; var summerRate = 0.6; var charge = 0; if (date.before(SUMMER_START) || date.after(SUMMER_END)) { charge = quantity * winterRate + winterServiceCharge; } else { charge = quantity * summerRate; } return charge;}// afterfunction test9() { var quantity = 2; return notSummer(date) ? winterCharge(quantity) : summerCharge(quantity);}function notSummer(date) { return date.before(SUMMER_START) || date.after(SUMMER_END);}function summerCharge(quantity) { var summerRate = 0.6; return quantity * summerRate;}function winterCharge(quantity) { var winterRate = 0.5; var winterServiceCharge = 9; return quantity * winterRate + winterServiceCharge;}合并条件表达式当发现一系列的条件检查,检查条件不一样,但是行为却一致。就可以将它们合并为一个条件表达式举个例子:// 合并条件表达式// beforefunction test10(x) { var isFireFox = ‘xxxx’; var isIE = ‘xxxx’; var isChrome = ‘xxxx’; if (isFireFox) { return true; } if (isIE) { return true; } if (isChrome) { return true; } return false;}// afterfunction test10(x) { var isFireFox = ‘xxxx’; var isIE = ‘xxxx’; var isChrome = ‘xxxx’; if (isFireFox || isIE || isChrome) { return true; } return false;}合并后的代码会告诉你,实际上只有一个条件检查,只是有多个并列条件需要检查而已合并重复的条件片段条件表达式上有着相同的一段代码,就应该将它搬离出来// 合并重复片段// beforefunction test11(isSpecial) { var total, price = 1; if (isSpecial) { total = price * 0.95; // 这里处理一些业务 } else { total = price * 0.8; // 这里处理一些业务 }}// afterfunction test12(isSpecial) { var total, price = 1; if (isSpecial) { total = price * 0.95; } else { total = price * 0.8; } // 这里处理一些业务}在不同的条件里面做了同样的事情,应该将其抽离出条件判断。这样代码量少而且逻辑更加清晰以卫语句取代嵌套条件表达式如果某个条件较为罕见,应该单独检查该条件,并在该条件为真时立即从函数中返回。这样的检查就叫卫语句举个例子:// 以卫语句取代嵌套条件表达式// beforefunction getPayMent() { var result = 0; if (isDead) { result = deadAmount(); } else { if (isSepartated) { result = separtedAmount(); } else { if (isRetired) { result = retiredAmount(); } else { result = normalPayAmount(); } } } return result;}// afterfunction getPayMent() { if (isDead) { return deadAmount(); } if (isSepartated) { return separtedAmount(); } if (isRetired) { return retiredAmount(); } return normalPayAmount();}函数改名(命名)当函数名称不能表达函数的用途,就应该改名变量和函数应使用合乎逻辑的名字。eg:获取产品列表 -> getProductList()变量名应为名词,因为变量名描述的大部分是一个事物。eg: 产品 -> product函数名应为动词开始,因为函数描述的是一个动作eg:获取产品列表 -> getProductList()将查询函数和修改函数分开如果某个函数只向你提供一个值,没有任何副作用。这个函数就可以任意的调用。这样的函数称为纯函数如果遇到一个既有返回值,又有副作用的函数。就应该将查询与修改动作分离出来举个例子:// beforefunction test13(people) { for (var i = 0, len = people.length; i < len; i++) { if (people[i].name == ‘andy’) { // do something 例如进行DOM 操作之类的 return ‘andy’; } if (people[i].name == ‘ChunYang’) { // do something 例如进行DOM 操作之类的 return ‘ChunYang’; } }}// afterfunction test14(people) { var p = find(people); // do something 例如进行DOM 操作之类的 // doSomeThing(p);}function find(people) { for (var i = 0, len = people.length; i < len; i++) { if (people[i].name == ‘andy’) { return ‘andy’; } if (people[i].name == ‘ChunYang’) { return ‘ChunYang’; } }}令函数携带参数如果发现两个函数,做着类似的工作。区别只在于其中几个变量的不同。就可以通过参数来处理。这样可以去除重复的代码,提高灵活性关键点: 找出不同的地方和重复的地方。推荐书籍《重构 改善既有代码的设计 》 基于 java 的《代码大全》相关链接个人博客代码片段 ...

December 20, 2018 · 5 min · jiezi

读完这篇文章,就能拥有炫同事一脸的超能力:JavaScript 魔幻代理

前言什么是代理?上小学的时候,李小红来你家叫你出去玩,第一个回应的不是你自己,是你妈:“王小明在家写作业,今天不出去!”上中学的时候,赵二虎带着小弟们放学在校门口等着揍你,走在前面的不是你自己,是二虎他爸:“考试没及格还学会装黑社会了!”拎起二虎就是一顿胖揍。上了大学,躺在宿舍里的床上,好饿。出门买饭并交代好不要葱蒜多放辣最后还直接端到床上的不是你自己,是快递小哥。这些都是代理。什么是 JavaScript 代理?用官方的洋文来说,是 Proxy:The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc).通过 Proxy 我们可以拦截并改变一个对象的几乎所有的根本操作,包括但不限于属性查找、赋值、枚举、函数调用等等。在生活中,通过代理我们可以自动屏蔽小红的邀请、自动赶走二虎的威胁、自动买好干净的饭端到床上。在 JavaScript 世界里,代理也可以帮你做类似的事情,接下来让我们一起琢磨一番。初识代理:Hello World以小学经历为例子,心里是喜欢小红的,于是我们定义:const me = { name: ‘小明’, like: ‘小红’ }这个时候如果调用 console.log(me.like),结果必然是 小红。然而生活并不是这样,作为一个未成年人,总是有各种的代理人围绕在你身边,比如这样:const meWithProxy = new Proxy(me, { get(target, prop) { if (prop === ’like’) { return ‘学习’; } return target[prop]; }});这个时候如果调用 console.log(me.like) 依然是 小红 ,因为真心不会说谎。但当我们调用 console.log(meWithProxy.like) 的时候,就会可耻的输出 学习 ,告诉大家说我们喜欢的是 学习 。小试牛刀:不要停止我的音乐刚才我们简单了解了代理能够拦截对象属性的获取,可以隐藏真实的属性值而返回代理想要返回的结果,那么对于对象属性的赋值呢?让我们一起来看看。假设你正在听音乐:const me = { name: ‘小明’, musicPlaying: true }此时如果我们执行 me.musicPlaying = false 这样就轻而易举地停止了你的音乐,那么如果我们挂上代理人:const meWithProxy = new Proxy(me, { set(target, prop, value) { if (prop === ‘musicPlaying’ && value !== true) { throw Error(‘任何妄图停止音乐的行为都是耍流氓!’); } target[prop] = value; }});这时候如果我们执行 me.musicPlaying = false,就会被毫不留情地掀了桌子:> meWithProxy.musicPlaying = falseError: 任何妄图停止音乐的行为都是耍流氓! at Object.set (repl:4:13)>释放魔法:封装全宇宙所有 RESTful API现在我们已经知道通过 Proxy 可以拦截属性的读写操作,那然后呢?没什么用?仅仅是拦截属性的读写操作,的确没有太大的发挥空间,或许可以方便的做一些属性赋值校验工作等等。但是,或许你还没有意识到一个惊人的秘密:Proxy 在拦截属性读写操作时,并不在乎属性是否真的存在!那么,也就是说:利用 Proxy,我们可以拦截并不存在的属性的读取。再进一步思考:利用 Proxy,我们可以在属性读取的那一瞬间,动态构造返回结果。然而,属性并不局限于字符串、布尔值,属性可以是对象、函数、任何东西。至此,你想到了什么?没想到?不要紧!根据刚才的分析,让我们一起通过下面 17 行代码,来封装全宇宙所有的 RESTful API !import axios from ‘axios’;const api = new Proxy({}, { get(target, prop) { const method = /^[a-z]+/.exec(prop)[0]; const path = ‘/’ + prop .substring(method.length) .replace(/([a-z])([A-Z])/g, ‘$1/$2’) .replace(/$/g, ‘/$/’) .toLowerCase(); return (…args) => { // <—— 返回动态构造的函数! const url = path.replace(/$/g, () => args.shift()); const options = args.shift() || {}; console.log(‘Requesting: ‘, method, url, options); return axios({ method, url, …options }); } }});定义了 api 这个代理之后,我们就可以像下面这样调用:api.get()// GET /api.getUsers()// 获取所有用户// GET /usersapi.getUsers$Books(42)// 获取 ID 为 42 的用户的所有书籍// GET /users/42/booksapi.getUsers$Books(42, { params: { page: 2 } })// 获取 ID 为 42 的用户的所有书籍的第二页// GET /users/42/books?page=2api.postUsers({ data: { name: ‘小明’ } })// 创建名字为 小明 的用户// POST /users Payload { name: ‘小明’ }以上所有的函数都在你调用的那一瞬间,通过代理人的魔法之手动态生成,供我们随意取用。简洁、优雅,哇~ 真是太棒啦!终极魔幻:通读代理人的魔法秘笈到此,我们仅仅使用 Proxy 改造了对象的属性获取、赋值操作,而对于 Proxy 来说,只是冰山一角。Proxy 的基本语法如下:new Proxy(target, handler)其中 target 是即将被代理的对象(比如:想要出门找小红玩耍的 me),handler 就是代理的魔法之手,用来拦截、改造 target 的行为。对于 handler 对象,我们刚才仅仅用到了 get、set 函数,而实际上一共有 13 种可代理的操作:handler.getPrototypeOf()在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。handler.setPrototypeOf()在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。handler.isExtensible()在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。handler.preventExtensions()在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。handler.getOwnPropertyDescriptor()在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, “foo”) 时。handler.defineProperty()在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, “foo”, {}) 时。handler.has()在判断代理对象是否拥有某个属性时触发该操作,比如在执行 “foo” in proxy 时。handler.get()在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。handler.set()在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。handler.deleteProperty()在删除代理对象的某个属性时触发该操作,比如在执行 delete proxy.foo 时。handler.ownKeys()在获取代理对象的所有属性键时触发该操作,比如在执行 Object.getOwnPropertyNames(proxy) 时。handler.apply()在调用一个目标对象为函数的代理对象时触发该操作,比如在执行 proxy() 时。handler.construct()在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy() 时。对于以上 13 种可代理的操作,还需要读者自行研究并实践方可踏上终极魔幻之旅。同学,我看好你。参考链接:Proxy - JavaScript | MDNHow to use JavaScript Proxies for Fun and Profit – DailyJS – Medium文 / 王小明本文已由作者授权发布,版权属于创宇前端。欢迎注明出处转载本文。本文链接:https://knownsec-fed.com/2018…想要订阅更多来自知道创宇开发一线的分享,请搜索关注我们的微信公众号:乐趣区。欢迎留言讨论,我们会尽可能回复。欢迎点赞、收藏、留言评论、转发分享和打赏支持我们。打赏将被完全转交给文章作者。感谢您的阅读。 ...

September 20, 2018 · 2 min · jiezi