方块日历

大过年的水一篇。

这是 5 年前的一个问题:利用四个骰子组合出一年 12 个月共 365 天。虽然当时已经有解了,就是题图的这四个方块,但毕竟是想通过自己思路来解决。

一开始想的笨办法,穷举之,0101 —— 1231 这些都列出来,然后暴力组合,但进一步想就觉得不太现实。

小技巧

首先可以确定的一点是 6 和 9 是可以互换的,这样我们就只需要考虑 0 —— 8 这 9 个数的排列了。

划分子问题

然后想一想,月份一共就 12 个数,算是两个骰子排出一个月 31 天的子问题,所以先尝试一下两个骰子能排出多少天,不够的天数再用剩余俩骰子凑凑看。

用两枚骰子排出天数

十位的数字也就 0 1 2 3 这 4 种,而且 3 还不常见,所以猜测每个骰子都会有 0、1、2 这三个数,剩下来还有 3、4、56、7、8,正好可以分布在两枚骰子上!

两枚骰子排出天数

解决了

这样排出天数后,如法炮制就可得到月数,所以证明四枚骰子是完全可以排列出一年 12 个月 365 天的!
等有空时写个 web 版的方块日历~

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

虽说老板和产品经理都不会太关心代码质量,所求无非是美观可用效率高等等,但作为一个略有追求的程序员在严酷的需求排期和反复推翻修改的生存环境之下,面对前人挖下的坑和自己几个月前寥寥草草写出的 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 安全,任何业务逻辑都是对这两个基本原则的封装。

时滞效应

在股市的朋友一般都知道「K 线图」这个东西,简单说来就是包含四个数据,即开盘价、最高价、最低价、收盘价,所有的 K 线都是围绕这四个数据展开,反映大势的状况和价格信息。我的姨夫号称一代股神,开一次张可以吃三年,特别崇拜这个图。我个人是有些不以为然的,因为 T + 1 制度的存在杜绝了超短线投机。任何时刻的交易都是基于前一时刻的交易,如果这种方式是靠谱的,那也只是扒下了边际效益的薄薄一层皮而已。如果要做超短线就是期货了,曾有个笑话说炒期货的要尿个尿都要把手上的先抛掉才敢去尿。这就是时滞效应。

当前华夏大地处在一片新型肺炎疫情的阴霾笼罩之下,时隔 1 – 14 天不等的发病周期,让防疫工作困难重重。一旦某个小区发现感染病例,整个小区都会被隔离。我们可以看下下面这张疫情趋势图,图中可见疑似病例一开始是低于确诊病例的,但年初二之后突然呈井喷般上升趋势,但幸好当下已经逐渐趋于平稳。

全国疫情趋势图

没有疑似病例意味着什么,意味着毫无病例筛查。疑似病例井喷说明什么,说明堆积了足够多的样本上报。量变引发质变,足够多的病例将导致疾病向着失控的方向蔓延,而这一切在确诊病例上是延时的,不确切的。所以需要更多的筛查渠道和病毒检测盒,而现在更可怕的是病毒会“伪装”自己,在感染的前 10 天并无法检查出阳性,这样就给医学意义上的密切接触者数量更提升了一个数量级。

医生的真知灼见无法实施

而反观政府现在在做什么?隔离,对,隔离是必要的。区域自治,区域封锁,假期延长,将蔓延的机会降到最低。另外一点,隔离使得城市面临生活必需品保障短缺的问题,各级政府苦心经营的第三产业在疫情面前已经濒临瘫痪,第二产业随着机器人的大量应用其实已经实现了自治,但第一产业 —— 农业,依然是需要保障的重中之重,农产品需要从城市就近通过内部交通网络输送到各家各户,而这些都是都市人口已经很久很久没有考虑过的了。

居委会给隔离群众送菜

所以说,这次疫情之类最应当做的两个工作是:

  1. 深化医疗行业的规范改革,切实保障一线医务工作者的安全以及避免就诊患者交叉感染。
  2. 大力发展现代农牧业,例如屋顶大棚果蔬养殖和人工智能养猪等等,保障危急时刻群众吃饭问题。

