程序员应该写出怎样的代码

虽说老板和产品经理都不会太关心代码质量,所求无非是美观可用效率高等等,但作为一个略有追求的程序员在严酷的需求排期和反复推翻修改的生存环境之下,面对前人挖下的坑和自己几个月前寥寥草草写出的 adhoc 代码,总会望洋兴叹一番,痛定思痛想要写出一些拯救苍生(几个月后的自己)的传世代码。

然鹅面对一堆静态动态模板混杂、数个参数可变返回数据可变的异步嵌套接口加上鬼都不认识的 model/view 继承堆积起来意大利面条一般的代码,更别说还有不知哪里引用的全局样式给兴高采烈调完所有组件的你温柔一刀。怎么办?

首先从简单的样式部分着手,CSS Module 或 styled component 完美解决了局部样式的问题,SSR 解决了前后端模板的一致性。这些都是社区提供的通用方案,剩下的基本就是如何组织 model 以及接口了,即前后端数据同步问题。

我们知道 Python 和 JavaScript 都是动态语言,而动态语言最臭名昭著的就是“动态一时爽,重构火葬场”,基本一个数据倒上那么三四次手之后就变得亲妈不认了。类型是一定要加的,我们用 TypeScript!

https://juejin.im/post/5d8efeace51d45782b0c1bd6

具体的好处前面已经提到过了,这里想说明的是类型定义不仅保证了代码逻辑的正确性,也给予阅读时良好的体验,有了类型以后相当一部分关于变量的说明都可以挪到 type 和 interface 上,代码更容易实现“自解释”。其实 ES Module 很大程度上已经完善了代码的引用逻辑,但相比之下类型定义具有更强的约束(字段级别)和更灵活的配置(interface 鸭子类型),所以都 0202 年了,还不上 TypeScript 真的都没法说自己是工程化的前端项目。

但 TypeScript 只是解决了前端的代码一致性,JavaScript 总还是要在 web 这个容器内运行的,目前边缘计算还不够成熟,以服务器为数据源和计算存储中心的 C/S 架构仍是未来一段时间的主流,所以接口相对于前端的重要性是不言而喻的(插一句,往后想想,与设备的连接接口以后会不会成为前端的新方向呢?这就成了边缘计算)。而对于 HTTP 请求而言,RESTful 是曾经统治一时的事实标准,非常 CRUD,但也非常不灵活。

我们需要的是一种组织更加灵活、分层结构、高度复用、最好实现“自文档”的接口,这就是 GraphQL。虽然它也存在嵌套查询时会拖慢数据库,参数传递不清晰等毛病,但瑕不掩瑜,它确实是目前前端对于结构化数据查询最好的方式,更为出众的是它支持自定义返回字段,不多不少刚刚好是前端想要的那些,这简直太香了。

示例查询,用户 -> 作品 -> 评论/收藏用户/章节列表 -> 自定义格式上架时间

如果不考虑性能和异步并发,这些已经足以一个前端掌控 95% 的开发任务了,但往往那 5% 的事儿占据了 95% 的开发时间,后面会写写对于 Web Assembly 和 Reactive Programming 的理解,长远来看,当前围绕 html 进行的框架或者语言升级将告一段落,往后会转入多语言及范式涌入 web 开发领域碰撞并融合,浏览器正式成为继 iOS 和 Android 之后的第三大移动操作平台,更为可贵的是 web 的普适性使它几乎没有什么前置使用成本。

过度包装

疫情期间每天都是自己做饭吃,隔两三天就要买一次菜,每次买菜光是拆包装盒就要耗费非常多的心力,更别提清洗食材和锅碗,切菜淘米调配料,蒸炒炖炸等工序了,难怪说疫情期间分手的情侣、离婚的夫妻比比皆是,敢情大家平时都是装作很会生活的样子,一旦没了外卖快递都退回到了饮毛汝血的状态。

正如做饭 1 小时,吃饭 10 分钟,洗碗半小时所说的那样,吃这样一个动作在当前都会有无穷的副作用,可以说现代都市生活真的是建立在非常完善的 O2O 服务行业之下的,要吃饭线上下个单线下送来,要保洁自如下个单线下保洁员上门,要 ml 都有 165/90 32C 的家政服务人员上门服务。人的生活变成了只需处理工作,其他时间都可以用钱买到,这样 WFH 所带来的工作时长延长对社畜的身心打击几乎到了极致。扯远了。

网上买菜或者超市买菜,相比于菜市场买菜最大的区别是包装。垃圾桶里丢掉的绝大部分是塑料包装而不是切掉的边角料菜或者剩饭剩菜。就像软件开发里,动辄几十上百层的封装之下,真正工作的是那么一两行代码。

  1. public
  2. js
  3. submit
  4. works
  5. manage
  6. components
  7. header-actions
  8. PriceActions
  9. PriceActions.tsx -> StartPromotionButton.tsx -> StartPromotionForm.tsx

