主干开发是相对GitFlowGithub Flow更贴合CI/CD(持续集成/持续分发)的高效版本控制管理实践,也更贴合Devops团队。

背景

在软件开发的早年期间,GitFlow和GithubFlow被用来在软件开发中做版本控制管理。

  • GitFlow工作流中,分为主干分支(master)、开发分支(dev)、特性分支、分布分支、热修分支等,这些分支都是长期存在的,并在需求开发完成/bug修复完成/代码发布等特定时候执行代码的CR(Code Review)和合码工作。且在合并时,由仓库的核心成员或者管理员们把握代码质量。随着项目的扩大,冲突的几率提高,每次代码合码时候的工作量也大大提升,带来了额外的仓库维护成本。Github Flow便随着Github逐渐流行起来。
    GitFlow
  • Github Flow工作流中,只有一个主干分支(master),一些特性分支以及发布时会用到的分布分支。开发者可以自由从主干分支签出特性分支开发、调试,并在需求完成后合入主干分支。一定程度上减少了长期存在分支的维护成本,但同样的,CR依然是项目管理者进行,特性分支合码时代码量依然有可能很大,团队内规范不好的话,也容易导致合码后主干分支不可发布。
    Merge Conflict

主干开发(Trunk Based Development)是一种更敏捷的git工作流,所有的开发者都可以合码到主干分支,结合CI/CD流程,有助于团队快速迭代。

什么是主干开发

主干开发和CI/CD相互依赖相辅相成。

  • 进行主干开发才能保证CI/CD,即每天充分多次的集成乃至发布
  • CI/CD过程中的快速自动化测试可以保证主干分支的可发布性

在主干开发中每位或者每小组开发者将自己的工作分成小份,然后以每天至少一次的形式从自己的分支合并到主干,由于每次合并代码量不大,CR时间以及CI时间都会缩短。一个典型的主干开发时间轴如下:

主干开发时间轴

在主干开发中,某些情况下需要从主干分支中选出最佳的bug修复合并到对应版本中,但如果每天发布多次,则根本不需要发布分支,可直接从主干中部署。这样做的最大优势在于减少开发线,频繁执行小批量合并,将代码保持最新。从而降低团队的合码成本。

如何实现主干开发

在进行主干开发时,开发者需要了解如何拆解工作为小份,同时,还需要让构建流程保持通过,如果CI失败,开发者需要立即停止当前工作修复问题,无法短期修复时,也要还原相应更改。通常来讲,主干开发有下面这样的特征:

  • 仓库里的活跃分支不超过3个
  • 分支的合码频率不少于每天1次,即分支的生存周期不超过1天
  • 没有代码冻结期或集成期

在实践中,有下面一些tips:

  • 小commit,多合码:将每次合码改动限制在少量的commit和较少的代码改动上,保证合码时轻松;主干分支应该有频繁的小批量改动合码
  • 最少1天1合码:每天合并或关闭待合并分支,有效减少合码压力,提高增量发布敏捷度
  • 同步Code Review:主干分支的每次合码需要保证CR的及时响应,这可以借助一些代码分析工具的辅助
  • 全面的自动化测试覆盖:确保有全面的自动化单元测试套件,保证测试通过后再合并代码,这也是保证主干分支可发布的基础
  • 构建快速:降低CI成本,构建和测试需要在几分钟内完成

同时也带来两个新概念

  • 分支抽象:在主干开发中,合码频率很高,有些大需求可能没有办法在1天内就完成合码,对于未完成的需求,可以在代码中先埋下未使用的新特性,等待需求完成后,再使用埋好的新功能。这种在源代码中提前埋下“代码分支”的方式成为抽象分支(Branch by abstraction)
  • 特性开关:在分支抽象的基础上,有些合并到主干的改动不一定想让所有用户都看见,可以在代码中预先埋入分支语句,再从配置中读取当前是否使用特性。这样可以实现功能灵活切换、实现ab-test等效果

feature-flags

标准开发流

1
2
3
4
5
6
7
8
git checkout master && git pull --prune
git checkout -b <feat/branch>
# coding
git push -u origin <feat/branch>
# after any updates
git push
# or add --force if rebased during updates
git push -f
  1. 更新本地master分支
  2. 签出特性分支
  3. 编码
  4. 推送本地分支到远端
  5. 提MR,如果MR后有更新,继续push,当rebase master遇到冲突时,推送需要增加-f参数
  6. 合码

注:因为主干开发每次合码量不大,建议使用rebase解决冲突

主干开发的好处

主干开发的最大好处在于对CI的亲和度。可以想象开发者完成当日工作,当日合码,当日测试通过完成集成,达到可发布状态。很大程度减少了合码的痛苦。进而有:

  • 持续CI
  • 持续CR
  • 持续CD

总结

非常适用于敏捷开发中,对于团队成员能力过关(要懂得怎样拆分需求)、测试有着良好建设的团队来讲,是种提效的好方式,值得一试。相反如果成员拆分不够良好、代码review不够及时,测试不够自动化和系统,则不适用主干开发。

参考

全文参考自马丁大叔的《架构整洁之道》,书中文笔清爽易懂,不过在后半段有点条理不清流于术而非道

在编程领域,问题就像一个生命体一样,是在不断繁殖和进化的。它甚至经常不会人们预期中一般发展。作为一名出色的软件工程师或架构师,你需要有超出普通程序员的视角,考虑系统宏观的未来的发展。你的使命是,在这种恶劣的开发环境下,绘制一幅相对最优的图纸,用最少的时间、人力、金钱构建和维护一个随时可能融化在熵增热汤里的软件系统。和现实物理世界里的架构师类似,你需要了解编程世界里的一般规律,帮助你挑选武器(技术),修炼秘籍(方法论),在不同的江湖里(业务)打造不同的门派(软件系统)。

开卷有益,祝你练武愉快~

你要做什么

简而言之,架构师的终极目标就是用最少的人力成本来满足构建和维护软件系统的需求。糟糕的架构设计会让软件在成功之前,就带来高昂的边际人力成本,即开发新需求的开支越来越大(因为程序员的时间都耗费在系统的修修补补上了)。然而,这种日益增长的边际人力成本现象并不少见。来源于类似龟兔赛跑中兔子的盲目自信,实际上,无论从短期还是长期看,胡乱编写代码的工作速度其实比循规蹈矩更慢

架构师存在的一个必要性就是,软件存在着两种价值维度:

  • 行为价值(现在时):即实现功能和弥补bug。这类价值是紧急却并不总是重要的
  • 架构价值(将来时):即软件是否足够“软”(易于被修改),这类价值是重要却并不总是紧急的

很现实的一点是,在公司中,团队之间的抗争本来就是无穷无尽的。你作为研发团队的一员,职责的一部分就是避免你的代码在抗争的风吹雨打下变成一坨没人爱的shit。

编程范式

没错,架构师们也有祖师爷。在1958到1968年期间,3大编程范式就已经陆续出现了。

  • 结构化编程,由Dijkstra在1968年提出,并发扬光大,它对程序控制权的直接转移(程序语句)进行了限制和规范
  • 面向对象编程,最早在1966年提出,Ole Johan Dahl和Kriste Nygaard注意到,函数调用堆栈可以被放到堆内存中,从而在函数返回后继续保留。它对程序控制权的间接转移(函数调用)进行了限制和规范
  • 函数式编程,启发自Alonzo Church于1936年发明的lambda演算,发扬于1958年的LISP语言。它对程序的赋值进行了限制和规范

值得思考是,三大范式做的都是限制和规范,即告诉我们不能做什么,而不是可以做什么。另外,多态带来的架构边界飞跃,函数式编程带来的数据访问限制,结构化编程带来的算法拆解为我们架构软件提供了强大武器。这也与软件架构的三个关注点所契合:

  • 功能性,即完整的功能实现
  • 组件独立性,即合适的耦合度与细粒度
  • 数据管理,即良好的数据结构设计

结构化编程