都市人已经习惯了吃饭了叫个外卖,生病了挂个号慢慢排队的日子,这次疫情让人体会到时间的宝贵,不珍惜时间就会浪费更多的时间(假期)来弥补时间的亏空。在商业软件开发中也是一样,不尊重用户的日常需求,用户切实反馈的问题得不到解决,用户的怨言就会堆积起来,最终导致用户流失。而对于自身而言,每天投资自己一点点,自己变好一点点,就是增强一点点今后的抗风险能力。

再谈业务逻辑

我面试过一些岗位,也面试过很多人,基础的岗位问的最多的大概就是对框架的理解,对语言基础的掌握等等,但如果是高级岗位往往问到对市场大环境的感悟和对业务逻辑的理解,体现了工具的应用必然是为完成目的服务的标准。

实际开发中,由于前后端现在工种的分离,业务逻辑的归属是很大的问题。

后端面向数据库和第三方服务集成,数据库总是以最小业务模型为基准,考虑扩展性进行建模,而第三方服务与领域模型融合则需要对业务逻辑进行抽象化,比如支付接口需要考虑本身提供的商品种类和第三方支付的种类进行分别编码并抽取公共逻辑,常见的形式有 Adapter(适配器模式)等。

前端当下也很复杂,虽然有了一些基础组件,但随着业务的深入,组件上难免多出很多随着各种特殊使用场景分别配置的参数,甚至一个组件接受几十个参数来配置,可能也有同学一看这不行啊,感觉封装了几个配置好的组件供业务使用,但上面的过程多了以后,组件变得又深又长,深体现在封装层级上,长体现在单个组件的长度上,比如封装一个电子书的展示列表。

图片 -> 封面 -> 带标签的封面 -> 电子书 -> 书籍列表 -> 可无限加载的书籍列表

这时如果要把「图片的尺寸」或者「可点击与否」传递给最内部的组件,就需要一层层从上面传下来,React 有 context 方法来在组件间传递参数,但这毕竟是不可见的,需要自己去 useContext 来得到。ES6 中提供了 Reflect 对象,比如 Reflect.getMetadata('design:paramtypes', target) 可以得到在参数里定义的参数类型,Angular 的控制反转和依赖注入就是依靠实现的。这为我们提供了一些可以想象的思路,当然本质上它只能解决编码上的便利性,组件的层级和复杂度并没有因此降下来。

当前前后端通讯主要靠两种方式

  1. 首屏页面内 script 注入初始数据
  2. AJAX 接口

阅读站新的接口优先考虑 GraphQLGraphQL 是一种面向领域模型的非常好的查询语言,我们不再需要多次往返前后端通讯获得更多数据,或者是重复定义各种 build_xxx_entity,只需要一次定义,任何用到单个字段的地方都只用写字段名就可以使用,可以说极大地降低了边际成本,而且越用收益越高。当然有同学会问那如果大批量查询的问题呢?Facebook 给出了 DataLoader 的方案,但我们的 peewee 版本以及早期的 hack 写法限制了我们真正将 ORM <==> GraphQL 的过程,现在其实是手动 ORM <==> 字段,带缓存的字段 <==> GraphQL

当后端和接口都做到了尽量少耦合业务逻辑时,业务逻辑就大量堆积在表单 POST 接口和前端展示逻辑上,又因为表单校验因 js 和 python 没法互通,需要前后端分别实现,而展示逻辑则是大多硬编码在组件内。这点其实很吓人。表单方面有 JSON Schema 的方案,但因为 Python 这种胶水语言快读开发的节奏每每难以落地,只是可惜展示上没法脱离业务组件进行编码,之前有过在 App 上设计根据 type 来组合 widget 的展示形式,但一旦需求变动代码就变得非常臃肿。

