组件设计之组合原则

表面上看起来这个优化工作包括两大部分:性能优化和结构重整。不过经过这几个月对十多个组件的重构之后,我们发现这两部分工作在很大程度上是同一件事的两个方面:好的设计往往可以带来更好的性能,反之亦然.

May 1, 2021 3 min

自动化重构 - jscodeshift

自动化重构(jscodeshift) 在这篇文章里我想要通过一些小例子来介绍使用jscodeshift来进行自动化重构的技术。具体来说,我想要介绍在一个组件库的开发和维护过程中,如何使用jscodeshift来自动修改公开的API接口,从而尽可能小的产生对组件用户的影响。 如果你们团队开发的组件被其消费者(组织内部或者外部)使用了,而这些代码又不在你的控制之内,那么这里讨论的技术和模式可能对你很有帮助。而如果你的日常工作更多的是使用组件库来开发应用程序,我希望这里的知识和技巧仍然对你有所启发,毕竟在软件系统中,我们往往都既是某些库的消费者,又同时是另外一些库的生产者。 从一个简单场景出发 设想这样一个场景,你发布了一个酷炫的组件库(fancylib),其中有一个按钮(Button)组件。这个Button的一个属性是当点击后处于加载中(loading)状态时现实一个表示加载中的小图标。 (图片来源:https://xd.adobe.com/ideas/process/ui-design/designing-interactive-buttons-states/) 在代码实现中,这个加载中状态被定义为了名为isInLoadingStatus公开prop。用户可以通过设置其值来控制Button的状态: import Button from '@fancylib/button'; const app = () => ( <Button isInLoadingStatus>Click me</Button> ) 一个实习生在某一天code review的时候提出了一个问题:在组件库中的其他地方,所有的boolean状态都是用一个单词来表示的,比如checked, disabled等。如果按照这个惯例,这里应该把isInLoadingStatus简化为loading。好主意! import Button from '@fancylib/button'; const app = () => ( <Button loading>Click me</Button> ) 假如所有用到Button的地方都在你的控制之内,字符串替换大约是一个快速且80%有效的方案。不过稍微分析一下,你就会发现简单的Shift+F6会遇到很多问题。 复杂情况 比如用户对其做了二次包装以适配更符合自己用户的使用习惯,这使得简单的全局字符串替换变成了不可能:: import Button as FancyButton from '@fancylib/button'; const MyEvenFancierButton = (props: FancyButtonProps) => ( const theme = { backgroundColor: "orangered", color: "white" }; <FancyButton {...props} theme={theme}>Click me</FancyButton> ); 除了这些问题之外,由于这是一个非常受欢迎的组件库,Button在很多(包括内部和外部的)产品中都有使用,你没有办法访问所有的用户代码,更没有办法让所有人都用手工的查找替换来做更新,你需要另寻出路。 你需要一个工具 – 一个可以读懂代码意图的工具 – 来帮助你做修改,而且整个过程最好可以自动化,比如通过执行一个脚本来完成。...

December 11, 2020 2 min

夜钓布莱顿与禅

夜钓布莱顿 如果你是一个严肃的钓者,你当然会知道,钓鱼并不想那些门外汉眼里那样仅仅是一项简单的运动。你更知道钓胜于鱼的道理。每次钓鱼,更像是一个项目:你需要事先(可能是数天之前)就开始计划,钓哪种鱼,用哪种钓法,采用的鱼饵,当天的天气,潮汐,气温/水温,大气压强,风力及风向等等,当然你也会将运气纳入考虑之中(虽然这是你无法控制的,这样至少当你两手空空离开钓场时心里舒坦一些)。 坐在水边,不论是鱼情如何,心中总是怀有一线若有若无,若即若离的希望。初学之时,轻微的鱼铃声都会惊动你的思绪,将你从葱姜蒜或者二荆条的迷思中拉回道黑暗且冷清的防波堤上。当你意识到那不过是小鱼在试饵时,又不得不开始怀疑它们是否已经将鱼饵偷走,从而不得不把线收回来再甩一次。你会不时的看看杆稍,细微的震动都令你兴奋 — 有时候甚至不是真的震动,而更多是你的幻觉 — 会不会有鱼在试探呢? 等到慢慢有了经验之后,鱼竿上细小的的震动再也提不起你的兴趣,你会视而不见,听而不闻,甚至当鱼上钩之后鱼竿开始大弯、震颤,你也会不慌不忙的走过去,然后将鱼杆抬起,杆稍朝上,在保持线是紧绷的前提下,缓缓收线,直到上鱼。 没有什么比将鱼提上岸时失去它更让人沮丧的事情了。不过这种事情时有发生,可能是线太细,也可能是钩子没有set好,也可能是其他故障,总之它会令你扼腕叹息,捶胸顿足。我有一次在land一条Port Jackson Shark的时候,被它拉断了鱼钩(注意不是鱼线,而是纯钢的鱼钩),不过好在这种鲨鱼并非食用鱼,我只是可惜没有拍张照片,并没有太难过。 而有一次印象深刻的则是丢了一条闪着若隐若现彩虹色彩的trevally,实实在在可以当鱼生的trevally。好容易将其拖进了岸边,结果由于钩子set的不是很牢固,被它挣脱了。这时候你心头有种欲说还休的惆怅,跌入谷底的失落,无以名状的悔恨,甚至歇斯底里的暴怒。 我在Williams Town的防波堤上钓鱼的时候,亲眼看到一个意大利中年男子在land一条40-50cm左右的Butter fish的时候,在提鱼上岸的一瞬间被它挣脱。那个瞬间他脑海里的那种快乐突然被人剥夺,恰如一根紧绷着的铉被人用利刃切断一般,他彻底疯了。站在风浪滔天的黑礁石上大声Fuck了大约有20遍后,他回头重新取出鱼线,开始制作线组,嘴里依然喃喃自语,Shit/Damn不休。 那时节,我有点担心他突然纵身一跃跳入狂风巨浪中一去不复返。不过当我收拾行囊准备撤离,经过他的时候,他明显的冷静了很多,低声自语:我当时应该松一手线的,但是我错误的大力提了一下。我似乎看到了他眼角依稀的泪光。 P.S. 发出来这篇纯属觉得《祭螃蟹》那篇太过孤单。

November 17, 2020 1 min

从创意到产品:ToBuy的故事

一个故事:从想法到产品 毫无疑问,新冠疫情在很多方面都深刻的影响了我们的生活。不论是出门必须戴口罩,在户外保持社交距离,还是外出回来要使用6步洗手法洗手,又或者由于远程办公而形成的物理隔离带来的心理上的负面影响等等,这些都深远的影响了我们的日常。 我今天要分享的一个故事正是与此相关:我是如何在维州宣布进入灾难状态的一个多月后,不得不全天几乎24小时呆在家里的情况下,从零开始进行一个iOS App开发并最终上线的故事。而这一切自然要从原始的需求分析开始说起。 原始需求分析 这个需求其实由来已久。在平时购物的时候,为了避免花费太多时间在超市,很多人都会随身携带一个购物清单,像下面这张图上列出来的那样: 大家平时买东西也可能有类似这样的纸质的购物清单吧。这种购物清单便于携带,随用随扔,非常方便。不过有几个小的缺点:一是买完这次之后,下次再去买的时候很可能卡片不知道丢到哪里去了;二是如果不配合笔一起使用的话,很可能要检查多次清单才能确定某样商品是否已经买过。此外,纸片上的信息毕竟有限,有时候你想要描述类似这样的需求:在永辉超市买甘竹牌豆豉鲮鱼,不要错买成鹰金钱,就会发现纸和笔可能不够用了。 我希望有一个手机App可以帮我管理这些清单。因为手机基本上是我出门必然会带的设备,手机本身可以作为纸笔Todo的替代品,而且有图片作为参考可以快速定位并找到我需要的商品。 另外一个典型的购物场景是:我每次购物往往会去多个超市,有的超市新鲜果蔬比较好,但是价格偏贵;而另一家则百货齐全,有时候水果不太新鲜;另外有一些商品比如豆腐乳,胡椒粉之类只能去亚洲超市才能买到。这时候我希望这个App可以提供分组,我可以在某家超市买完所有想要的商品之后,再换一家买剩余的。 灾难状态的封城在某些方面促进了这个想法的实现:首先被关在家里不能外出的我平白多了很多时间,用这段时间来学习一项新的技能显然是可行的;其次每次购物必须速战速决,尽量避免在人多的地方待太长时间而增加感染新冠的可能性;最后每个家庭每天只能有一个人出去购物,需要将清单尽可能细化然后实施(软件可以在某种程度上简化这个过程)。 当然,苹果自带的Notes或者Reminder都有类似的Todo管理,但是并不专门面向买菜。我设想的这个应用专门用来管理购物清单。在实施方面,我不打算从Hello world这样一步步从Swift的基本与反学起,而是先确定要做的App的主要功能,然后带着问题一边学习一边实践,最终把它演进成一个公开发布的工具。 实施 现在我终于到了我有一个价值千万的idea,就差一个程序员的阶段了。而巧的是,我就是个程序员。唯一的问题是,我的Swift和移动App开发经验约等于零(虽然多年前在项目间隔中参加过张帅的Android和黄磊的iOS workshop,但是时间隔得太久已经基本上还回去了)。此外,一个产品还有很多杂项,比如界面设计,图标,legal相关,App发布流程等等,这些对我来说完全都是未知数。 在实践中学习 通常来说,我比较喜欢通过循序渐进的方式来学习,从简单的例子开始,逐渐学习各个主题(如何设计界面,如何连接数据库,如何使用网络等等),这种方式的好处是基础会比较扎实,而且很多细节都可以得到充分练习。不过缺点是战线太长,往往需要耗时数月而没有实际产出。也因此如果过程中被一些其他事情耽搁,就很难再连续起来。 另一种方法是结果导向,然后用结果反过来驱动需要学习的内容。比如你想要开发一个本地的图片编辑App,那么肯定需要学习访问摄像头,访问本地文件系统等,而无需考虑网络和数据库。这种方式的优点是可以提供实时反馈,并且时刻有明确目标。 同样的,这种方法也有一些缺点: 会花费很多时间在Google和Stackoverflow上 很多资料都严重过时了,很多object-c的资料,以及很多swift老版本的资料 大部分人分享的内容,都比Hello world级别稍微复杂一点点,但是基本上不可能直接应用到你的应用中 因此在这个项目里,我尝试将两者结合起来。比如在第一个milestone里,我通过结果导向的方式,仅仅浮光掠影的学习必要的知识点,比如列表视图,iOS里的MVC,navigation之类。主要课程就是参考youtube上众多的入门教程:通过一个简单的例子,手把手的写一个Todo之类。这个过程可以熟悉编辑器,快捷键的使用,Swift的简单用法,界面设计器的使用等等。 这个阶段之后就可以开始“野蛮”编码了,开始的时候并没有特别的章法,就是照着例子写就好了。如果进行的顺利的话,很快就会有一个简单的原型出来,但是过程中会遇到海量的问题。小到如何添加图标,配置颜色,大到两个view之间如何通信,如何同步数据。不过作为一个有工程经验的程序员,大部分问题都可以通过Google+Stackoverflow解决。实在自己找不到答案的,还有很多热心的同事可以请教。 如果说我有一丁点成绩的话,无非是把别人用来思考变量名的时间花在了尝试Google关键字上了而已。 – 鲁迅 过了这一个阶段之后 - 有了一个可以工作的原型 - 就可以好好的打磨它了。于是我开始系统的学习udemy上的课程,比如如何使用使用protocol模式,如何使用MVVM,使用segue的正确方式等等。这时候可以适当的做一些重构和修一些简单的bug。 下图是从第一个原型到当前最新版本的用户界面的演进示意。从一开始的原生的TableView到定制的单元格,再到对数据按照超市信息分组并展现,每一次都比之前略复杂一些,而且每次的修改基本上都是基于客户的反馈来改进的。 此处的一个建议是:如果时间充裕,还是建议从基础学起,循序渐进,这样的方式可以确保基础比较牢固。而如果时间有限,又害怕被别的事情分心,那就可以边做边看,用到了再学,等建立起正向反馈之后再回过头来查漏补缺。 项目管理 在实施过程中我发现:一个可视化的,有迹可循的故事板非常有用。当然比这个故事板更加重要的是你需要制定一些规则并确保执行。比如,所有的卡片都应该从Backlog开始(即所有的需求都要经过仔细分析),开始前需要和stakeholder确认优先级,在开发过程中需要及时收集反馈,有明确的DoD(definition of done)等等。简而言之,尽量将其当成一个正式的项目来运作。 另外我还发现,之前在客户那里见到的诸多反敏捷的实践,我自己也往往会再犯一次。比如不及时更新卡的状态,临时插入Doing任务或者不限制在制品数量,不及时从stakeholder那里收集反馈,验收条件不明确等等。不过后来就好很多了,我尽量让其变得正式一些,而这个过程也确实可以帮助我更加聚焦在高优先级,更具价值的(而不是更fancy)的功能上。 这个大约是最简单,投入成本最小,又可以产生很好效果的实践之一了。特别是如果被一些临时任务中断之后(比如过了一个周末,或者项目上很忙晚上要学一些其他资料),一个故事板可以快速的帮你建立上下文并快速进入状态。 上线 经过了大约两周之后,我注册了Apple developer账号,并提交了第一个原型。第一个原型包含了三个列表(一个商品catalogue,一个待买列表,一个推迟列表)结果被无情reject了,原因是……功能太简单。仔细分析之后,我发现功能虽然不算单调,但是界面太过于原始,很多界面元素也需要调整。于是有花费了一周的时间来调整设计,比如字体的选择,字号,色彩对比等,以及实现了共享列表功能(可以将你的购物清单通过Airdrop或者微信等发给别人)。第二次提交之后,3个小时后就审核通过了。 V1的开发大约耗时3周时间,每天晚上学习并编码2-3个小时左右,周末会稍微多一些。随后V2也差不多2-3周,除了功能开发之外,还有些周边的定义,比如screenshots海报的设计和实现(详见下一小节)。不过我最近发现To Buy在iOS 14上有个bug(原因是iOS自身对NSFetchedResultsControllerDelegate的更新上有个同步的bug,如果你正好知道解决方法的话,请不吝赐教,非常感谢),所以如果你使用的以后发现什么异常的话,请反馈给我。此外,所有代码都在Github上,如果有感兴趣的同学也可以一起来完善(野生iOS程序员写的代码,请轻拍)。 To Buy应用 这个应用叫To Buy: grocery shopping list,目前已经在App Store上线了。这是我的第一个从界面设计,到编码实现,到架构及运维(虽然目前主要用户只是我和我老婆),以及图标和海报的设计,文案的编写等完全端到端的产品。虽然没什么用户,不过作为第一个App,我自己还是挺满意的。 特性列表 再经过了几个版本(当前版本V2.5.0)的迭代之后,现在的To Buy的主要功能就是:维护一个购物清单。用户可以: 通过拍照/相册里的照片,文字等来添加一个要买的商品 商品会按照超市分类,这样我就可以在A超市买所有果蔬,在B超市买日用等 可以把历史上买过的商品存到字典里,方便下次购买 可以将购物清单分享给另一个人(比如通过Airdrop) 可以把Apple自带的Photos/照片中的照片直接添加到To Buy中 所有数据会同步到iCloud,即跨设备可以完全同步 在迭代中,我发现了一个很有意思的现象:不论我当初如何笃定某个特性的设计,它的最终形态都会和开始的时候想法相去甚远。比如最早的想法是个内置一个大而全的商品字典,然后用户从这个字典里选择商品。但是最终发现这样的操作效率很低,特别是在手机上操作的时候。...

October 19, 2020 1 min

抽象的不同层次

一个关于抽象的故事 抽象能力应该是程序员所需要具备的、再如何强调都不为过的、最重要的能力了。通过抽象,我们可以从纷纭错乱的、独立的、看似互不干涉的大量问题中解脱出来,找到一个方案,然后一举解决一类问题。 由于抽象是提取了众多具体事物的某种特征并进行了简化的结果,抽象的概念有时候反而会阻碍我们的思考(大概人的大脑更喜欢具体的事物吧)。这时候我们与需要具体的实例来帮助理解。也就是说,在思维活动中,不论是代码编写,还是方案设计时需要不断的从实例中提取概念并形成抽象,又需要不时的将抽象具体化成实例来验证理论。Bret Victor曾将这个过程比作抽象的梯子是非常有见地的 – 你需要不断的爬上爬下,从高处看一看概览,然后回到地上做一些实际而具体的动作。 另一个有趣的事实是,抽象可以有多个层次,当我们终于脱离了众多细节,建立起更高层的概念时,这些高级(high-level)的概念又变成了新的细节,而我们又可以基于这些细节作出新的抽象。就好比我们现在使用JavaScript或者Python来写程序的时候,基本上不需要考虑诸如寄存器的编码含义,机器指令长度等等细节,也无需关注内存的分配和释放等等细节,而只需关注业务功能即可。 这么说起来太过于抽象,我们来看一个具体的例子吧。 背景介绍 在这篇文章中,我希望通过对一个模拟项目的自动化测试(使用cypress)的重构来描述一些代码编写/重构时的模式。其中涉及到的重构方法比较常规,并没有高深的技巧,不过我觉得通过详细解析一个从可以工作的代码演进为较为整洁且便于理解/修改的测试用例,整个过程还是比较有趣的,希望你也可以有所收获。 所谓模拟项目,是对我从2019年1月到10月所经历的一个项目的模拟。纯粹从业务来说,该项目的业务规则/逻辑并不是十分复杂,不过技术细节上有很多值得反思的地方。我们的自动化测试也是经历了N个版本的迭代,最后我也成功将这篇文章中描述的模式应用到了该项目中,效果也是比较令人满意的。 这里的模拟应用Questionnaire是这样一个服务:通过类似调查问卷的方式来输入用户技能,角色,技术水平,项目偏好等等,根据后台的规则引擎来返回一些最适合用户的offer列表。后台的计算逻辑我们这里刻意将其淡化,这里只关注信息的采集部分。 应该注意的是,这里的调查问卷是动态的,比如如果对问题Q1选择了答案A,则下一个问题会变成Q303,如果选B的话则需要回答问题Q1024等。也就是说,问卷的路线有可能是多条的,而且每一条都是同等重要的(至少在测试里需要完整走通一遍)。 从界面上来看,我将整个问卷简化成了3步,实际场景里则可能是10+步,而且每一步中需要回答的问题长短不等,有些问题展现/隐藏还会依赖于前面某一步/某几步用户的答案等。用户只有完成了当前所在的步骤的所有必选项之后,才可以进入下一步(Next按钮才会可用)。 抽象101 - 函数 从功能测试编写的角度来看,将每个步骤视为一个独立的单元是一个合理的做法。每一个步骤需要做的事情也非常类似: 验证该步骤的标题是正确的 填写该步骤的所有必选项 点击下一步按钮 在写一个section的测试时,直观的通过cypress提供的API的结果大致为(假设这里的所有data-test标记我们已经在应用代码中打好了桩): it('Verify the basic information section', () => { cy.get('[data-test="step-title"]').contains('Basic information'); cy.get('[data-test="email-address"] input').type('juntao.qiu@gmail.com'); cy.get('[data-test="assignment"] input[value="assigned"]').check(); cy.get('button[data-test="next-button"]').click(); }); 第二个section的流程大同小异,都是先用selector找到页面元素,如果可以找到的话,通过cypress的API来模拟用户的实际操作: it('Verify the details section', () => { cy.get('[data-test="step-title"]').contains('More details'); cy.get('[data-test="ps-role"]').click(); cy.get('[data-value="dev"]').click(); cy.get('[data-test="developer"] input[value="frontend"]').check(); cy.get('[data-test="rating"] [for="rating-4"]').click(); cy.get('button[data-test="next-button"]').click(); }); 如果逐字对比的话,每行代码几乎都不一样,但是如果仔细看又会发现很多重复。消除这些重复显然可以让代码干净一些,可读性也可以得到提高。一个立即可以想到的重构方法是抽取函数,将验证标题和点击下一步抽取如下: const checkStepTitle = (title) => { cy.get("[data-test="step-title"]").contains(title); } const goToNextStep = () => { cy....

July 9, 2020 3 min