Dijkstra在1950年代思考编程时,得出过一个结论:编程是一项难度很大的活动。他倾向于把编程类比为数学推导过程,并发现goto某些使用会导致模块无法被递归拆解成更小的单元。然而,去掉这些使用的goto可以被顺序结构、分支结构、循环结构这三种最小集等价表示出来。从而,大问题可以被逐步拆解为小问题。

不过,事情也并非这么理想,当程序复杂后,我们不可能像Dijkstra一样,用严格的数学推导形式化证明编程的正确性。相反,类似实验学科的无法被证伪即正确,我们现今依旧使用着Dijkstra的结构化编程思路将大问题拆解为小问题。

有趣的是,“无法被证伪即正确”和Dijkstra的一个观点“测试只能展示bug的存在,并不能证明不存在bug”不谋而合

简而言之,去掉goto糟粕诞生的结构化编程中,最有价值的地方就是,它赋予我们创造可证伪程序单元的能力,从而架构起大程序。在架构设计领域,功能性拆解仍然是最佳实践之一

什么?什么叫做可证伪?你应该写过单元测试吧。

面向对象编程

什么是面向对象?有人说面向对象是“数据和函数的组合”,也有人说是“对真实世界的一种建模方式”。但这两种理解要么片面,要么虚无缥缈。为了总结这种范式,我们先从它的3大特征入手:

  • 封装,即将一组关联数据和函数圈起来。然而这种特性,从C语言起就支持(struct + 头文件),很难说它是面向对象编程的必要条件
  • 继承,即可以在某个作用域对外部定义的一组变量与函数进行覆盖。不过C语言也能模拟出这种能力,看起来也比较勉强。
  • 多态,即在同一接口描述下的不同具体实现形式,C语言起也做了支持(STDOUT),然而使用函数指针显式实现多态问题就在于指针的危险性。而面向对象编程对这种程序间接控制权的转移做了约束。

传统的函数调用树中,系统行为决定了自上而下的控制流,而控制流决定了源代码依赖(代码实现)是自上而下的,比如在C中会使用#include引入依赖。此时不论是代码实现还是代码执行都是自上而下的。然而在多态的帮助下,底层函数需要依赖高层接口实现,作为高层函数的插件引入,从而将这种依赖关系和控制流反向,即依赖反转。实际上,借助安全便利的多态实现,可以轻松将依赖关系反转。

从而架构师可以完全控制这种方式下,系统中所有的源代码依赖关系,进而随意更改源代码依赖关系。让每个组件都有独立部署独立开发能力。好了,我们现在可以说明面向对象编程的含义了:

面向对象编程就是以多态为手段来对源代码中的依赖关系进行控制的能力。这种能力让架构师可以构建插件式架构,让高层策略性组件和底层实现性组件相分离。借助接口,底层实现性组件作为插件,可以独立于高层组件开发和部署。

函数式编程

函数式编程依据的原理早在编程之前就已出现,相对前两种范式,函数式编程的风格可能相对陌生一点。在这类风格中,变量都是不可变的。从而让竞争问题、死锁问题、并发更新问题不复存在。一个架构良好的系统,需要将状态修改的部分和不需要修改的部分隔离开,然后用类似事务型内存的方式来保护可变量。另外,架构师应该着力于将大部分逻辑归于不可变组件中,可变组件的逻辑越少越好。

书中还提到了只包含CR的事件溯源存储逻辑,即通过事务日志的方式保存当前状态。因为不存在更改和删除,从而不存在并发问题。也是一种很新颖的思路。

回顾上面的三种编程范式,都在说什么不应该做。也即编程好似在充满死路的熵增旷野中,只有沿着相对安全的几个大方向才可拨开迷雾。

设计原则

软件的中层需要具有良好的可读性、可扩展性。这里就不得不提到SOLID原则:

  • SRP:单一职责原则,每个模块有且只有一个需要被改变的理由
  • OCP:开闭原则,对扩展开放,对修改封闭
  • LSP:里氏替换原则,子类型应该能够无无缝替换类型使用
  • ISP:接口隔离原则,依赖的模块不要包含不需要的接口
  • DIP:依赖反转原则,高层策略性代码不要依赖底层实现性代码

SRP

任何一个软件模块(一组紧密相关的函数和数据结构)都应该只对一个行为者负责。实际上,代码和数据就是靠着和某一类行为者的相关性组合起来的。我们需要将服务不同行为者的代码进行切分

OCP

设计良好的软件应该易于扩展,同时抗拒修改。实现方式可以通过将系统划分为一系列组件,并且将依赖关系按层次组织,使得高阶组件不会因为低阶组件修改受到影响。

LSP

里氏替换原则表示,子类型应该能够被当做父类型使用。它实际上表示了一种面向接口设计的设计原则。一旦违背了这种可替换性,就会不得不在系统架构中增加大量复杂的应对机制。

ISP

ISP告诉我们任何软件设计如果依赖了不需要的东西,都是不好的迹象,很容易带来不必要的麻烦。

DIP

DIP告诉我们,应该多引用抽象类型,而不是具体实现。因为软件是经常变动的,而抽象出共性的接口则是较少变化的。从而可以衍生出一些守则,譬如:

  • 应该多使用抽象接口,避免使用多变的实现类
  • 不要在实现类上创建衍生类
  • 不要覆盖具体实现的函数

不过当然了,还是得有人做实现的脏活累活的

组件构建

组件

组件是构建软件的最小单元,同时也是源代码的集合。在早期会使用链接技术将程序和库函数链接起来,而后随着机器性能的增长,我们会在程序运行中插入动态链接文件,如今这种组件化插件式架构是最常见的软件构建形式。

组件聚合

和类相似,组件也有一些原则指引我们的构建工作。

  • REP:复用/发布原则,即可以一起发布的最小粒度就是复用的最小粒度,也即按可以同时发布聚合
  • CCP:共同闭包原则,即因为同一原因修改的放在一起,反之不要放在一起,也即按变更原因聚合
  • CRP:共同复用原则,即会被一起复用的放在一起,反之不要放在一起,也即按减少无用耦合聚合

这三大原则相互牵制,在项目的不同阶段,某一原则重要性也会不同;比如在项目早期CCP就会更重要,而后REP会比较重要。

组合耦合

本节提出了一些可以定量衡量耦合健康度的指标,比较新颖。

  • 无依赖环原则:依赖关系中不能有环,会不利于厘清依赖关系;可以通过依赖反转创建第三方依赖组件解决。循环依赖关系务必持续监控。
  • 稳定依赖原则:依赖关系必须指向稳定的方向,简单点说就是让经常变更的组件依赖于不经常变更的组件。一个组件的位置稳定性可以通过入向和出向依赖算出,它要能和组件的实际稳定性匹配。
  • 稳定抽象原则:抽象化程度需要和稳定性程度一直,即经常变更的组件要容易变更,即更具体实现;反之,稳定的组件要不容易变更,即更抽象。结合上条看,依赖关系应该指向更抽象的方向。

使用位置稳定性指标I和抽象程度A,可以绘制一个坐标系。在主序列上的是最健康的,相反的两块痛苦区和无用区则是不健康的表现。用偏离主序列线的距离可以大致衡量依赖关系的健康程度。结合发布版本的变化来看,还可以得到变化趋势。

软件架构

软件架构目的就是方便在工作中更好地对组件进行研发、部署、运行和维护。其中的策略就是保留尽可能多的可选项。让系统最大化程序员的生产力,同时最小化系统运营成本:

  • 开发:系统架构需要方便开发团队对它的开发,不同的团队结构应该采用不同的架构设计,比如团队的大小就会影响架构的选择
  • 部署:一键式部署
  • 运行:几乎任何运行问题都可以通过增加硬件来解决
  • 维护:减小新功能和系统缺陷占用的人力资源