一种现有的整理业务逻辑的形式是把 Business Logic 存到数据库中 …… 简单来说,如果数据库可以方便地表达因果关系,或许这是一种可行的方案。还有就是很通用的配置文件方式,也是我们搭建开发环境的方式,通过脚本配置在环境初始化时载入一系列的变量最终构建出符合业务需要的开发环境。

理想的情形可能是,前端只提供组件库和组件间的组合逻辑,后端只提供数据的存储和加工工具,业务逻辑由第三方提供配置,设置极限值和用户可选值,用户可以在享有最多自主权的情形下使用 web 产品,当然这也只是一种美好的愿望……毕竟当下都是想把产品做到尽善尽美,只给用户一种最好的选择,可这是不是用户想要的呢?做得越多也许越错呢?没有人真的会去想,大家都是假装想想罢了。

类型定义即是量纲

2019 年阅读的前端项目终于吃上了 TypeScript,群众纷纷反映真香!

我在思考,类型定义对于程序语言来说究竟算是什么呢?我们大家都知道编译型语言类型检查不通过是会编译失败的,返回一个错误信息,那么我们为什么要保证变量类型呢?
我不想先谈那些内存结构啊,指针之类的,我想说的是类型定义就好比物理学里的“量纲”。

我们会说 1 立方米的木头,1 吨的木头,都是可以的。但我们不会说 1 安培的木头,或者 1 摄氏度的木头。因为这些量纲没法衡量木头的多少,或者说即使确实有 1 安培电流通过的木头,表面温度为 1 摄氏度的木头,我们也没法了解它到底意味着什么(能换多少钱,需要多少辆车才能拉动?)。

计算机程序是简单的,每个变量,每个函数,他们都只会做两件事,那就是被写入内存和从内存读取,量纲是他们在内存里分配的形式,u32i64 就是不同的变量,长得不一样,功能也不一样。

JavaScript 是弱类型的语言,它容忍隐式类型转换。但 TypeScript 不允许这样做。我们不能把 string 类型的变量传给一个接受 number 类型参数的函数,就好像我们没法把木头接到灯泡上指望它发光一样(好吧,如果这是一段带点的木头就当我在放屁),尤其是你接到的还是一段看起来很像电池的木头,比如名为 battery,但类型是 wood

另外,保证量纲被定义的好处是我们拥有了更好的接口提示,即使之前就有 document comment,但我们依然只能通过 description 来辨识变量类型和猜测该用的参数,有 TypeScript 以后 ……

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

最后,我想说的是,即使 TypeScript 给开发过程带来了巨大的便利,但我们仍应该清楚地认识到它只是一门 JavaScript 的预处理语言,运行时仍是那个不带类型检查的玩具语言(?),当 API 接口返回的数据不同于类型定义的 Interface 怎么办?

Runtime Type Check

一种方案是 WebAssembly,在 Rust 代码里用 Serde 之类的序列化工具来整理 JSON 数据,如果遇到不存在的属性会直接报错

另一种就是 JS 方式:invariant

1
2
3
4
5
6
7
var invariant = require('invariant');

invariant(someTruthyVal, 'This will not throw');
// No errors

invariant(someFalseyVal, 'This will throw an error with this message');
// Error: Invariant Violation: This will throw an error with this message

排序

算法中的排序算是老生常谈的问题了,不过我的问题是生活中真的有那么简单只需要按照一个值从小到大排列的需求吗?

如下图
web
在 App 端变成了这样
App

显然这些已注销用户是在不同时间关注的,web 上将他们混排了,App 上则是单列在最后,前一种强调了关注顺序,后一种强调了社交性(注销账号当然再也没有了社交价值)。并且 App 理论上不太会有用户真的去翻到最后一条,Elastic Search 默认最大条数是 10000,恐怕意义也是如此。

当然排序在程序上而言也就一行代码的区别,不过在产品上区别可是很大,是在从一个笔记本在向电话簿转变。

这一切是否预示着我们每个人之间的关系都是从路人成为朋友,然而最终将深埋通讯录底不见天日,直到天人永隔。

谁都只能陪谁走一段路,但这条路毕竟是要靠自己走完。

行色秋将晚,交情老更亲。