一个页面上小小的组件,被封装在 9 层目录之下,同目录平级之间还有嵌套关系。当然组件化分治思想也是 React 一直火到现在的根本原因,但有时也会想想这种扁平化的目录结构是否真的合适项目的发展,或者说在业务需求猛增前提下如何科学组织目录结构以便修改维护也是门学问了,当然最好的方式是:不组织。根据实际发展来让文件自行组织,见 destiny

拉扯这么多包装的弊端,但包装实际上是一项很重要的工序,因为不包装带来的风险,往往是看不见的:
小吕去年写了个活动页的接口,因为是第一次办这种活动,小吕本着以产品需求为约束的理念写好了无届数限制的 API 接口,然后前端同学以此接口封装了一个组件给页面加了上去。转眼间一年过去了,活动办了第二届,产品要求有新功能,小吕又哼哧哼哧写了一个第二届活动的 API,但这时候出分歧了:前端觉得接口应该稳定,如果每做一届都要回溯一遍做过的接口映射,太累且容易出错;而后端觉得分离 API 有助于更清晰地业务逻辑。大家说的都没错,怎么办呢?包装一下接口吧!

原俩接口实现,红线为本次修改
Adapter 模式

可以看出是适当的包装有助于分清业务界限,明确各端职责是非常值得提倡的。

但为什么实践中经常分不清这些呢?因为 Python 本质是一个胶水语言,更多起黏合剂作用,而 JavaScript 更想火柴棍,可以飞速搭原型,但两者都不是拥有持续集成性和可维护性的代表。个人以为类型系统以及所有权(或者说内存锁)是一个合格的工业级语言必备的,一个保障 AOT 安全,一个保障 JIT 安全,任何业务逻辑都是对这两个基本原则的封装。

通用设计

我们知道页面是 web 设计中单次呈现给用户的一个功能整体,但功能的粒度随着 AJAX 的出现不仅仅限于表单即 form 与服务器的单次交互,变成了 XMLHTTPRequestfetch 甚至 WebSocket 的多次往返,这样而言单一页面也可以承载更多的用户交互行为。与之同时带来的问题是如何有效划分功能区域以便实现最大程度的组件化,增强复用性和可维护性?

签约与填写身份信息分离

以某次产品设计为例,产品需要将 签署合同填写身份信息 作为一个步骤页面放到拉力赛作品的创建流程里,原先的页面长成下图这样。

原签署合同页面

产品的想法是直接复用这个页面,作为前端当即表达了反对:这个页面包含了签协议和填信息两种操作,但却共用一个接口,不利于今后业务逻辑更为复杂时拆分(当下就已经属于过度耦合了),建议将 填写身份信息 放到单独的页面去。但产品随即表达了反对,认为用户的操作流程不应该多一个页面跳转。这怎么办呢?

用弹窗啊!

弹窗作为一种全局模态框可以在不修改整体页面流式布局的基础上让用户看到额外信息,甚至进行交互操作。弹窗内的组件可以被复用,因为是动态挂载到页面上的,一般对页面已有数据的依赖较小。

现签署合同页面
在身份信息不全情形下会弹出弹窗以供填写

在实际代码构建时为了便于使用,将整个弹窗加上作者身份数据的获取、存储与更新逻辑整体封装到了一个 hook 中:

1
const { agentInfo, loaded, showAgentInfoFormDialog } = useAgentInfo(agentId)

这样将数据和方法返回给调用方,将代码的侵入性做到最低。如果以后有只需要身份信息而不需要修改弹窗的使用情形,也可以尽情复用。

更通用的标签筛选

一日接到了一个增加筛选器筛选条件的需求(筛选器是一个巨大的,有着上百个 CheckBox,相比各种电商筛选条件毫不逊色的怪物👹)。

需求

作为一个凡是遇到需求不杠一杠就不开心的开发,我理直气壮地提出了自己的看法:这种一次性使用的需求,会在文件里留下谜一样的代码,而且根本不可能实现复用,如果要重构或者迁移都会是巨大的沉没成本。

随后根据下图的需求来源项

征文大赛筛选项,五个硬编码的分类条件,实则为普通分类

提取出了可以重复利用的「标签输入框」

标签输入框

有人会问,这样经常需要输入特定标签会不会不方便呢?早就想过啦,可以使用「加入书签」功能

加入书签

会映射成形如 /admin/editorial_service?tags=亲密关系+轻装上场 的 url 存储在 localStorage 中,方便下次使用。大功告成!

总结

通用设计可以提供组件的复用能力,尽可能做到少暴露接口给外界,毕竟多一行代码就多一分风险和维护的成本,同时通用性足够好的情形下也能促进产品往更高效的方向发展,而不是拘泥于小而美的自我天地。