保持可选项,忽略那些无关紧要的实现细节。任何软件系统都可以拆解成策略(业务的宏观逻辑和流程)和细节(具体操作行为)。而策略才是系统的真正价值所在。细节是指那些和策略交互的东西,包括:

  • I/O设备
  • 数据库
  • Web系统
  • 服务器
  • 框架
  • 交互协议

在设计时,可以尽量拖延上面这些的设计,这样我们做出的决策才不会依赖各种很容易变化的信息。另一方面,也可以增加实现底层的可替换性。举个具体例子:设备无关性

独立性

一个良好的架构应支持下面几点:

  • 系统用例:设计良好的架构需要能够看起来就可以反映系统的设计意图,比如一个购物车应用架构应该看起来就该是用来实现购物车的
  • 系统运行:可以解耦出多个独立服务,然后通过某种网络协议通信,这种架构即微服务
  • 系统维护
  • 系统开发
  • 系统部署:理想的独立部署应该能够做到热更新

要注意留意表面的重复和实际的重复,如果两段代码变更速率和缘由不同,那么就不算是真正的重复。

划分边界

  • 设计良好的系统架构不应该依赖细节,而应该尽可能推迟细节性的决策。通过划清边界,可以推迟和延后细节性的决策,从而节省大量时间,避免问题。
  • 边界线应该画在不相干的事情中间,譬如GUI和业务逻辑
  • 针对核心业务逻辑的插件式架构可以提高可维护性和可扩展性

边界剖析

简言之,应该尽可能从底层组件指向高层组件。

策略和层次

  • 变更原因、时间和层次不同的策略应该属于不同的组件
  • 按距离系统输入、输出距离的远近,可以确定策略的层次
  • 源码间的依赖关系,应该主要和组件所在的层次挂钩
  • 低层组件应该以插件的方式依赖高层组件

业务逻辑

业务逻辑是程序中真正用于或者体现赚钱/省钱的逻辑与过程。其中关键逻辑和关键数据紧密组合成为业务实体。业务实体应该只有高层逻辑,没有具体实现。而用例是业务实体在不同侧面的具体体现。通过用例可以规范用户和业务实体的交互方式。

“尖叫”的软件架构

“尖叫”即所见即所得。软件架构本身就足以能够体现其用途。一个良好的架构设计应该围绕用例展开,推迟和延后框架的选择,不要过度拘泥于框架。框架只是一个可选项,是一个工具,而不是一种信念,更不是一种架构。

整洁架构

一些常见的系统架构通常具有以下特点:

  • 独立于框架
  • 可被测试
  • 独立于UI
  • 独立于数据库
  • 独立于外部接口

Main组件

  • Main组件包含了系统中最细节化最底层的策略,它应该在做完脏活累活后,将程序的控制权交给最高抽象层的代码去执行
  • 针对不同系统可以配置不同的Main组件,即将Main组件视为应用程序的一个插件

服务:微观和宏观

  • 系统的架构边界事实上并不落在服务之间,而是穿透所有服务,在服务内以组件形式存在
  • 服务可以提升系统的可扩展性和可开发性,不过服务却并不能代表整个系统的架构设计

整洁的嵌入式架构

  • 固件即对平台或硬件的强依赖代码,在固件和软件之间可以设置HAL(硬件抽象层),为它上层的软件提供服务,它可以帮助软件脱离目标硬件平台来测试
  • 类似地,我们还可以引入OSAL(操作系统抽象层)来减少软件对操作系统的依赖

实现细节

那么什么算是实现细节呢?

  • 数据库,数据的组织结构和模型都是系统架构的一部分,但是从磁盘中存储/读取数据的机制或手段则没那么重要,就比如数据库或静态文件
  • Web,Web只是UI,只是一种I/O设备
  • 应用框架,框架被创造的目的是解决作者遇到的问题,它要求我们去阅读文档,按照作者的要求整合到我们的应用中,可以使用但是不要被框架绑定

案例:视频销售网站

  • 系统架构设计的第一步是识别系统中的各种角色和用例

–END–

这本书从Unix的设计理念等各方面讲起,内容充实有趣,尽管介绍细节的部分对于不太了解Unix的人呢来讲有些生涩,但在道的介绍上有不少可取之处。由于某人的出现,生活中多了新的追求,比想象中多用了一些时间看完了它。下面摘取一些其中精华的观点,力图尽量还原书中的本意。

Context

哲学

  • 每过18个月,就有一半的知识会过时
  • X致力提供一套“机制,而不是策略”
  • 提供机制而不是方针的哲学让Unix长期保鲜
  • Unix传统将重点放在尽力使各程序接口相对小巧、简洁、正交
  • Unix管道发明人Doug McIlroy曾说过:
    • 让每个程序就做好一件事
    • 假定每个程序的输出都会成为另个程序的输入
    • 尽早将设计和编译的软件投入使用
    • 优先使用工具而不是拙劣的帮助来减轻编程任务的负担
  • Rob Pike曾从不同的角度表述了Unix的哲学
    • 你无法断定程序会在什么地方好费时间,所以别急于找地方改代码,除非你已经证实那儿就是瓶颈所在
    • 没对代码估量最耗时的部分前,别去优化速度
    • 花哨的算法在n很小的时候通常很慢,而n一般很小
    • 花哨的算法比简单算法更容易出bug、更难实现
    • 编程的核心是数据结构,而不是算法
  • 书中对Unix的哲学,总结为下面这些点
    • 模块原则,使用简洁的接口拼接简单的部件
    • 清晰原则,清晰胜于机巧,程序是写给人看的
    • 组合原则,设计时要考虑拼接组合
    • 分离原则,策略和机制分离,接口和引擎分离
    • 简洁原则,设计要简洁,复杂度能低则低
    • 吝啬原则,除非没有办法,不要写大程序
    • 透明性原则,设计要有可见性(如输入输出、打点上报),便于审查和调试
    • 健壮原则,健壮源于透明和简洁
    • 表示原则,把知识转移到数据中,保证逻辑的简朴而健壮
    • 通俗原则,即最少惊奇原则,接口设计避免标新立异,缓和学习曲线
    • 缄默原则,设计良好的程序将用户的注意力视为有限的宝贵资源
    • 补救原则,出现异常时,马上退出并给出错误信息
    • 经济原则,宁花机器1分,不花程序员1秒
    • 生成原则,避免手工hack,编写程序去生成程序
    • 优化原则,过早优化会妨碍全局优化,先制作原型,再精雕细琢;先可用,再优化
    • 多样原则,不相信“不二法门”的断言
    • 扩展原则,未来总比预想来得要快
  • Unix哲学一言以蔽之,即KISS(Keep It Simple, Stupid!)
  • 善用他人写好的工具,尽可能将一切自动化

历史

  • 计算机不应仅被视为一种逻辑设备而更应视为社群的立足点
  • 1985年IEEE支持的POSIX标准表述了BSD和SVR3(System V Release3)调用的交集
  • 1987年初,GNU C编译器第一版问世
  • 1995年,Linux找到自己的杀手级应用——开源的web服务器Apache
  • 过度依赖任何一种技术或者商业模式都是错误的

Unix哲学和其他哲学的对比

  • 统一性理念:一切皆文件 & 管道概念
  • 多任务能力:抢先式多任务
  • 协作进程:低价的进程生成和简便的进程间通讯
  • 内部边界:程序员最清楚一切
  • 文件属性和记录结构:没有文件属性
  • 减少使用不透明的二进制文件格式
  • 首选CLI命令行界面
  • Unix是程序员写给程序员的
  • 开发的门槛:轻松编程
  • 操作系统的比较
    • MacOS:MacOS有一个自己的界面方针,非常详细地说明了应用程序GUI的表现形式和行为模式
    • Windows NT:有注册表蠕变现象,不过支持了Cygwin,实现了Unix API的兼容
    • MVS:一切皆批处理
    • Linux:贴近终端用户的愿望使得Linux开发者比专有Unix更注重系统安装的平稳性和软件发布问题

Design

模块性

  • 展开来说就是,要编写复杂软件又不至于一败涂地的唯一方法就是用定义清晰的接口把若干简单模块组合起来
  • Unix程序员骨子里的传统是:更加笃信重视模块化、更注重正交性和紧凑性等问题
  • 封装良好的模块不会过多向外披露自身的细节
  • 缺陷个数随着模块的代码行数会先减小,随后按代码行数的平方上升
  • 紧凑性和正交性
    • 人类短期记忆能够容纳的不连续信息数就是7,加2或减2
    • 紧凑性:有经验的用户通常不需要操作手册,让人乐于使用,不会在想法和工作间格格不入;紧凑不等于薄弱;一个功能子集,能够满足专家用户80%以上的一般需求
    • 正交性:任何操作均无副作用,改变每个属性的方法有且仅有一个;重构代码就是改变代码的结构和组织,而不改变其外在行为
    • 任何一个知识点在系统内都应当有一个唯一、明确、权威的表述(Single Point of Truth, SPOT)
    • 提高紧凑性的精妙但强大的办法就是围绕“解决一个定义明确的问题”的强核心算法组织设计,避免臆断和捏造。形式化往往能极其明晰地阐述一项任务,与形式法相对的是试探法——凭经验法则得到的解决方案,这种思路的问题是回增生出大量特例和边界情况
  • “限制不仅提倡了经济性,而且某种程度上提倡了设计的优雅”。要达到这种简洁性,尽量不要去想一种语言或操作系统最多能做多少事情,而是尽量去想这种语言或操作系统最少能做的事情——不是带着假想行动
  • 设计有自顶向下和自底向上两种思路,前者通常先考虑主事件循环,再插入具体事件;后者通常先考虑封装具体任务,再按次序粘合在一起
  • 出于自我保护,程序员尽量双管齐下——一方面以自顶向下的应用逻辑表达抽象规范,另一方面以函数或库来手机底层的域原语(原子操作)
  • 实际代码往往是自顶向下和自底向上的综合产物。同一个项目经常同时兼有,这就导致了“胶合层”的出现
  • 胶合层是个挺讨厌的东西,必须尽可能薄,这一点极为重要。薄胶合层原则可以看做是分离原则的升华。策略(应用逻辑)应该与机制(原子操作集)清晰地分离和解耦。
  • “完美之道,不在无可增加,而在无可删减”
  • OO语言鼓励“具有厚重的胶合和复杂层次”的体系。当问题域真的很复杂、确实需要大量抽象时,这可能是好事,但如果编程员到头来用复杂的方法来做简单的事情——仅仅是为他们能够这样做,结果便适得其反
  • 全局变量意味着代码不能重入
  • “就我个人而言,如果局部变量太多,我倾向于拆分子程序。另一个方法是看代码行是否存在(太多)缩进。我几乎从来不看代码长度。——Ken Thompson”
  • 如果通过电话向另一个程序员描述说不清楚,API可能就是太复杂,设计太糟糕了。

文本化

  • 序列化有时被称为列集(marshaling),其反向操作(载入)过程称为散集(unmarshaling)
  • 互用性、透明性、可扩展性和经济性都是设计文本格式和应用协议需要考虑的问题
  • 设计一个文本协议往往可以微系统的未来省不少力气;使用二进制协议的唯一正当理由是:如果要处理大批量的数据集,因而确实关注能否在介质上获得最大位密度,或是关心数据转化时的时间或指令开销。大图像和多媒体数据的格式有时可以算是前者的例子,对延时有严格要求的网络协议有则算作后者的例子
  • 文本格式的位密度未必一定比二进制格式低多少;设计紧凑二进制格式的思路往往不能够兼顾干净扩展的要求
  • 数据文件元格式是一套句法和词法约定,已经正式标准化或者通过实践得到充分确定
    • DSV:冒号是默认的分隔符
    • RFC 822:字段名不得包含空格,通常用横线代替,空格和tab作为当前逻辑行的延续
    • XML:需要文档类型定义(如XHTML)和相关应用逻辑赋予其语义。通常可以语法检查就能发现形式问题或数据错误
    • Unix文本文件约定
      • 如果可能,以新行符结束的每一行只存一个记录
      • 每行建议少于80字符
      • #开始注释
      • 支持反斜线\
      • 用冒号或连续空白作为字段分隔符
      • 在节格式中,支持连续行
      • 要么包含一个版本号,要么将格式设计成相互独立的自描述字节块
      • 不要只压缩或者二进制编码文件的一部分
  • 应用协议设计:如果应用协议是文本式的,那仅凭肉眼就能容易地分析,例:SMTP、POP3、IMAP
  • 应用协议元格式:应用协议元格式是为了简化网络间事务处理的序列化操作而发展出来的,因为网络带宽要比存储昂贵得多,所以需要重视事务处理的经济性
  • 目前还没有个制订较完善的元协议非常适合真正的P2P应用,不像客户端-服务器应用——HTTP在这一领域的游刃有余

透明性

  • 如果软件系统包含的功是为了帮助人们对软件建立正确的“做什么、怎样做”的心理模型而设计,这个软件系统就是可显的
  • 用户喜欢UI中的透明性和可显性,是因为这意味着学习曲线比较平缓,而“最小立异原则”就是一个体现
  • 优雅是力量与简洁的结合。优雅的代码事半功倍;优雅的代码不仅正确,而且显然正确;优雅的代码不仅将算法传达给计算机,同时也把简洁和信心传递给阅读代码的人
  • 编写透明、可显的系统而节省的精力,将来完全可能就是自己的财富
  • 用户的注意力是宝贵的,让Unix工具正常运行的最好策略是在大部分时间里沉默
  • 真正的聪明是找到方法,可以访问部分细节,但是又不让它们太显眼
  • 为透明性和可显性而设计
    • 不要在具体操作的代码上叠放太多的抽象层
    • 透明性和可显性同模块性一样,主要是设计的特性而不是代码的特性
      • 程序调用层级最大深度是多少?
      • 代码是否有强大而明显的不变性质
      • API的各函数调用是否正交
      • 程序的数据结构或分类和它们代表的外部实体间,是否有一一映射关系
      • 有多少魔法数字
    • 隐藏细节和无法访问细节有着重要区别
    • 透明的系统更容易实施恢复措施,首先就是更能抵抗bug的破坏
  • Unix程序员的品性:“宁愿抛弃、重建代码也不远修补蹩脚的代码”
  • 选择简单的算法

多路程序控制

  • Unix最具特点的程序模块化技法就是将大型程序分解成多个合作进程
  • Unix的设计风格强调用定义良好的进程间通信或共享文件来联通小型进程。因此,Unix操作系统提倡把程序分解成更简单的子进程,并专注考虑它们之间的接口
    • 降低进程生成的开销
    • 提供方法(shellout、IO重定向、管道、消息传递、套接字)简化进程通信
    • 提倡使用简单透明的文本数据格式来通信
  • 真正的难题不在协议语法而是协议逻辑——协议必须既有充分的表达能力又有防范死锁的能力
  • Unix的IPC分类
    • 最简单的形式:调用另一个程序来完成任务;专门程序通常借由文件系统和父进程通信
    • 管道、重定向和过滤器:过滤器即从标准输入顺序读数据,然后向标准输出写数据;管道操作把程序的标准输出连接到另一个程序的标准输入
    • 包装器:包装器或者将调用程序专用化,或者为它创建新的接口
    • 从进程:子程序通过连接到标准输入和标准输出的管道,交互地和调用程序收发数据
    • 对等进程通信:需要对等的通道
      • 临时文件:最古老的的IPC技法,灵活但有风险
      • 信号:每个信号都对机收进程产生默认作用,进程可以声明信号处理程序,让信号处理程序覆盖信号的默认行为
        • SIGHUP,重新初始化
        • SIGTERM,温和的终止
        • SIGKILL,立即杀死进程
      • 套接字:套接字类似文件描述符,创建时可以指定协议族来告诉网络层如何解释套接字名称
      • 共享内存:共享内存通常依靠mmap,把文件映射成可以被多个进程共享的内存

微型语言

  • 程序员每百行代码出错率和所使用的编程语言在很大程度上无关
  • 有两个好方法和一个坏方法做好微型语言的设计
    • 预先认识到可以使用微型语言设计把变成问题的规格说明提升一个层次
    • 注意到规格说明文件格式越来越像微型语言——规格中蕴含着行为
    • 错误的方法是通过扩展变成微型语言
  • 微型语言的范畴从声明性发展到命令性,从而逐渐具有通用性,当他们明确为完备图灵机时,它们就是解释器
  • 样例
    • SNG,PNG的纯文本表达
    • 正则表达式
    • Glade,描述GUI界面的XML文件
    • m4,一套宏指令集,规定文本串扩展成其他文本串的方式
    • XSLT,描述XML数据的变换
    • awk,将文本输入变换成文本输出
    • PostScript,向成像设备描述排班文本和图片的微型语言
    • bc、dc,任意精度计算
    • Emacs Lisp
    • JavaScript
  • 设计微型语言
    • 控制复杂度,声明性微型语言应该具有一个明确、一直、类自然语言的语法被人类所阅读
    • 扩展和嵌入脚本语言,实现命令性语言
    • 编写自定义语法
    • 慎用宏

生成

  • 人类其实更善于肉眼观察数据而不是推导控制流程
  • 数据比程序逻辑更易驾驭
  • 数据驱动编程:把代码和代码作用的数据结构分清楚,始终把问题层次往上推,尽量把程序逻辑转移到数据中
  • 专用代码的生成:尽可能少干活,让数据塑造代码,依靠工具,分离机制和策略

配置

  • 无论何时想增加配置选项,最好考虑下下面的问题
    • 能省掉这个功能么
    • 能否有无伤大雅的方式改变程序常规行为
    • 选项是否过于花哨
    • 需不需要一个独立的额外程序
  • Unix的程序配置信息一般在以下5个位置
    • /etc下的运行控制文件
    • 系统设置的环境变量
    • 用户主目录下的运行控制文件(通常用.开头)
    • 用户设置的环境变量
    • 启动程序命令行参数
  • 可执行未见后面加rc表示“运行控制”(命名来自CTSS的runcom命令脚本功能)
  • 一些最为常见的系统环境变量:USERLOGNAMEHOMELINESSHELLPATH
  • 常见的从-a-z的命令行选项的可能含义
    • -a,所有、添加
    • -b,缓冲区、批处理
    • -c,命令、检查
    • -d,调试、删除
    • -D,定义
    • -e,执行、编辑
    • -f,文件、强制执行
    • -g,全局
    • -h,头部、帮助
    • -i,初始化、交互式
    • -k,保留、杀死
    • -l,列表、登录、加载
    • -m,消息、邮件
    • -n,数字、否
    • -o,输出
    • -p,端口
    • -q,安静
    • -r,递归
    • -s,缄默,大小
    • -t,标记
    • -u,用户
    • -v,冗长
    • -V,版本
    • -w,宽度
    • -y,是
    • -z,启用压缩

接口设计

  • 从最小立异原则出发,启动后程序通常从下列来源获得输入或命令
    • 程序标准输入端的数据和命令
    • 通过IPC的输入
    • 已知位置的文件和设备
  • 最小立异原则不应被理解为在设计中号召机械的保守主义,新颖性提高了用户与接口最初几次的交互成本,但是糟糕的设计永远使接口令人痛苦而多余
  • “我们提倡以共生和委派策略来提高代码的复用并降低软件复杂度”
  • 最小立异原则目的就是为了减少用户在使用接口时必须学习的复杂过程
  • Unix接口设计历史:CLI => X
  • 接口的5种度量标准:简洁、表现力、易用、透明和脚本化能力
    • 简洁:事务处理需要的时间和复杂度需要有上限
    • 表现力:接口可以触发广泛的行为
    • 易用性:接口要求用户记忆的东西较少
    • 透明度:用户使用接口时,几乎不用记忆什么问题、数据或者程序状态
    • 脚本能力:容易被其他程序使用
  • CLI和可视化接口的对比
    • CLI更具表达力、脚本化能力、简洁性,适用于举例:SQL
    • 可视化接口透明度、易用性较好,适用性举例:画图、网页浏览器
    • 随着用户越来越熟练,对CLI接口的抵触也越少
  • Unix接口设计模式
    • 过滤器:接受输入,转换成其他格式,再输出到标准输出端;宽进严出、不丢弃不需要的信息、不增加无用数据
    • cantrip(没有输入输出)、源模式(无输入且输出在启动条件中控制)、接收器模式(接收输入但不发送东西到输出)、编译器模式(无标准输入输出,但会发送信息到标准错误端)
    • ed模式(编辑器模式)
    • roguelike模式(来自BSD的地牢探险游戏rogue,用字符阵列显示界面UI),如vi、emacs,没有鼠标参与,适合指法熟练的人
    • 引擎和接口分离,又或者模型和视图分离,了解MVC模式的人自然了解
      • 配置者、执行者
      • 假脱机、守护进程
      • 驱动、引擎
      • 客户端、服务端
    • 基于语言的接口模式
  • 浏览器作为通用前端
  • 如果程序没有什么有趣的或者惊奇的东西要说就应该闭嘴(有点意思)

优化

  • Unix的经验告诉我们最主要的就是如何知道何时不去优化
  • 最强大的优化技术就是不去优化
  • 先估量,后优化,直觉是糟糕的向导
  • 最有效的代码优化方法是保持代码短小简单
  • 核心数据结构必须留在最快的缓存
  • 吞吐量和延迟时间的权衡是普遍现象,例TCP、UDP
  • 对于减少延迟来说,阻塞或等待中间结果都是致命的
  • 按需计算出昂贵的结果,再缓存起来之后使用,可以兼得低延迟高吞吐

复杂度

  • 简单即美即雅即善,而复杂即丑即怪即恶
  • 程序员为了理解一个程序,会建立思维模型并调试之;程序的复杂度即模型建立和程序调试的困难程度
  • Unix思想的一个主题就是工具小巧锐利,设计从零开始,接口简单一致
  • 偶然复杂度的产生是因为没有找到实现规定功能集合的最简方法,可以通过良好设计去除;选择复杂度和期望的功能相关联,只能通过修改工程目标解决
  • 计算资源以及人类的思考,同财富一样,不是靠储藏而是靠消费来证明价值的
  • 选择需要管理的上下文环境,并且按照边界所允许的最小化方式构建程序

Implementation

语言

  • C和C++以增加实现时间和(特别是)调试时间为代价来优化效率
  • C的内存管理是复杂性和错误的渊薮
  • C语言最佳之处是资源效率和接近机器语言,糟糕的地方是槟城简直是资源管理的炼狱
  • C++试图满足所有人的所有要求
  • Perl是增强版的shell,它为替代awk而设计,适合大量使用正则表达式的地方
  • Java的设计目标是“write once, run anywhere”,但它并没有实现这两个最初的设计目标
  • Java对小项目是大材小用
  • Emacs Lisp传统上只用于为Emacs编辑器编写本身的控制程序

重用

  • 重新发明轮子之所以糟糕不仅因为浪费时间,还因为它浪费的时间往往是平方级
  • 避免重新发明轮子的有效方法就是借用别人的设计和实现,即重用代码
  • 文档并不能传达代码具现的所有细微差别之处
  • 开放源码和代码重用的关系,许多地方很像浪漫爱情和有性生殖的关系
  • 设计最好的实践需要情感的投入;软件开发者,同其他任何类型的工匠和技师一样;他们想要成为艺术家,这并不是什么私密。他们有艺术家的动力和需求,也拥有听众的欲望
  • 开放源码是从意识形态上解决这些所有问题的优先方法
  • 发布不够格软件的作者会承受许多的社会压力来修正或撤回代码(不一定)
  • 阅读代码是为未来而投资
  • 许可证
    - 许可证能够授权代码以某种形式使用,否则在版权法之下是禁止或者需要付费的
    - 非商业使用的许可证并不等同于开源许可证

Community

可移植性

  • C语言基于早期Ken Thompson的B语言解析器,脱胎于BCPL(Basic Common Programming Language),因此这个C代表Common(通用)
  • 在IETF传统中,标准必须来自于一个可用原型实现的经验;不幸的是,这并不是标准通常发展的方式
  • 搞笑RFC大概是唯一能够立即成为RFC的提议,比如RFC 1149(IP数据报的信鸽传递),RFC 2324(超文本咖啡壶控制协议)
  • 对于具备提倡标准资格的RFC,其规格必须稳定,经过同行评审,并且已经吸引了互联网社区的极大兴趣
  • IETF标准化过程有意提倡由实践而非理论驱动的标准化过程
  • 国际化的首要动作:分离信息库(配置)和代码
  • “暗含的意思就是,成为标准的最好方法就是发布一个高质量的开源实现” —— Henry Spencer

文档

  • “一切皆HTML,所有引用都是URL”
  • 绝大多数软件的文档都是由技术人员写给可能连最小公分母都不知道的普通大众的——渊博者写给无知者
  • 编写Unix文档的最佳实践
    • 数量多不会被认为是质量高
    • 信息密度适中,少用屏幕截图
    • 没人喜欢庞大的文档,考虑提供快速的摘要

开放源码

  • 开源开发的规则
    • 源码公开
    • 尽早发布,经常发布
    • 给贡献以表扬
  • major.minor.patch,补丁号修正错误和次要功能;次版本号为兼容的新功能;主版本号为不兼容的更改
  • 发布前对文档和README进行拼写检查
  • 基于所需功能而不是平台来编写移植层(面向接口编程)
    • #ifdef#if是最后一招,这通常是思路不当、产品过度差异化,无理由‘优化’或是无用垃圾聚集的先兆” —— Doug Mcllroy
  • 选择一个编码规范(lint)
  • 常见的标准文件命名规范
    • README
    • INSTALL
    • AUTHORS
    • NEWS
    • HISTORY
    • CHANGES
    • COPYING 项目许可证条款
    • LICENSE
    • FAQ
  • 以版本号来命名目录,考虑多版本在同一系统共存
  • 在设计讨论中更广泛的参与常常是件好事,但是如果列表相对开放,迟早就会有些用户在其上询问一些初级问题
  • 开源许可证
    • MIT:授予无限权利的拷贝、使用、修改和对修改的再发布,只要在所有修改版本中保留版权和许可证条款
    • BSD:授予无限权利的拷贝、使用、修改和对修改的再发布,只要在所有修改版本中保留版权和许可证条款;同时在广告和软件包相关文档中包含致谢
    • Artistic:授予无限权利的拷贝、使用和本地修改的权利。允许在发行修改后的二进制版本,但是限制源码再发行
    • GPL、Mozilla

未来

  • 分离机制(配置)与策略(算法)成为一个明确准则
  • Unix文件仅仅是个字节大袋子,而没有其他文件属性
  • 开放源码将软件业转变为服务业

–END–

2019年写下第一篇总结的时候,也未曾想到,这会成为一种一年一度的仪式,甚至能吸引到听众。但既然开始了,最好也能坚持下去。

如果说2019充满意外的话,2020只会是加大分量。不论是喜悦还是痛苦,2020留下的印记都格外得深一些。整体来看,由于遇到了一些预料之外的事情,在自我提升上向陌生方向多走了一些,不知是好是坏。

2020回顾

内在素质上,阅读技术非技术方面的书籍完成5本,小说6本。基本完成任务。

整体还是在东看看西看看的阶段,其中《程序员思维修炼》一书对思维方式的介绍补足了自己的知识体系中方法论的部分。《软技能》一书启发了在理财方面的一些认识。

大多数的书还是看了又忘,忘了就再看看总结,然后再淡忘。这个过程中,留在知识体系里幸存的部分并不多。尤其是编程相关的书籍,还是需要靠实践做项目来巩固所看的内容。没错,说的就是《Go语言入门学习》。

个人形象上,作息时间基本达到预期计划。但由于出现了新爱好(调酒),买衣服的计划被长期搁置。工装裤和卫衣还是去年那些,果然这两样买再多都不够。体重略有下降,回到80。算是基本完成目标。发型在推平后,又往偏分的方向留了起来,和此前的侧背区别不甚大。短发在身边朋友的反馈里居然还不错。

社交上,丢失和主动放弃了一些朋友,大多是三观不合或者事业变动。在和朋友的沟通交往中,更偏向主动。不过长期如此,还挺累的。热情不用更热情了,倒是应该更加增大交友面。有试图发展亲密关系的朋友,在对象上有过努力,也同时认识到很多道理,此处略去。

生活情趣上,在骑车、拉琴、看电影上仍旧投入较多业余时间,其中骑车的设备也一直还是那辆老旧的山地车。拉琴和看电影上也没有什么投入。同时,发展了喝酒和调鸡尾酒的爱好。在冰箱中屯了很多,对基酒和调酒也有了些认识。骑行上,因为圣僧的工作变动,离开了北京,一位年轻的同事成为了新的骑友。百里画廊未能成行,但是青海湖成功成行,也是一次难忘的经历。旅行上,疫情原因日本没去成,年末去了广州,吃了好吃的一本满足。拉琴上,一直也有练习,有能熟练演奏的曲目,不过都还需要看谱演奏。另外,也在公司内做过相关分享。新爱好上,看了第一次话剧,livehouse一直没遇到喜欢的乐队,未能成行。另外因为厨房卫生问题,在家做饭频率下降,反而提高了探店的频率。整体看,生活还蛮有趣味。

工作事业上,发展尚可,背靠还不错的项目组,也遇到了很多挑战,有了带小方向的机会,在团队建设上也开始有了思考。不过进步速度在下半年有所减缓,注意力有些偏移。希望在下一年能调整过来。

简单说几句

  • 积累太过理想化书面化,积累些现实点的东西,不是坏事
  • 穿衣上,裤长是最严重的问题
  • 要少喝酒,小酌小酌
  • 坚持作息和三餐,坚持骑行登山游泳轧马路
  • 要能清醒思考个人价值和现实价值
  • 一段长久的亲密关系应该是相互成就而不是相互束缚的
  • 爱合理分布在亲人、伴侣、朋友身上
  • 事业上决断力要提高,还要画饼能力,越大越好

2021展望

OK,又到了flag时间。吸取前两年的经验,目标不宜太明确,往往有意外惊喜。

  • 内在:看完囤积的4本业务书籍和10本左右小说
    • 再寻找一项和前端覆盖率类似的技术点去探索突破
    • 能清醒地评估个人价值,先知己
    • 回顾过去几年积累,完善知识体系
  • 外在:尝试新风格,保持健康,坚持运动
    • 坚持吃早饭和早睡
    • 增加锻炼频率
    • 发型尝试
    • 基本款补齐(同去年)
  • 情感
    • 平衡爱的收入和支出,更有自信
    • 能给一个人未来
  • 生活
    • 寻找一些提高生活幸福感的小东西或者经验
    • 保持室内干净卫生
    • 必要的旅行
    • 固定的户外长途骑行
    • 尝试新的菜式
    • 坚持拉琴、电影、探店
    • 至少一次livehouse
  • 事业
    • 思考职业规划和发展路线
    • 考虑落脚城市、医疗、教育等现实因素
    • 也去考虑父母的未来生活质量

就这么多吧,流水账短点为妙。诸位明年见~

Bye~

随便写写。旅程中的大多数美好,事后回忆,原来都在造化中。

序:准备阶段

这次旅行本该在去年国庆成行,由于假期有限等种种原因delay到今年这个长假。又碰巧赶上疫情和中秋带来的额外假期,于是7、8月便早早和档期向来紧张的圣僧约好。圣僧和我本是隔壁宿舍关系,从3年多前的一次结伴骑行结缘,是我的长期骑友。他是个旅行达人(友链一波:肥叉烧),身在外企,有钱有闲,经常出游,经验丰富。正巧这次碰上疫情,国外无处可去,和我定好日程后,三下五除二便把攻略搞定。和他一起出游真是省心。

Day 1:银川

出于下面多个因素考虑(其实主要是第三项),我们第一天先出发去银川。

  • 假期时间充裕
  • 慢慢适应高海拔
  • 中转机票更便宜
  • 凭借圣僧的会员,中转免费送一晚住宿外加去市区的大巴券

因为这次是从大兴机场出发,还是8点半的飞机,早上5点就得起床。

清晨的知春路口

我们在机场值机柜台碰面,托运完行李时间已经不早了,于是直接就安检进站。安检中还没收了我的修车工具(😢)。这也是这一路意外离开我的第一样东西。突然到我都没拍张照给它留个纪念。

进站后,借助圣僧招行信用卡会员,一起在VIP休息厅吃了个早餐,条件还不错。

VIP休息厅视角

9月26号的银川天气不错,我们住宿的酒店——西港航空饭店距离银川河东机场非常近。酒店建筑整体很新,周围被各种绿化围绕,完全感觉不到在西北。另外房间很大,由于远离市区,环境也很安静。加上是中转免费送的,算是我们整个旅程中最满意的住宿体验了。

西港-1

西港-2

在房间里稍作休息,我们步行到机场坐大巴前往市区,大巴券25元一位,我们使用的中转服务的免费大巴券直接乘车。大约1小时车程,我们在终点站鼓楼下车便进入了市中心。

鼓楼

稍微逛逛,我和圣僧很快就发现了多不胜数的蜜雪冰城,几乎不见其他知名品牌,剩下的也都是没听过名字的小牌子。在竞争上,蜜雪冰城比上有其很能打的性价比,比下又有标准化保证。4元的柠檬水、5元的奶茶、2元的圆筒冰淇淋。天哪,这也太香了。蜜雪冰城也将成为我们后面旅程中的一大精神寄托,按下不表。

蜜雪冰城

作为回族自治区的省会,菜单中自然没有猪肉,取而代之的是各种羊肉。从鼓楼步行街向西走过几个路口,我们惯例性在地在邮局寄了明信片。随后在旁边的迎宾楼吃了午饭,尝了下号称特色的手抓羊肉。这么一盘98元,没想象中好吃,吃了三分之一就有点腻了。最后打包给圣僧当了晚餐。

手抓羊肉

吃饱喝足,下个目标是带些伴手礼。宁夏的枸杞是比较出名的,其中以中卫的为最优。枸杞分红枸杞和黑枸杞,都没有明确的药用价值。但是日常保健还是有用的,号称能补气虚,很适合阳气虚弱的男性。黑枸杞据说劲儿更野一点。稍微做了下功课,我们在超市一人来了2斤,一斤75元。事后看,这个价格似乎有些上当,建议各位小买怡情即可。

银川城区内的共享单车似乎都是电动车,不论是青桔还是小蓝,还是美团单车。在一天的观察下,只注意到凤毛麟角的人力单车。这到底是因为当地人自行车技术很烂还是电动车驾驶技术很普及?挺有意思。

共享单车

天色渐暗,在人民公园感受了城市氛围后,我们乘坐大巴回到西港航空饭店,为第二天前往西宁做准备。

Day 2: 西宁

西宁相比银川更有省会的感觉。不同于银川的回族居多,西宁有着比较明显的藏族气息,藏民也很常见。高海拔的强烈紫外线下,大多数人都是黝红的脸。由于第二天要坐大巴前往西海镇,我们这一天住在汽车站旁边的如家,在市区中处于偏东郊的位置。不出意外,楼下又是一家蜜雪冰城。

酒店

西宁城区内有湟水自西向东流过,我们下午先是打车到了河边的中心广场,类似北京的元大都城垣遗址一样,不过明显更大且更有层次,湟水河宽度近似海河。在河边坐也好,走也好,感觉都是很不错的。下午3点,气温十余度,在河边的长椅上,一直都能看到飞机在高楼的天际线上划过。

西宁海拔约2100左右,处于多个山脉中的河谷地区,适宜建设城市的面积不大,只有大概一个十字形状的区域。所以城区中的楼房都格外的高,且能看出一些地势起伏。整体感觉,似乎来到了小号版的重庆。和重庆一样,这里看不到共享单车。

城市景观

不过从一些城市的角落,还是能瞥见藏在深处的旧工业时代的厂房气息。

旧厂房

我比较偏好用随便走走四处看看的方式感受城市的风格。下午我们从中心广场绕过力盟商业区,再穿过九龙城寨般的商业巷大世界,从人民公园门口向回走,最后又回到颇具人气的力盟步行街。不能免俗,吃了顿呷哺呷哺。

呷哺呷哺

Day 3: 西海镇 - 种羊场

里程:48km

去西海镇的车票我们是提前一天买好的,25元。但因为游客不多,其实也可以当天早上买。从火车站背后上京藏高速,一路穿过湟中区、湟源县、海晏县就到达了最终骑行的出发点西海镇,全程约2小时。租车上,我们早已提前和当地租车师傅约好,6天行程,租车500元,外加解决住宿700元(对租车店老板安排的住宿不要抱太高期望)和押金500元(结束骑行后退还),一共1700元。

西宁到西海镇

西海镇是海北藏族自治州的州首府,不过却意外的荒凉,街道两边政府部门倒是周正齐全,基础设施也是一应完善,可是就是人迹稀少,连车都没几辆。我们下车后已经10点,第一天尚有40多公里的路程要骑。我们调试好车辆,装好驮包,穿戴好装备,擦好防晒(这个十分重要),简单吃了个饭就出发了。

出发合影

沿着刚察路直走,穿过和G213国道的路口,就上了环湖东路。沿着环湖东路直走就可以达到第一天的目的地——湖东种羊场。路线也是相当简单。

路牌

初见丘陵和草原,配合着壮观的云层,还是很有新鲜感的。

草原1

草原2

山丘1

第一天的路并不像路书中那样全是平路,相反上下坡并不少。幸运的是,路边的景观稍微减缓了骑行的疲惫感。

上坡1

上坡2

穿过和G213国道的分岔路口后,我们离湖越来越近了。

996

路牌

随着湖水越来越近,金银潭和沙岛上起伏的沙丘却是也越来越显眼。湖边的荒漠化土地是我预先没想到的,据说是气候原因导致。但从沿路看到的各种牛羊来推断,和过度放牧可能也有些关系。

沙漠1

牦牛1

穿过金银滩沙漠区,是一个大下坡,下坡后路两侧又逐渐恢复了植被和河流,看起来顺眼多了。

大下坡

绿地

青海湖边地广人稀(整个青海除了西宁和海东应该都是地广人稀),除了风声,路两旁十分宁静,若是没有毒辣的紫外线,实在惬意极了。

车1

再往前骑数公里,终于到达种羊场。这并不是一个镇或县,顶多是围绕湖东种羊场的一个聚落。住宿环境自然十分感人。卫生糟糕,门窗都可以从外面打开。具老板所说,这里民风淳朴,姑且只能信了。

种羊场住宿

幸好有炕锅羊肉和壮观的落日场景可以抚慰。

炕锅羊肉

落日1

落日2

青海湖周围海拔3200到3400,昼夜温差较大,太阳一落山,气温就比较冷了。种羊场也无甚可逛,我们直接倒头就睡,洗漱更衣直接放弃。只期待第二天住宿能好点。

Day 4: 种羊场 - 江西沟

里程:55km

第二天一早我们吃了点买好的干粮,擦了防晒就直接出发。种羊场实在没什么好留恋。青海湖环湖的路都很直,两侧要么是无边的草原,要么是起伏的山丘,要么是看不到头的湖面。以至于路也看不到终点,似乎永远都骑不到头。

路1

不过在骑行中,能察觉到远处的村庄、山岭、景观一点点靠近,还是蛮奇妙。

路2

路边是牧民们的牛羊,似乎和内地的品种并不相同。牦牛们都很沉默,羊们也是,我蹩脚地起个头,它们也只是懒懒地抬个头望向我,依旧沉默。马儿们有的自由吃草,被人看管的则用来招徕游客骑行创收。

牛

羊

骑过错果,便告别了环湖东路S207,来到G109国道,相比环湖东路,国道上的车辆明显多了很多。

错果

路牌2

从环湖东路和G109的交叉口到二郎剑景区,靠湖一侧都有维护良好的辅路,路面起伏不大,还有成片的油菜花可以欣赏,油菜花都不高,游客在其中拍照能露出脑袋,从车上看颇有趣味。

油菜花

路的另一侧是连绵的不知疲倦的山丘,它将一路陪伴我们告别G109国道。

山丘2

临近二郎剑景区有可以直接接近湖面的地方。这是一路第一次能亲近湖水的机会。

青海湖

在二郎剑边的中石油稍作休息,是一路枯燥的景观,16km后便到达终点江西沟。令人欣慰的是,住宿环境有了些微的改善。

江西沟住宿

Day 5: 江西沟 - 黑马河/茶卡

里程:48km

这一天的里程虽然不长,却是环湖几天中,最疲惫的。原因是没有止境的逆风。一路上,景观和前一天无异,枯燥的山丘和草原,连湖面也不太能看见了。因为恼火的逆风,我甚至都没心情抬头看看路两旁的风景。

也许是因为大风把碎云都卷走,天上的云倒是挺好看。

云1

云2

我大约提前圣僧1个小时到达,考虑到高原和糟糕的风向,圣僧临时修改计划,将第二天的茶卡盐湖之旅提前,把本来一天到达刚察县的计划拆成石乃亥 - 刚察县。这一决定不仅让我们看到了茶卡盐湖上的落日,也极大提升了后两天的骑行体验。

黑马河

黑马河位于G109和环湖西路S209的交叉口,前往茶卡、鸟岛,大西北自驾环线、环湖骑行都要经过这里。本来是发展不错的一个小镇。近些年考虑到环保因素,拆了大多数建筑,统一由政府管理建设,道路管道也大兴土木整修,目前看起来完全感受不到当地人所说当初的繁华。

我们运气不错,碰巧民宿老板要前往茶卡办公事,在圣僧下午3点到达后,我们搭了个便车去往茶卡(来回两人300元)。走G109去茶卡镇要穿越橡皮山,最高海拔3800m,且单程80km。对于骑行来说不太现实。老板比较健谈,一路和我们介绍了青海湖的旅游发展以及周边地理人文,挺有意思。

橡皮山1

橡皮山2

茶卡盐湖的风景十分看天气。晴天无风的天气下,才有天空之镜的感觉。我们去的那天虽然晴空万里,不过风力较大,差不多夕阳落山时,西风终于转弱,得以让我拍到下面的景象。

夕阳3

夕阳4

盐湖门票原价60,对青海、湖北、浙江籍游客免票。小火车单程50,车速和步行差不多,没有强需求可以不做。小火车沿着一条向湖区深处的路一直开到最里面,大约30分钟。路的西边湖水较浅,许多已蒸干出盐巴,还能看到湖盐厂的旧铁路和新厂址。许多游客穿着租赁的胶鞋在水面嬉戏。

盐湖游客

搭配雪一样的湖盐,有种北极村的感觉。

"北极村"

坐车回到黑马河时已经晚上9点了,晚饭是牦牛肉串 + 牛肉面,巴适得很。

肉串加牛肉面

Day 6: 黑马河 - 石乃亥

里程:39km

没有了恼人的逆风后,这一天好骑多了(再加上里程短,应该是环湖最轻松的一天)。骑出黑马河镇,就和G109国道告别,上了环湖西路,车辆明显稍多了,整体体验大大提升。经过一段笔直的路程,可以绕到离湖不远的地方,接下来的一路也都如此,还能时不时看到自驾游的游客在路边拍照。

湖边

等接近石乃亥后,我们便离湖越来越远。经过一个小上坡,便能远远看到石乃亥乡的轮廓了。

小上坡1

小上坡2

石乃亥不大,基础设施倒是挺完善。由于到得比较早,我们很轻松吃了个土火锅作为午饭。土火锅里面有牦牛肉和各种素菜,价格145,还算划算。我们大快朵颐了快1个小时,最后也没吃完。

土火锅

Day 7: 石乃亥 - 刚察县

里程:78km

骑出石乃亥约20公里,我们告别了海南共和县,来到海北刚察县境内。一如往常又是广袤的草原和起伏的山丘。骑过关闭的鸟岛,就来到了湖边的最后一段路。

湖羊

站在环湖东路的末尾处回头望,能看到最后一片湖面的波光粼粼。

波光粼粼

从岔路口右拐上G315国道,会贴着青藏铁路走一段。之后越过新修的西和高速便进入县城内。

岔道

铁路1

铁路2

刚察县虽然只是个县城,但是热闹程度和城市美化程度一点不亚于西海镇,甚至更甚。

刚察县

县城里的藏城电影院也在营业,虽然放映厅很小,还没有大学大教室大,但两人包场看《我和我的故乡》的体验还不错。

刚察电影院

吃厌了羊肉,我们晚饭在一家成都冒菜馆解决了晚饭,能有吃菜自由的感觉真好。

冒菜

另外,刚察的旅游厕所做的是真不错,体验比北京的管理公厕都要好。

刚厕

Day 8:刚察县 - 西海镇

里程:90km

最后一天离湖就很远了。出刚察县先是一个大上坡,接着几乎一路平路骑28km可以到达哈尔盖河边的哈尔盖镇。

刚察县上坡顶

在哈尔盖中石化稍作休整,沿着315国道再骑20km到达甘子河乡,开始最后一个长坡,从海拔3200m来到3415m。

哈尔盖车站路口

G315-170km

从这个坡顶一个俯冲下来穿过西和高速,便从G315回到G213国道。这一段逆风很大,由于修路,大车也很多。它们开过的同时,尘土飞扬,恨不得把自行车都给吸进去。

G315下坡

在一段艰辛的骑行后,我们来到修路的终点,也是整段路的最高海拔3445m,接着一路下坡,20km后在G213的拐弯处,西海镇已经越来越清晰。

骑行结尾

我们最后还了车,又寄了明信片。在西海镇停留了最后一天。这里作为海北藏族自治州首府,各种政府部门和基础设施完备齐全。就是人迹稀少。偌大的跳广场舞的活动中心也没个人影。

海北藏族自治州文化活动中心

夜晚了,路边的住房都亮起灯,仿佛又回到当初两弹一星时期建立原子城的岁月。

原子城夜景

Day 9/10: 西海镇 - 西宁 - 北京

最后一天,我们回到西宁。重回城市的感觉真好,标准化和工业化给人太饱满的安全感了,躺在床上,回想起这为期6天的骑行,尽管风景很原生态很美好,但这也是以牺牲了工业化和标准化为代价的。除开住宿环境,缺少便捷的物流和交通,让沿途小镇的餐饮费物价居然比在西宁高。离开西宁前往西海镇前,我们恐怕没有预期到,以前毫不在意的蜜雪冰城和德克士居然成为了一种奢求和精神寄托。也许,人们都是图个新鲜,享受惯了工业化城市化带来的福利,居然也会想吃吃贫瘠和原生态的苦。

经过此次“艰苦”的骑行,圣僧说他心心念念的川藏线骑行也要好好考虑考虑了,可能磨练人的不是骑行本身,反而是沿途令人生畏的住宿环境。我们脸上和腿上的晒伤需要时间痊愈。不知道,圣僧心理上的畏惧又需要多久痊愈。

–END–

0%