普通视图

发现新文章,点击刷新页面。
昨天以前Python 工匠

一份关于 AI 编程的简明行为指南

2026年3月20日 17:31

AI 编程发展迅猛,Claude Code、Codex 等 AI Agent 已成为许多软件工程师工作时必不可少的得力助手。然而,对于如何在项目中更好地实践 AI 编程,目前仍然是一副“八仙过海,各显神通”的模样。

假如同一个团队里的工程师,在如何使用 AI 工具的“大问题”上没有达成共识,就会出现协作上的摩擦,对项目产生不良影响。

因此,本文尝试面向软件工程师群体,对 AI 编程的推荐行为实践做一些总结,弱化具体的工具和技巧(比如 Spec 驱动还是不 Spec 驱动?),着眼于更通用的方面(比如初级工程师该完全委派 AI Agent 修 bug 吗?)。

内容分为“对所有人”和“对初级工程师”两部分,其中“对初级工程师”部分,针对这类人群身处的特殊阶段,提供了一些量身定制的建议。

说明:本文部分内容由 jianan、wklken 参与共创;本文观点基于笔者所身处的工作环境总结而来,不一定完全适用于其他项目,酌情采纳;

对所有人

谨记 Agent 不对代码担责

  • AI Agent 拓展了人的能力,是一种“分身”,但不直接承担任何责任,人依旧是代码的最终责任人
  • Review AI 生成的代码,理解它们,不要提交自己不理解的代码
    • 如何判断自己是不是真的理解某件事?“费曼学习法”——你是否能向其他没有背景的人解释它?
  • 不要过度依赖其他人(AI)在 Review 阶段对你的工作做最后把关,这是不负责任的表现

多“协作”,少“委派”

  • 两种心智模型,“协作”意味着你在理解对方的基础上共同做决定,“委派”意味着你只关注最终结果
  • 不要去模仿一位产品经理,仅用自然语言高高在上地提需求,不关注实现细节
    • ~~“怎么实现我不管,这个需求很简单!”~~
  • 深入探索程序设计和整体结构,和 Agent 一起推敲出更优的答案
  • 重复一次:AI 不对代码担责,也不对项目的长期可维护性负责

拆分你的 PR

  • AI 写代码又快又多,很容易产出几千行改动的 PR
  • 大 PR 极大增加了 Review 难度,Review 过程容易浮于表面,最终导致工程质量不可控
  • 不憋“大招”,控制 PR 粒度(推荐<600行),小步快跑,分阶段交付
  • 如果 PR 实在无法拆分,尽量提供 PR 的设计文档(design_notes 目录)以辅助 Reviewer 理解

说明:设计文档并不是详细到代码级别的 Spec 规格说明,而是站在更高抽象层的,供人阅读的功能“说明书”。推荐由人编写核心内容或框架,AI 可辅助补全细节。

善用 Plan 模式,多做前置探索

  • 开发新需求,应当先熟悉相关模块/领域知识,了解项目当前实现等(可借助 AI 调查梳理)
  • 不要在真正理解需求前,就让 AI 开始写代码
    • 重复一次:“费曼学习法”判断自己是否真正理解
  • 先提问或启用 plan 模式与 AI 澄清需求(学习 AI 提示词技巧)
  • 别被 AI 带着走,独立思考,怀疑主义,通过反问等方式来开拓思路
  • 只有对问题有清晰的认知,才能判断 AI 的实现是否合理,而正确的判断无价

采纳稳定的库,优于 AI 从零实现

  • 对于工具函数和库,比起使用第三方库,AI 有种偏向于“自己手撸”的倾向
    • 不是说“造轮子”一定不好,但是……
  • 解决常见领域的成熟问题(例如 gjson),倾向让 AI 采纳库而不是自己实现
  • 如何知道哪些库适合?跟 AI 聊,或自己动手查阅资料,日常积累不可少
  • 多个库可用时,人判断最适合项目的(功能契合度、活跃度,未来演进等等)

创建 PR 前使用 AI Review

  • 创建新 PR 前,应当使用 AI Agent review 当前改动并根据建议调整代码
  • AI 前置 review 可减轻之后人工 review 的负担,提升迭代效率
  • 可使用任何 review SKILL 或简单 prompt 完成

下面是一条示例 prompt:

你是一名代码评审专家。和 master 分支对比以了解当前项目的所有改动,review 这些改动并产出报告。我最关注这些内容:

- 代码逻辑错误或未考虑边界情况;
- 常见的不符合编程语言最佳实践的设计或写法;
- 过于复杂的可以被简化的类/接口/函数设计;
- 可能存在安全隐患的写法(例如 SQL 注入);
- 重新实现了成熟库或项目其他模块已实现的功能(未复用);
- 其他任何不符合项目规范、打破一致性,有问题的内容;

注意:创建 PR 前的 Review 并不直接替代之后的 AI 和人工 Review,这只是一种前置的自查。

让改动可验证,并总是验证

  • 对于 AI 实现的代码,做好自动化测试和自测工作
  • 杜绝提交未经测试的代码,去等待 Review 来“兜底”
    • 代码 Review 不可能找到所有问题,有些问题只有实际执行才能触发
    • 越后期发现的 bug,修复成本越高
  • 鼓励编写自动化测试代码(单测、API 测试)等,做到 AI Agent 可自动验证,进入“验证<->修复”循环

其他

  • 保持好奇心:对 AI 的代码,其中觉得新奇的库、模式或代码片段,打破砂锅问到底
  • 拓展能力边界:打破思维惰性,去解决一些“如果没有 Agent”你绝不会去完成的任务
  • 做正确的事:如果一开始的需求就错了,AI 会帮你在错误的道路上极速狂奔,所以“做正确的事”从未像现在这么重要

对初级工程师

经验不够丰富的初级工程师,对于编程语言和项目领域的理解尚处于早期,此时过度依赖 AI 或拖累学习效率,长远来看对能力发展有不良影响。因此,针对该类人群提供特定建议

质量胜过效率

  • AI 虽然是提效工具,但处于初级阶段时,应当更注重质量而非速度
  • 快速地交付低质量的代码,实际上更低效(后续 review 反复调整、技术债务)
  • 如果更慢的方式,能让自己学到更多东西,那么选更慢的(沟通好 deadline 的前提下)

尝试手动修复 Bug

  • AI Agent 非常擅长全自动修复 Bug,但对于初级工程师…
  • 不建议一股脑全丢给 Agent,由 Agent 完全无监督地“自动化”修复
  • 用聊天模式,提出自己的思考和疑问,在分析 Bug 的过程中加深对项目的理解
  • 这有助于更快、更深入地理解代码逻辑和架构

重复一次:多把 Agent 作为协作者来共同工作,而不是完全委派给 Agent。

自己先动脑,在 AI 给出的方案之外寻求

  • 对任何任务,AI 给出的是“最有可能成立”的答案,但它不一定适合你
  • 盲目遵循,丧失了拓展自身技术视野的可能性
  • 先花一个固定的时间(如半小时)自己设计思路,之后询问对比 AI 给出的思路(先有解再“对答案”)
  • 鼓励极限探索 AI 的能力边界

其他

  • 让 AI 反问自己,通过问答来加强对项目和相关技术的理解
    • 类似让 AI 给你做一场关于项目的模拟“面试”
  • 读官方文档:AI 知识有截止日期,存在幻觉问题, 文档仍然是深入掌握某项技术的首选(慢即是快
  • 补充软件设计和架构模式方面的短板,了解和学习设计模式、领域驱动设计(DDD)等
    • 现阶段 AI 并不擅长整体设计,更擅长在明确的框架下做具体的函数级实现
    • 良好的设计、健壮与灵活的架构,保证项目可持续迭代,离不开人来掌舵
    • 通过阅读书籍构建自己的知识体系
  • 关注非功能性需求:AI 往往默认只关注“功能跑通”,忽视其他工程层面的问题:安全、可维护性、并发安全和扩展性等,有意识地去关注它们

结语

如有不同看法或补充条目,欢迎讨论。

AI 编程是一种“框架”

2026年2月10日 12:04

使用 AI 类编程工具越多,我便越来越强烈地感觉到:AI 编程是一种“框架(Framework)”。

框架是每位程序员的老朋友,它通常针对特定领域所设计,能极大提升编码效率。以 REST API 服务为例,一些成熟的框架(比如 Django REST Framework)能做到仅需定义数据模型和视图类,便生成一套功能完备的 CRUD API 服务。

AI 编程工具和传统框架一样,都是一种“杠杆”,它们允许人们用少量输入撬动庞大的功能。二者区别仅在于输入类型不同,框架需要代码(或配置),而 AI 仅需一句简单的自然语言提示词——“写一个书店网站”

框架的问题

作为一种新型框架,“AI 编程”所带来的便利性,足以把人类从前发明过的任何一种框架按在地上摩擦,但是,框架天生所具备的问题并未消失。

1. 抽象泄露

所有框架都有一个共性,它们提供了极高级的抽象,以降低实现特定功能所需的工作量。比如,Django 的 ORM 提供了“数据模型”这层抽象,免去了手写 SQL 语言、设计数据校验等繁重工作。

但不幸的是,抽象都会泄露

一名 Django 初学者编写的网站上线后,客户投诉爆表,因为列表页加载极为缓慢。此时,打开数据库监控面板,他会发现这个看似简单的页面,每访问一次就会发起 400 次数据库查询。要解决该问题,他必须弄脏双手,撕开 ORM 这个抽象层,弄懂“属性如何被加载?”,搞清楚“N+1 问题”的真相。

而使用 AI 编程工具,“抽象泄露”则发生在我们不得不抛弃轻松的自然语言提示词——“实现 XXX 功能”,转而说出“你对 items 变量的状态转换的理解不对”的时刻。

毋庸讳言,现阶段的 AI 仍存在智能瓶颈。当自然语言无法驱动 AI 准确完成工作时,打破抽象,打开已布满灰尘的 IDE,用精确到变量名的提示词帮 AI 找到根因,是我们的唯一选择。

2. 丧失控制权

世上实现代码复用的所有模式,大致可被分为两种:框架(framework)和库(library)。

要区分一个东西是框架还是库,关键在于找到“谁控制着程序的整体结构?” 这个问题的答案。使用框架,控制权牢牢掌握在框架手中,你所编写的程序,是镶嵌在伟岸的框架程序中的一部分。这有点像是去完成一副卡通图,所有元素都已用浅灰色线条勾边,你只负责给不同部位涂上不同颜色。

而使用库,控制权则仍掌握在你手里。你负责调配和使用不同的库,来搭建起整个程序。这像是玩积木,手边有千千万万个积木和模组,你负责把它们组装成想要的样子。

丧失控制权会带来哪些危害?这主要体现在可修改性上。

使用框架时,当程序需要定制一项功能,由于缺少控制权,工作的开展依赖于框架其是否支持该自定义选项。如果支持,那么整个过程会像热刀切开黄油一样丝滑。但假若框架并不支持,那么很不幸,你极可能在一个简单需求上花掉成吨时间。

平心而论,在“控制权”维度上,AI 编程还称不上天生属于“框架”还是“库”。但不可否认的是,当下最流行的一种趋势就是把它当成框架来使用。比如在 Vibe Coding 时,人类仅负责提供模糊的自然语言提示词,毫不关心程序的入出口与整体结构,一切由 AI Agent 这个框架所掌控。

认知成本:DRF 的启示

为何人们倾向于将 AI 当成框架?这是个有趣的问题。

我认为答案的关键在于一个词:认知成本。框架与生俱来的特质,给了我们一种暗示:你可以用最低的认知成本实现最复杂的功能。 而人类生来就爱“省力”,少动脑而完成更多,是我们与生俱来追求的目标。

拿 DRF(Django REST Framework) 来举例。在 DRF 框架中,为一类数据模型实现一套 CRUD API,只需编写以下 4 行代码:

class AccountViewSet(viewsets.ModelViewSet):
    queryset = Account.objects.all()
    serializer_class = AccountSerializer
    permission_classes = [IsAccountAdminOrReadOnly]

一个 class,三个属性,极低的认知成本,全套 RESTful API,怎么看这都是一门极其划算的生意。

但是,正如前面所提到的,框架的便利性是一把双刃剑,伴随框架出现的“抽象泄露”和“控制权丧失”问题不可避免。

做个假设,现在我们需要调整 API,让 create 方法返回不同的响应体,为 list 方法增加额外的过滤条件,基于以上代码该怎么做?答案是,先重写包含 get_queryset 在内的多个方法,再用一批 if/else 补丁将整个 ModelViewSet 爆改到面目全非。

既然如此,有没有更好的办法?答案是肯定的,我们完全可以弃用高级的 ModelViewSet,仅采用不附带任何魔法的 ViewSet,完全基于模块的组合来实现相同功能。

class AccountViewSet(viewsets.ViewSet):
    permission_classes = [IsAccountAdminOrReadOnly]

    def list(self, request):
        # 针对 list 的特殊过滤逻辑
        qs = Accounts.objects.filter(...)
        serializer = AccountSerializer(qs, many=True)
        return Response(serializer.data)

    def create(self, request):
        # 针对 create 的特殊响应结构
        serializer = AccountForCreationSerializer(obj)
        return Response(AccountForCreationSerializer(obj).data)

相比起来,新方案最直观的变化是代码量变多了,也显得更加啰嗦,但更大的改变其实发生在“认知成本”层面上。

ModelViewSet 版代码里,认知成本表面上很低,但更多成本其实被藏了起来,作为一种隐藏的认知债务所存在。后续,当需求发生变化时,这些债务给我们实现功能带来了巨大的麻烦。调整代码结构后,原本隐藏的债务浮出了水面,代码的可视性和可维护性也得到了有效提升。

假如以“框架和库”这个设定来观察上面的案例,新代码的组织模式更接近于“库”——人作为程序的主人去组织整个功能,而非“框架”——人仅作为 ModelViewSet 的仆人去填补空缺。

至此我们可以看到,框架与库并非一种对物品的二元化严格分类,而更像两种不同的思维方式。因为即便是在 DRF 这个庞大的框架中,也存在各种不同的代码组织模式,一些偏框架,另一些则明显偏向库。

也许,该把 AI 编程当成一种库

回到 AI 编程。将 AI 编程作为一种框架,会引导我们采用框架式思维,不断追求编写更短的提示词、对代码付出更少的关注,付出更少的认知成本来达成目的。

然而,就像前面的例子所展示的,在这种思维模式下,认知债务将不断堆积,框架与生俱来的“抽象泄露”与“控制权丧失”问题,也将在未来对我们产生严重危害。

既然如此,何不转变思维,将 AI 编程当成一种“库”?就像去使用任何其他库一样,我们作为程序的主人,调用它来完成工作。

这种思维模式的转变,可能意味着:

  • 不再追求“写更少实现更多”:用更少的提示词(代码)实现更多功能,看上去很美,但也意味着大量的认知债务随之累积;
    • 找到编写提示词的“甜蜜区”,付出 相对较少 而非绝对意义上的最少的认知成本;
  • 关注程序结构: 比起在前 AI 时代,你现在可能更需要关注程序的整体结构,作为总设计师去设计整个程序,将正确的结构和约束内化到 AGENTS.md 中;
  • 更精准的提示词: 在理解已有程序的基础上,编写更精准的提示词来引导 AI 完成工作,而不是任其发挥,让 AI 主导一切;
  • 审查代码: 即便使用同一种框架,在遇到棘手问题时,一位熟读框架文档的人也会比另一位愣头青更有效率,如果把 AI 编写的代码归为框架,那么你应该去审查这份代码,从而在不可避免的“抽象泄露”发生时,将其危害降到最低。

毋庸置疑,AI 编程是一项革命性的进步,它的出现,让我们可以站在一个之前只存在于想象中的思维高度来编写程序,但 AI 编程毕竟不是魔法,如同历史上任何一个框架,极度便利的背后隐藏着抽象泄露、控制权丧失及认知债务等多重风险。

所以,AI 编程仍然不是银弹,它无法将我们从认知成本中真正解放出来。然而,在真正的银弹出现前,认识到 AI 编程作为一种新型“框架”的局限性,并适当使用“库”的心智模型来驾驭它,或许是我们的最佳选择。

Python 的 OrderedDict 为什么有序?

2025年10月30日 10:42

现在是 2025 年,网上已很少见到 Python 字典有序性的相关讨论。自从 Python 在 2018 年发布 3.7 版本,将“字典保持成员的插入序”写进语言规范后,人们已渐渐习惯有序的字典。那曾经调皮、无序的字典,早已像 2.7 版本一样成为过去,只在某些老登们忆苦思甜时被提起。

而在那个字典无法保持顺序的年代,如果我们要用到有序的字典,我们用什么?答案是:collections.OrderedDict

但是,随着内置字典已经有序,OrderedDict 似乎也渐渐变得不再必要。不过,截止到目前(3.14 版本)为止,它仍然存在于标准库模块 collections 中。这主要是出于以下几个原因:

  1. 保持向前兼容,已依赖其的旧代码可以保持不变;
  2. 行为不同:OrderedDict 在判断相等性时会将键顺序纳入考量,内置字典不会;
  3. 更多特性:OrderedDict 拥有 move_to_end 等方法。
>>> d
OrderedDict([('a', 1), ('b', 2), ('c', 3)])
>>> d.move_to_end('a')
>>> d
OrderedDict([('b', 2), ('c', 3), ('a', 1)])    # 1
  1. move_to_end() 可以把某个键移动到字典的末尾

本文将深入 OrderedDict 类型的内部实现,了解在 Python 中实现一个有序的字典,需要做哪些工作。

注:具体来说,标准库中的 OrderedDict 数据结构有 C 和 Python 两套不同实现,各自适用不同的运行环境,二者的实现类似;本文针对 Python 版本编写。

一个双向链表和另一个字典

OrderedDict 是一个有序的字典,它像普通字典一样支持键值对操作,只是保留了键的顺序。实现 OrderedDict 的关键在于以下两点:

  1. 继承 dict:自动拥有内置字典类型的所有操作,所有键值对存放在 OrderedDict 对象自身中——self 就是一个 {}
  2. 引入额外数据结构:引入额外的有序数据结构,让其作为一种外部参考来维护键的顺序。

数据结构有很多种,到底该使用哪一种来维持键的有序性?由于字典是一种基于哈希表(hash table)的高性能结构,最擅长在 O(1) 的时间复杂度下完成键值对的存取操作。因此,OrderedDict 所需的用于保存键顺序的额外结构,首先应满足性能要求——“维护顺序”的过程不能拖慢字典的原操作。

为了达到这个目标,OrderedDict 同时使用了两个数据结构:一个双向链表和另一个字典。

  1. 双向链表:有序结构,根据链表节点可以方便地在链表中新增或删除成员(时间复杂度为 O(1)),节点所保存的内容为 OrderedDict 的键名。
  2. 另一个字典:在链表中查询一个节点,通常需要按序遍历完所有节点,平均时间复杂度是 O(n),这显然不满足性能需求,因此 OrderedDict 引入了另一个字典作为链表的索引,使用键可快速拿到链表节点(时间复杂度 O(1))。

整个数据结构如下图所示:

图:OrderedDict 内部数据结构示意图,包含三大数据结构:self(保存键值对的字典自身)、self.root...(有序双向链表)、self._map(链表索引字典)

下面以 __setitem__ 方法为例,详细看看 OrderedDict 如何完成键值对的写操作,以下是相关代码:

    def __setitem__(self, key, value,
                    dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link):
        'od.__setitem__(i, y) <==> od[i]=y'
        if key not in self:
            self.__map[key] = link = Link()  # 1
            root = self.__root
            last = root.prev
            link.prev, link.next, link.key = last, root, key  # 2
            last.next = link
            root.prev = proxy(link)  # 3
        dict_setitem(self, key, value)  # 4
  1. 创建一个新的链表节点,并将其存放到 self.__map 中,之后可以通过 key 来快速读取该节点;
  2. 修改新节点 link 的前后节点,将其插入到 root 前,也就是作为尾部节点加入链表;
  3. 修改另外两个相关节点 last(原尾节点)root(根节点),至此完成整套链表操作;
  4. 修改自身字典中的对应键值对。

假设执行代码 d["aa"] = 4,往字典中插入一个新成员,整套数据的变化如下图所示:

图:插入键值对 "aa": 4,OrderedDict 内部数据结构发生的变化

双向链表、链表索引字典,以及 OrderedDict 字典自身,都需要处理 "aa": 4 这个新成员。

__setitem__() 类似,__delitem__()(删除成员)和 pop()(弹出成员)方法除修改自身字典外,也需要调整对应键在链表和索引字典中的数据状态,在此不再赘述。

为了让 OrderedDict 在被迭代时能有序返回所有键, __iter__ 方法也需要有所调整,下面是相关代码:

def __iter__(self):
    'od.__iter__() <==> iter(od)'
    root = self.__root
    curr = root.next
    while curr is not root:
        yield curr.key
        curr = curr.next

可以看出,遍历一个 OrderedDict,实际上就是在遍历它内部的双向链表。遍历由一个 while 循环完成,它将链表中每个节点通过生成器返回,从而实现有序。

小结

通过引入额外的数据结构,OrderedDict 最终实现了有序。双向链表加索引字典的组合,最大程度降低了 OrderedDict 在数据存取时的开销,虽付出了额外存储空间,但仍维持了较好的存取性能。

有趣的细节

在阅读 OrderedDict 实现时,我发现几个有趣的细节。

1. 对 weakref 的使用

Python 语言的垃圾回收主要基于引用计数完成。引用计数算法简单高效,但唯独无法很好地处理“环形引用”。以下面这个场景举例,在操作双向链表时,向链表尾部插入新节点,需要:

  1. 将新节点的下一个节点修改为根节点(link.next = root
  2. 将根节点的上一个节点修改为新节点(link = root.prev

这将在 linkroot 对象之间创建一个环形引用,二者都将使对方的引用计数加一,最终导致无法有效被 GC 及时回收。

介于此,OrderedDict 在处理类似情况时使用了 weakref 模块。相关代码如下:

link.prev, link.next, link.key = last, root, key  # 1
last.next = link
root.prev = proxy(link)  # 2
  1. link 和 root 通过 link.next 建立了一个方向的引用关系;
  2. root 和 link 再通过 root.prev 建立另一个方向的引用关系,但这次采用 proxy(...) 修饰了 link 对象,其中 proxy 来自于 weakref 模块。

一旦对象被 weakref 模块修饰过,引用它将不会触发引用计数器的增长,这有效阻止了“环形引用”的产生,能让 GC 更及时地回收内存。

2. 传入 object() 作为默认值

同内置字典一样,OrderdedDict 也需要支持 pop 操作。pop 方法负责从字典中“弹出”一个键(key)所对应的值,如果 key 不存在,返回调用方法时传入的 default 默认值。

>>> d = {"a": 1}
>>> d.pop("a", 42)
1
>>> d.pop("c", 42)
42  # "c" 不存在,返回默认值 42

对于 OrderdedDict 而言,其在 pop 方法中,需要完成从自身字典中 pop 以及更新双向链表两件事。核心代码如下:

class OrderedDict(dict):

    __marker = object()

    def pop(self, key, default=__marker):
        marker = self.__marker
        result = dict.pop(self, key, marker)
        if result is not marker:
            # The same as in __delitem__().
            # 更新链表部分已省略 ...

你可以注意到,在 dict.pop(self, key, marker) 中,代码传入了 marker 作为 key 不存在时的默认值。marker 并不是什么魔法对象,它仅仅只是类初始化时创建的一个小 object()

为什么选择一个 object() 作为默认值?这是因为,此处需要通过 pop(...) 的返回值来严格区分“key 存在”和“key 不存在”两种情况。所以,一个绝不可能在用户字典中出现的新鲜热乎的 object() 对象,是最为理想的默认值选择。

设计服务端软件配置的 4 条建议

2022年5月13日 10:50

在设计和开发服务端(后端)软件时,配置文件是一个绕不开的话题。

配置文件是一种用于存放各类可配置项的特殊文件。每个软件都会预设一些默认配置,但这些默认值不可能适用于所有情况。因此,到了不同的环境中,我们常常需要用配置文件对其进行扩展和修改。

拿版本控制软件 git 举例。大部分用户的 home 目录(~)都存放着一份 .gitconfig 配置文件,里面写着自己的用户名和邮箱地址:

[user]
    name = piglei
    email = piglei2007@gmail.com

就像软件的任何一个主要功能一样,配置文件也会极大的影响软件的使用体验。良好的设计能让软件变得更易用,糟糕的设计则会带来许多意想不到的问题,将用户拒之门外。

在这篇文章中,我整理了 4 条关于“软件配置文件”的设计建议,希望能对你有所帮助。

1. 最好不给配置也能用

在网络世界里,每过一天,人们的耐心似乎就又比前一天减少了一丁点。一个制作精良的短视频,如果 3 秒钟之内无法抓住你,你的右手拇指就会条件反射般将它划走。

现在,假设你开发了一个非常有用的工具软件,并发布到了网上。软件的功能非常全面,所有人在使用它之前,需要编辑一份包含 20 项配置的配置文件。你猜,有多大比例的潜在用户会直接掉头走掉?

当我们想要一件东西时,总是一刻也不想等。因此,初始的配置过程麻烦与否,会强烈影响人们尝试软件的决心。在这方面,我认为最好的体验是:无需提供任何配置,便能直接使用软件 80% 以上的功能。

假如无法做到这一点,我们也应该试着从以下几个方面着手,尽量降低用户的配置成本,压缩从“开始安装”到“可使用”之间的等待时间。

1.1 预设合理的默认值

为了将必须由用户提供的配置项,压缩到最少。你得给软件的所有配置项预设一个足够合理的默认值。这些默认值应当能让尽可能多的用户满意。

举个例子,假如你的软件依赖一个可用的 Redis 服务。那么请将该项配置的默认值设为约定俗成的缺省地址: redis_url: redis://localhost:6379/0。这样,当用户的机器上刚好跑着一个 Redis 服务时,便可免去调整这个配置项的麻烦。

1.2 延迟部分配置项

软件复杂到一定程度后,可配置项也常常会多到令人咋舌。许多配置在开发者的眼里,常常都显得特别关键,无法通过预设默认值来简化。此时,想让用户少填一些配置,似乎成了不可能完成的任务。

面对这种情况,我们可以尝试把配置项拆分为两种类型:最少配置项其他配置项。用户在使用软件时,只需提供一份包含最少配置项的文件。剩下的配置,可延迟到使用软件时,通过友好的交互界面要求用户填写。

举个例子,你提供的软件包含一个 Web 站点。要使用它,用户只需提供 1 个配置项:MySQL 数据库地址即可。之后,他可以通过浏览器访问站点,逐步补完其他功能所需的剩余配置项。

2. 永远别默认“管理员密码”

在给配置项预设默认值时,有一点很容易做过头——将关键的安全类配置设置固定的默认值。举个例子,你的项目在第一次启动时,会创建一个具有最高权限的内置管理员角色。为了降低用户的配置成本,该角色的密码被硬编码了一个默认值:

# 重要:请在你的配置文件里修改该默认密码,否则将会带来严重
# 的安全风险。
SUPER_USER_PASSWORD = "proje2p#admin@321"

一旦系统设置了一个默认密码,那么 90% 以上的用户绝对不会去修改它——无论提醒文字说的多危险都没用。当软件传播开来后,这个固定密码会给许多黑客提供便利,带来灾难性的后果。

除了密码以外,任何秘钥类的配置也都不应该提供固定的默认值。你应该总是要求用户手动配置它们。或者,在软件第一次启动时,生成一个随机值也行。做法如下所示:

# 成功安装软件后,弹出提示文字:

恭喜,你已经成功安装了 nice_software。因为你没有在配置里提供
管理员密码,该值已经被设置为随机值:fuiwe2shdvwi23

在之后的流程里,你可以再次要求用户输入该随机密码进行验证,并提供一个新密码。这样也能有效降低安全风险。

除了密码和秘钥外,Web 服务启动时默认绑定的网卡设备,也很容易制造出安全漏洞。假如某软件在启动时,默认绑定所有的网卡接口,那么当用户在携带公网 IP 的机器上安装软件时,服务可能会在毫无准备的情况下被暴露给外网,风险极高。具体可参考 Redis 配置文件中的例子

3. 描述性文本格式优先

在一些使用了动态编程语言(比如 Python、Ruby、Lua 等)的项目中,常常会出现一种独特的配置文件格式:源代码文件

举个例子,假如你有一个项目,是用 Django 框架开发的,那么你在分发项目时,完全可以支持用户提供一个 Python 脚本来作为配置文件。

# 配置文件 my_settings.py 内容
CONTACT_USERNAME = 'contact_us'
CREATE_DEFAULT_ADMIN = True
ATTACHMENTS_DIR = '~/.attachments'

# 启动项目
$ start_project -s ./my_settings.py

第一眼看上去,这种做法非常不错。如果你四处转转,甚至能发现不少开源项目都在这么干——比如 Sentry 在使用 PythonGitLab 在使用 Ruby。在灵活性方面,源码和常见的描述性文本文件(YAML、JSON 等)有着天壤之别。采用源代码格式,意味着用户能通过编写代码,折腾出令人眼花缭乱的复杂配置。

但问题是,对于配置文件来说,灵活性(或者说“可编程性”) 并不是最重要的评价指标。要评价一份配置文件设计的好坏,是否“简单易懂”、是否“易于修改”,每个配置项是否有详尽的说明才是最重要的。而对比源代码和普通描述性文本文件,你会发现前者在这些维度上并不占优。

比如,不论你选了用哪门编程语言作为配置文件格式。当用户想使用你的软件时,就非得了解一些基本语法和数据结构不可。这很可能就会把一些不热衷于此的人拒之门外。

用源代码作为配置,在某些角度上还会鼓励用户把复杂的逻辑塞进配置文件里。有时这不算什么问题,但也有些时候——尤其是当用户需要同时管理许多份配置文件时,这些藏在配置里的复杂代码逻辑,会让维护变得尤其困难。

如上所述,在满足项目需求的前提下,你应该尽量选择简单的描述性文本格式作为配置文件——YAML、TOML、JSON 都行。它们虽然不如代码灵活,但胜在上手简单。

另外,即使同为文本格式,灵活性也各有差异。当你需要从多种格式中挑选一个时,选更简单、更灵活的那个就行。也就是说,如果 TOML 和 YAML 都可以,那就用 TOML 吧。

配置文件结构

在采用了文本格式作为配置文件后,一般项目的配置会形成类似下图这种三层结构:

说明:

  1. 最左侧的主配置模块:由项目开发者维护,负责提供默认配置,同时也包含绝大多数“智能”的配置生成与简化逻辑
  2. 中间部分是用户在使用软件时,需要提供的个性化配置文件
  3. 如环境异常复杂,用户亦可针对具体场景,开发工具来辅助管理和操作配置文件,降低维护成本

4. 部分或完全支持环境变量

The 12-factor App 是一个非常流行的应用架构建议。它推荐使用环境变量来作为应用配置来源,并认定这就是最好的配置管理模式——比任何文件格式都要好。

和配置文件比起来,环境变量确实有着一些天生的优势:比如门槛低、易于修改、操作系统无关性,等等。在 Kubernetes 等云原生平台上,使用环境变量配置服务尤其方便。

正因如此,除了支持从文件中读取配置项以外,你的软件最好也允许用户通过环境变量来修改配置项。

一个示例的配置文件 sample.TOML

contact_username = 'piglei'

同样的配置项,也可通过环境变量来设置:

export CONTACT_USERNAME="piglei"

要实现这个功能,你可以直接在项目中读取环境变量的值:

# config.py
import os

# 优先读取环境变量,而后是配置文件
contact_username = os.environ.get('CONTACT_USERNAME', toml_config['contact_username'])

如果你觉得手动读取环境变量太麻烦,每种编程语言一般都有各自流行的配置管理库,其中一些就支持同时管理文件和环境变量里的配置。比如 Python 的 dynaconf、Go 的 viper 模块,都非常好用。

总结

本文分享了一些设计服务端软件配置的经验之谈,我认为其中最重要的一点,就是认识到:用户总是希望立马用上软件,而不是先折腾半天配置。

希望我们能更积极地思考如何优化配置设计,为软件增添更多色彩。

用 Python 编程 13 年后,我把经验写成了 400 页的书

2022年3月2日 09:21

我写了一本 Python 语言的编程进阶书:《Python工匠:案例、技巧与工程实践》,本文是关于这本书背后的故事与一些致谢。

cover

我一直觉得编程某种意义上是一门“手艺”,因为优雅而高效的代码,就如同完美的工艺品一样让人赏心悦目。就像一位用锤子敲敲打打了十几年的铁匠,总结出一套“如何锻造出锋利的刀”的经验之谈——我在代码世界也“敲敲打打”了十几年,沉淀下来的便是这本《Python工匠》。

我将自己这些年掌握的技巧、经验与编程建议,去粗取精后悉数写入书中,希望它能帮读者朋友们完成从初学者到工匠的跃迁。

书目前已经正式上市,欢迎点击购买

本文主要内容来自书中的“前言”部分,略有删改。

结缘 Python

我初次接触 Python 是在 2008 年末。那时临近大学毕业,我凭着在学校里学到的一丁点儿 Java 知识四处求职。我从大学所在的城市南昌出发去了北京,借宿在一位朋友的出租屋里,他当时在巨鲸音乐网上班,用的主要编程语言正是 Python。

得知我正在寻找一份 Java 相关的工作,那位朋友跟我说:“写 Java 代码有啥意思啊?Python 比 Java 好玩多了,而且功能还特别强大,连 Google 都在用 !”

在他的热情“传道”下,我对 Python 语言产生了好奇心,于是找了一份当时最流行的开源教程 Dive into Python,开始学起 Python 来。

实话实说,之前在学校用 Java 和 C 语言编程时,我很少体会到编程的快乐,也从未期待过自己将来要以写代码为生。但神奇的是,在学了一些 Python 的基础知识,并用它写了几个小玩意儿以后,我突然意识到原来自己很喜欢编程,并开始期待找到一份以 Python 为主要编程语言的开发工作——也许这就是我和 Python 之间的缘分吧!

幸运的是,在当时的 CPyUG(中国 Python 用户组)邮件组里,正好有一家南昌的公司在招聘全职 Python 程序员。看到这个消息后,我立马做出了决定:结束短暂的“北漂”生活,回到学校准备该职位的面试。后来,我成功通过了面试,最终在那家公司谋得了一份 Python 开发的实习工作,并从此开启了后来十余年的 Python 编程生涯。

为什么写这本书

回顾自己的从业经历,我从中发现一件有意思的事:编程作为一项技能,或者说一门手艺, 给新手带来的“蜜月期”非常短暂。

一开始,我们对一门编程语言只是略懂些皮毛,只要能用它实现想要的功能,就会非常开心。假如再学会语言的一些高级用法,比如 Python 里的装饰器,把它应用在了项目代码里,我们便整天乐得合不拢嘴。

但欢乐的时光总是特别短暂,一些类似的遭遇似乎总会不可避免地降临到每个人头上。

在接手了几个被众人称为“坑”的老项目,或是亲手写了一些无人敢接手的代码后;在整日忙着修 bug,每写一个新功能就引入三个新 bug 后……夜深人静之时,坐在电脑前埋头苦干的我们总有那么一些瞬间会突然意识到:编程最初带给我们的快乐已悄然远去,写代码这件事现在变得有些痛苦。更有甚者,一想到项目里的烂代码,每天起床后最想干的一件事就是辞职。

造成上面这种困境的原因是多方面的,而其中最主要、最容易被我们直观感受到的问题就是:烂代码实在是太多了。 后来,在亲历了许多个令人不悦的项目之后,我才慢慢看清楚:即便两个人实现同一个功能,最终效果看上去也一模一样,但代码质量却可能有着云泥之别。

好代码就像好文章,语言精练、层次分明,让人读了还想读;而烂代码则像糊成一团的意大利面条,处处充斥着相似的逻辑,模块间的关系错综复杂,多看一眼都令人觉得眼睛会受伤。

在知道了“代码也分好坏”以后,我开始整日琢磨怎么才能把代码写得更好。我前前后后读过一些书——《代码大全》《重构》《设计模式》《代码整洁之道》——毫无疑问,它们都是领域内首屈一指的经典好书,我从中学到了许多知识,至今受益匪浅。

这些领域内的经典图书虽好,却有个问题:它们大多是针对 Java 这类静态类型语言所写的, 而 Python 这门动态类型的脚本语言又和 Java 大不一样。这些书里的许多理念和例子,假如直接套用在 Python 里,效果不尽如人意。

于是,话又说回来,要写出好的 Python 代码,究竟得掌握哪些知识呢?在我看来,问题的答案可分为两大部分。

  • 第一部分:语言无关的通用知识,比如变量的命名原则、写注释时的注意事项、写条件分支语句的技巧,等等。这部分知识放之四海而皆准,可以运用在各种编程语言上,不光是 Python。
  • 第二部分:与 Python 语言强相关的知识,比如自定义容器类型来改善代码、在恰当的时机抛出异常、活用生成器改善循环、用装饰器设计地道的 API,等等。

当然,上面这种回答显然过于简陋,略去了太多细节。

为了更好地回答“如何写出好的 Python 代码”这个问题,从 2016 年开始,我用业余时间写作了一系列相关的技术文章,起名为“Python 工匠”——正是这十几篇文章构成了本书的骨架。此外,本书注重故事、注重案例的写作风格也与“Python 工匠”系列一脉相承。

如果你也像我一样,曾被烂代码所困,终日寻求写好 Python 程序的方法,那么我郑重地将本书推荐给你。这是我多年的经验汇集,相信会给你一些启发。

推荐语

致谢

在我写作“Python 工匠”系列的过程中,许多媒体转发了我的文章,帮助提高了整个系列的影响力。它们是“腾讯技术工程”知乎专栏董伟明(@dongweiming)的 Python 年度榜单, 以及以下微信公众号:“蓝鲸”“Python 猫”“Python 编程时光”“Python 开发者”“腾讯 NEXT学院”等。由于名单过长,如果你的媒体也曾转发过“Python 工匠”系列,但没有出现在上面的列表中,还请见谅。

感谢我的前同事与朋友们。当我在朋友圈转发“Python 工匠”系列文章时,他们总是毫不吝惜地给予我赞美与鼓励。虽然受之有愧,但我的确深受鼓舞。

特别感谢我在腾讯蓝鲸团队的所有同事与领导,他们在我写作“Python 工匠”系列的过程中, 提供了许多积极反馈,并且不遗余力地转发文章。这些善意的举动,为本书漫长而充满磨炼的写作过程,注入了强大的驱动力。

感谢参与审阅本书初稿的所有人。他们中有些是我相识多年的同事与朋友,更多则是我从未谋面的“网友”。因慕名各位在开源世界的贡献,我邀请他们审阅本书内容,无一例外, 所有人都爽快地答应了我的请求,并围绕本书的内容和结构提出了许多精准的修改意见和建议。他们是赖信涛(@laixintao)李者璈(@Zheaoli)林志衡(@onlyice)王川(@fantix)laike9m冯世鹏(@fengsp)伊洪(@yihong0618)明希(@frostming)李卫辉(@liwh)

尾声

《Python 工匠》是我的第一本书,因此,它对我而言意义重大。虽然在写作过程中,我已经竭尽所能地让它容易阅读、容易理解,并努力做到只将那些“最有用”的知识置入其中。但我也深知自身能力有限,无法处处做到完美。假如你在阅读本书时,对内容有所疑问或发现错漏,欢迎通过邮件与我交流(我的邮箱是:piglei2007@gmail.com)。

人们在讨论 Python 时,常常评价它是一门实用的编程语言。在我看来,“实用”这个评价其实相当高。所以,希望在几个月后,大家在谈到 Python 相关的书籍时,也能有一两个声音说:“《Python 工匠》是一本实用的编程书。”那样对我而言便是最好的鼓励了。


直接购买:

购买《Python 工匠》(京东)

下载样章:

访问《Python 工匠》的图灵社区主页,可下载样章 PDF 文件试读。

久等了,我的新书《Python工匠》开始预售

2022年2月15日 07:13

cover

引子

2008 年末,北京奥运会刚刚结束后不久。我——一名计算机专业的大四学生,因为一位朋友的极力推荐,开始试着把 Python 作为自己的第一编程语言。大学毕业后,我顺利成为了一名 Python 程序员,先后在搜狐和赶集网工作过,后来加入了深圳腾讯,目前担任蓝鲸工具 PaaS 平台的负责人。

在这十几年的职业生涯里,我参与过许多项目的后端开发与架构设计工作。虽然每个项目的复杂程度、活跃用户量各不相同,但它们大都使用 Python 语言编写。

在我看来,Python 是一门“易于上手,难于精通”的编程语言——虽然许多人都能用 Python 编写出可运行的程序,但只有少数人,掌握了写出高质量 Python 代码的诀窍。

而在现实世界中,代码质量常常会决定项目的成败。我见过许多被寄予厚望的大项目,前期光彩夺目,后期却躺在烂代码堆上逐渐腐败,缓慢走向死亡。我也参与过许多老项目,亲手偿还过代码质量方面的“技术债”,助它们重获生机。

“Python 工匠”的由来

慢慢地,我积累的 Python 编程经验越来越多,于是有了一份想把它们分享出去的冲动。后来,实在压抑不住那份冲动,便试着把这些经验落笔成了文章,日积月累,开源的技术文章系列:“Python 工匠” 就此诞生。

2020 年中,我开始思考如何把“Python 工匠”系列文章,升级成一本以“Python 进阶”为主题的编程书。虽然在当时的市面上,Python 进阶图书可谓汗牛充栋,又有《流畅的 Python》、《Effective Python》等珠玉在前,似乎没有再多一本“中文 Python 进阶书”的必要。

但也许因为自己是湖南人,骨子里有股“蛮”劲吧——总觉得自己写的东西独一份,能给读者一些不一样的体验。下定决心后,开始闭门写作,一年后终于成稿,之后便看着书稿在传统出版流程中缓慢流动。

今天,我很高兴地告诉大家,图书《Python 工匠:案例、技巧与工程实践》已经走完了出版前的所有准备工作,马上就要上市了。你在京东上就可预订本书,现在下单还可享受 8 折优惠。

图书简介

《Python 工匠:案例、技巧与工程实践》是一本编程进阶图书,旨在帮你掌握如何用 Python 写出结构清晰、易于维护的好代码。但与传统进阶书不同,它不是一本大而全的“语言高级特性参考手册”,而更像是一份“答卷”,一份我针对问题 “如何写好 Python 代码?” 交出的答卷。

答卷中的所有知识点,大体可分为两大部分:

  • 第一部分:语言无关的通用知识,比如变量的命名原则、写注释时的注意事项、写条件分支语句的技巧,等等。这部分知识放之四海而皆准,可以运用在各种编程语言上,不光是 Python。
  • 第二部分:与 Python 语言强相关的知识,比如自定义容器类型来改善代码、在恰当的时机抛出异常、活用生成器改善循环、用装饰器设计地道的 API,等等。

在表达形式方面,就像副标题“案例、技巧与工程实践”所说,全书的知识点会通过好几种不同的形式展现。你会读到一些深入语言核心的概念释疑,也会读到一些贴近工程实践的编程建议,在某些章节,你甚至还会发现一些像电子游戏一样有趣的案例故事。希望它们能为你带来些许不一样的感受。

当然,要了解一本书,看再多介绍文字,都不如读一段书中的内容来得真切。你可以访问《Python 工匠》的图灵社区主页,在“随书下载”部分找到《Python 工匠》的样章 PDF 文件。不妨先读读看。

“这本书适合我吗?”

在我看来,《Python 工匠》最适合以下人群阅读:

  • 以 Python 为主要开发语言的工程师
  • 工作中需要写一些 Python 代码的工程师
  • 有其他语言编程经验、想学习如何写出高效 Python 代码的工程师
  • 任何爱好编程、喜欢 Python 语言的读者

全书内容以进阶知识为主,里面虽有少量基础知识讲解,但并不全面,描述得也并不详尽。正因如此,假如你从未有过任何编程经验,我并不建议你通过本书来入门 Python。

在 Python 入门学习方面,我推荐由人民邮电出版社图灵公司出版的《Python 编程:从入门到实践》。当你对 Python 有了一些了解、打好基础后,再回过头来阅读本书,相信彼时你可以获得更好的阅读体验。


好了,关于《Python 工匠》的预售信息先介绍到这。感谢所有朋友一路以来对“Python 工匠”的支持,期待你拿到书后,第一时间与我分享你的阅读体验。再会!

技术写作二三事:原创情结

2021年12月4日 12:18

    2019 年的春节假期,我闲在家中,准备一次性刷完攒了大半年的“Python Weekly” 周报。在 6 月份的一封周报里,有一篇名为 “Python Idioms: Multiline Strings” 的文章,内容很精炼。前半部分先描述痛点:在 Python 中写多行字符串字面量,随后演示如何用 textwrap.dedent 函数来优化它。

    部分内容摘录如下:

    ... The problem is that it’s just ugly, because indenting the strings actually inserts the indentation into the string. So you have to do this:

    def create_snippet():
        code_snippet = """\
    int main(int argc, char* argv[]) {
        return 0;
    }"""
    

    With dedent, we can indent the whole multiline string according to the current scope, so that it looks like a Pythonic code block...

    巧合的是,我两个月前刚在博客上发布了“Python 工匠”的第 3 篇文章:“使用数字与字符串的技巧”。在这篇文章中,同样出现了 textwrap.dedent 函数。

    我写的内容:

    日常编码时,还有一种比较麻烦的情况。就是需要在已经有缩进层级的代码里,插入多行字符串字面量。因为多行字符串不能包含当前的缩进空格,所以,我们需要把代码写成这样:

    def main():
        if user.is_active:
            message = """Welcome, today's movie list:
    - Jaw (1975)
    - The Shining (1980)
    - Saw (2004)"""
    

    但是,这样写会破坏整段代码的缩进视觉效果,显得非常突兀。你可以用标准库 textwrap 里的 dedent 函数来解决这个问题...

    看出来了吗?我的写作思路和代码样例,与前面那篇文章如出一辙。

    现在听上去或许有点可笑,但当时我的心情相当沮丧。为什么?因为我那时是个“原创情结”特别强的人。每次写作前,我一定要确认自己没看过类似的内容,才会下笔。也就是说,假使我在写作前一天读过 “Python Idioms: Multiline Strings”,那么 textwrap.dedent 十有八九不会出现在我的文章里。

    我说不清这种拧巴的“原创情结”从何而来。似乎从十几年前,我的博客上线后的第一天起,我就已经这样了——如果有一个技术点,别人已经写的足够好了,那么我打死都不会再写一遍。但结果你们已经知道了——懂 textwrap.dedent 技巧的人远不止我一个。所谓“写出原创内容”带给我的那点虚荣感,就像小朋友吹出的肥皂泡一样,一触即破。

    表面上看来,“原创情结”对作者有好处。它让后者对自己要求更高,不至于心甘情愿沦为知识的搬运工(或更恶劣一点:沦为抄袭者)。

    但实际上,对于写作者(尤其是技术写作者)来说,“原创情结”带来的坏处远大于那一丁点好处。因为对“内容是否原创”的过度痴迷,会彻头彻尾地消灭一个人的创作冲动。“原创情结”像是一条无形的锁链,我们被它牢牢捆在“一举成名天下知”的幻梦中,无法挣脱。

    同其他领域相比,在技术写作领域追求原创性尤其困难。在 StackOverflow 上,每天都有上百万的技术大佬,慷慨分享着自己的技巧和经验。 在各种独立博客和云厂商(点名 DigitalOcean)的知识库里,每天都有无数高质量的技术文章新鲜出炉,足以让人眼花缭乱。在这样的创作密度下,要找到一片没有其他人开垦过的处女地,难度可想而知。

    在行业摸爬滚打了十几年后,我如今只有一个感触:装在自己脑子里的技巧、概念和知识,几乎没有哪一样可称得上是原创。 有时候,我以为自己写了一些颇具原创性的观点。结果过几天听英文播客时,居然从访谈嘉宾那听到了一模一样的内容——翻译成中文后,与我的原文几乎只字不差。

    假如我们诚心认为自己掌握的某项知识是原创,那或许只是因为读的还不够多、不够广、不够深。除此之外,还有一种可能,那就是我们忘记了知识来自何处。

    拿文章开头的 textwrap.dedent 来说。如果仔细回忆,我也许会想起在某个工作日的午后,我随意打开了某个开源库的代码,发现了这个小技巧,并记住了它。光阴荏苒,经过了足够长的时间后,我逐渐忘记了那段经历——这个小技巧被我据为己有,放进了大脑里的“原创知识”那一栏。

    所以,技术文章的原创性,大多数时候只是作者们的幻觉。现在的我在写东西前,不会再考虑任何“内容是否足够原创”之类的问题。我唯一遵循的原则,就是绝不有意偷窃其他人的写作成果。

    许多时候,我们想动笔写点技术文章,即不是为了硕士毕业证,也不是为了申请国家专利,我们只是想记录与分享一些朴素的经验而已。既然如此,要那么高的“原创度”顶个球用?

    如果你常年琢磨着写一些技术文章,却被“原创情结”所束缚,无法下笔。每年一到年底,打开自己的博客一看,总是空空如也。我可以尝试给你一些建议:

    • 在某种意义上,任何创作都是有价值的。因为价值是由读者定义的,和作者从哪儿(原创还是抄袭?)得到的创作灵感无关
    • 你不重要,你写的东西也没有那么重要。没人期待在你的文章里,学到任何独家秘笈,别有任何心理负担
    • 假如从内容上无法创新,也可以在形式上做一些尝试。有些文章的内容光芒璀璨,却常因原作者糟糕的写作形式和文字技巧被埋没
    • 假如遇到一个你感兴趣的主题,其他人已经写过了,但你觉得你能写的比他更好,何不试一试?

    上面这些话,有时我也会用来宽慰自己。

    所以,别再纠结于原创与否,打开电脑,随便写点啥吧。“某个诡异的线上 Bug”;“Python 的 5 个面向对象小技巧”;“详解 Kubernetes 的某个配置项” ,所有主题都是好主题。不必想着写出什么惊世佳作,坚持写,我们总会慢慢进步。

    也许某一天,你在写了许多东西以后,抬头一望,会看到一片瑰丽的新大陆,正向你徐徐展露。

    再见,面包旅行!

    2025年10月17日 12:00

    2012 年初,北漂的我抱着满心憧憬加入了一家创业公司,和另一位同事一起成了公司唯二的两名后端程序员。这家公司当时的产品是个名叫“遨游记”的移动 App,后来它变成了现在的 面包旅行

    在面包旅行的几年,我遇到了许多优秀又有趣的同事,写了一打又一打的代码,把一头黑发写的花白(不夸张,后来又黑回来了)。我和许多同事一起,见证了“面包旅行 Appstore 首发”、“安卓版首发”、“苹果年度推荐”等诸多高光时刻,亲身体会到了做为一名程序员最大的幸福——看到自己的代码服务了百万级的用户,为无数人的旅途带来了些许不同的体验。

    后来因为家庭原因,我选择在两年半以后离开了面包旅行。虽然我之后的工作和在线旅游行业再无半点关系,但在三里屯 SOHO 工作的那三十个月,永远是我职业生涯里最为有趣的一段经历之一。

    此时此刻,我猜在面包旅行的服务器上,应该仍然运行着我曾写过的一些代码——“POI 与航班查询”、“游记推荐管理平台”、“用户积分系统”,等等。但我想等到半年以后,等到面包旅行真正关站的那一刻,我曾写过的哪些函数和类便会迎来真正的死亡,同无数其他代码一起消失在互联网的滚滚长河之中。

    有些事情,你明知它迟早会来,但等它真的发生以后,你还是难免会感到突然,觉得难过。对我来说,面包旅行不止是一个游记 App,在它上面,凝聚着太多我和许多同事曾有过的灵感、快乐与骄傲。

    看到关站公告,心中纵有万般不舍,其实也清楚,这个小小的游记 App 能在残酷的移动互联网时代存活下来,一直运营到今日已实属不易。

    最后,让我用自己的方式和面包旅行再道个别。

    from __future__ import print_function
    
    print("Hello, breadtrip!")
    print("Bye, breadtrip!")
    

    人人都能写英文博客

    2024年7月5日 06:56

    时间过得很快,转眼间,2024 年的进度条已经走到了 50% 的位置。作为一名博主,我很惭愧 🥹,过去半年我只写了一篇新文章,算是相当低产。不过,虽然没写太多新文章,但我干了另一件值得记录的大事。

    在今年 2 月份,我给博客增加了“英文”板块,并在其中发表了 4 篇英文文章,几乎每一篇都获得了不错的反响:

    1. "After 14 years in the industry, I still find programming difficult"Hacker News 217 points(Top 20) 评论数 190+,reddit r/programming 773 votes(Top 1) 评论数 310+,被翻译成俄语、韩语
    2. "6 ways to improve the architecture of your Python project (using import-linter)":登上 PyCoder's Weekly 周刊,被播客节目 Python Bytes 推荐,被 Real Python 官方账号推荐
    3. "3 important things I overlooked during code reviews"reddit p/programming 90 votes(Top 5),评论数 16,Lobster 21 votes 评论数 46

    图:piglei.com 登上 Reddit /p/programming TOP1,并在上面停留了整整一天

    简单来说,在阅读量和读者反馈方面,这些英文文章成绩斐然。并且在某些维度(比如评论数量)上的数据表现,远远超我所写过的任何一篇中文文章。

    不过,也许现在屏幕前的你已经皱起了眉头,想说:“行了,行了,piglei 你这个货别显摆了,现在我知道你英文很牛逼了,能写出流利的英文文章来,满意了吧!”

    先别急着下结论,其实我的英文能力非常普通(大学六级考两次的水平)。因此,这些文章其实也并非由我从零开始写就,或许眼尖的你已经发现,它们都是由我写过的中文文章翻译而来。

    我借助了 GPT 4 和 DeepL Write 等先进工具完成了翻译,并尽全力保证译文“信雅达”,让它们读起来就像是出自一位熟练的英文写作者之手(此处有吹牛成分)。

    图:一位读者表示“根本看不出来文章是由中文翻译而来”,incredible!

    我使用的翻译方式没有什么门槛,任何一位中文写作者,都能用它来创作属于自己的英文文章。我将在本文教会你具体的方法。

    但在进入正题前,让我们先把时钟往回拨一拨,回到今年的二月份,看看究竟是什么事情,在我心里埋下了想开始“英文写作”的种子。

    开始“英文写作”的契机

    二月份的某个周五,在用谷歌搜索资料时,我无意读到一篇关于 ChatGPT 的英文文章。不读不要紧,一读吓一跳,这篇文章的内容,根我在 2022 年写的一篇中文文章《ChatGPT 正在杀死编程里的乐趣》一模一样。

    然后,我顺藤摸瓜点进作者的主页,发现了更多源自我的博客 piglei.com 的文章,比如《Python 工匠》系列,等等。

    图:名为 bo leo 用英文大量洗稿了我的博客文章

    这些文章没有注明原始出处,作者 bo leo 也从未联系过我征求授权,明显侵犯了我身为原创作者的权益。于是,我在 Medium 平台上举报了这些文章,几个小时后,它们就被下线了(也可能是被作者主动删除,因为事情在推特上被曝光)。

    这件事看似得到了圆满的解决,但我的心情却无法平静。因为在点开几篇翻译质量粗糙的“英文盗版文章”后,我在评论区发现了不少高质量的读者评论,部分观点极具启发性——若是它们出现在自己博客的正版文章的评论区,那该多好啊!

    图:《ChatGPT 正在杀死编程里的乐趣》英文盗版,右边的读者评论写得很棒

    既然那么糟糕的英文版都能获得读者认可,假如翻译质量再好一点呢?再进一步,为什么要给这些偷偷洗稿的卑鄙小人可乘之机,为什么我不干脆自己动手,直接为每一篇自己的得意之作发布对应的“官方英文版”呢?

    就这样,在一个月朗星稀的周五晚上,piglei 坐在笔记本电脑前,下决心开始自己的“英文写作”之路——当然,如果你非得要较真的话,他走上的并非真正意义上的“写作”之路,而是一条名为“LLM 英文翻译 + 工具润色”的捷径 😅。

    用 LLM 翻译初稿

    “机翻(机器翻译)”,在很长一段时间里都是“低质量翻译”的代名词。然而在 ChatGPT 3.5 等大语言模型横空出世后,“机翻”的质量跃升到了一个前所未有的高度。不论是在准确度、流畅度和专业名词翻译方面,优秀的大语言模型所生成的译文,几乎接近普通人类的翻译水准。

    因此,我选择用 LLM 来完成译文的初稿。在模型方面,我用到了 OpenAI 的 GPT-4(通过 API 调用),你也可以用其他模型来替代。

    为了尽量提升译文的质量,我编写了以下提示语(prompt)来帮助 GPT-4 更好地完成翻译:

    You are a professional English translator. I'll send you Chinese content, please translate it into American English.
    
    Requirements:
    
    - Correct any grammatical errors in the original content before translating it.
    - The English version should use a concise, direct and clear writing style.
    - The content may use markdown format, please keep the format as it is.
    
    Respond only with the translated content.
    

    之后的操作流程比较简单:把中文发给大模型,它就会吐给你一份英文。一篇文章的篇幅通常很长,你需要将其拆分为多个段落,分段完成翻译。

    一些注意事项:

    • 在翻译每个新的段落前,记得清空当前聊天的上下文,否则会被多收很多钱(按 token 计费时)
    • 不同段落的译文,对同一个专业名词的翻译需保持一致
    • 如果原文是 Markdown 格式,注意让译文维持原格式
    • 如果原文中的书名或引用段落的原始语言就是英文,那就不要用 LLM 的译文,找到被引用内容的原始英文表达

    图:使用 GPT-4 翻译一个段落

    这样重复执行多次“复制 -> 聊天 -> 粘贴”后,一篇由 LLM 完成的初稿便落到了我们的手中。

    一眼看上去,初稿的翻译质量似乎已然十分出色(前提是使用的模型能力足够强大)。但是如果细读,还是会发现文本中藏着许多小瑕疵,值得进一步优化。因此,我建议继续对初稿实施二次校对和润色。

    用 DeepL Write 润色

    假如你的英文水平非常过硬,那么你可以直接自己动手来完成润色。但是,对于我这种英文半吊子来说,借助工具是更合适的选择。工具方面,我挑选了知名翻译网站 DeepL 出品的写作助手:DeepL Write

    DeepL Write 用起来很简单,只要把原文粘贴到左侧文本框,选择语言为 English,右侧便马上会出现优化过的版本。

    图:DeepL Write 界面截图,右侧带下划线的文字是建议优化的内容

    但是请注意,虽然 DeepL Write 工具会提供一些优化建议,但它们就像 LLM 的翻译一样——并非 100% 可靠。

    所以,作为唯一的人类创作者,我们仍需亲自决定每一个词语、每一种句式。 也正是因为如此,在润色阶段,拥有优秀的英文语感非常重要,因为你要凭借这份语感,来判断哪种写法会给读者提供更优的阅读体验。

    在培养语感方面,我认为长期阅读高质量英文文章很有帮助。

    推广你的文章

    万事俱备,只欠东风。有了英文文章后,下一步便是给它找到最匹配的读者群。幸运的是,在英文世界中推广自己的文章,比在中文世界要方便太多,大多数时候,你只需要轻点小手,把文章的 URL 提交到心仪的资讯站点即可。

    目前,我尝试过以下几种渠道:

    1. Hacker News:最知名的科技类资讯站点,流量巨大
    2. Reddit r/programming:知名的编程相关资讯节点,流量很大
    3. Lobster:流行的科技资讯站点,与 HN 风格类似,流量较大,技术讨论氛围非常好
    4. PyCoder's Weekly:知名 Python 编程语言周刊,订阅量巨大

    除了以上渠道以外,你也可尝试一些契合文章调性的其他渠道,打个比方,一篇 Go 语言的技术文章,就很适合提交到 Reddit 的 /r/golang 节点上。

    ⚠️ 注意:虽说积极推广自己的文章不是什么坏事,但也请不要滥用。每次投稿前,请确保内容质量达到标准,并契合对应渠道的读者群。否则会讨人嫌哦!

    ❤️ 我喜欢这种创作模式

    如你所见,我使用的“英文写作(翻译)”方式非常非常非常简单,似乎稍微有点脑子的人就能想到。但其实,在实际上手用 GPT-4 + DeepL 完成第一篇文章前,我的心情极度忐忑。我能听到心中有个小人不停小声念叨:“机器翻译的文章,真的能让英文读者满意吗?”

    待到第一篇文章发布,收获了大量的正反馈后,我才敢真正确认,这确实是一条相当可行的创作模式。

    我个人非常喜欢这种模式。因为在这之前,我从未想过自己能用第二语言,写出被数万人喜爱的技术文章。 即便这算不上“一字一句”完成的那种真正的写作,但当你读到最终的英文成品时,会发现无论从语言、节奏还是腔调上,它都同自己的创作灵魂契合得天衣无缝,让你满心欢喜。

    结语

    我花了整整三十年,才学会如何用自己的母语写出文通字顺的文章。假如,我从现在开始学习完全用英文写作,不知还得练习多少年,才能勉强达到及格线。但如今借助 LLM 等现代化工具,我轻松实现了自己的“英文写作梦”。

    记得发布完第一篇英文文章后,深圳已经进入深夜,但因为时差原因,文章在 Reddit 和 Hacker News 的热度却在一路走高。看着 vote 数和评论数不断增长,我兴奋得完全无法入睡,几乎每隔三十分钟就要抓起手机,看一遍最新的访问数据。

    后来几经辗转,终于进入了梦乡。我已经忘了那晚梦见了什么,但我能想起的是,第二天早晨醒来后,我感受到了一种就像是刚刚学会写字时的喜悦。

    入行 14 年,我还是觉得编程很难

    2023年2月18日 19:07

    很多年前,当我还是一名计算机专业的大四学生时,整天上网浏览各类招聘信息,想找到一个合适的程序员实习岗位。

    除了实习岗位外,我偶尔也会点进一些“高级工程师”的招聘帖里。现在回想起那些帖子,抛开让人眼花缭乱的技术名词,我印象最深的就是常出现在第一行的岗位年限要求:“本职位要求 工作经验 5 年以上”。

    作为一只一天班都没上过的小菜鸟,这些年限要求在我眼里简直长到夸张。不过,望洋兴叹之余,我有时也会在心中暗暗憧憬一下:“五年工作经验的程序员,那该多厉害啊?写代码对于他们来说,是不是像吃饭一样简单?”

    时光荏苒,一晃十几年过去了。如今回头一望,自己也成了一名有着 14 年工作经验的光荣打工人。在软件开发行业摸爬滚打这些年后,我发现很多事情,与我在大四时所想象的大不相同,比方说:

    • 随着经验增长,编程并不会变简单太多,“像吃饭一样简单”只出现在梦里
    • 给许多“大项目”写代码不光没意思,还很危险,远不如在 LeetCode 上做一道算法题有趣
    • 只从技术角度思考问题,成不了好程序员,有些东西远比技术更重要

    细想起来,这类关于编程的感触还有许多。我整理了其中 8 条,写成了这篇文章。如果其中某些观点引起了你的共鸣,我会非常高兴。

    1. 写代码很简单,但写好代码很难

    编程曾经是一项门槛很高的专业技能。从前,一个普通人想学编程,最常见的做法就是通过教材和书本学习。不过大部分编程专业书,十分艰深晦涩,对于初学者来说很不友好。因此不少人在尝到编程的乐趣前,就早早地半途而废。

    但如今,学编程正在变得越来越容易。学习不再像以前那样,只能硬啃书本,而是多了许多新途径。观看教学视频、参加 Codecademy 的交互式课程,甚至直接在 CodeCombat 通过玩游戏来学编程,每个人都能找到适合自己的学习方式。

    “妈,我真没在玩游戏,我在学编程呢!你看屏幕右边!”

    此外,编程语言也在变得越来越易用。经典的 C 和 Java 不再是大多数初学者的首选,许多更简单、更易上手的动态类型语言如今大受欢迎,与之相关的 IDE 等工具也变得越来越完善。这些因素进一步降低了编程的学习门槛。

    总而言之,编程早已褪去了它的神秘面纱,从只有少数人才能掌握的神秘技能,变成了一门人人皆可学习的普通手艺。

    但更低的学习门槛,更友好的编程语言,并不意味着人人都能写出一手好代码。如果你已经工作,参与过一些项目,那我很想问你一个问题: ”你日常接触的这些项目的代码质量如何?是好代码多,还是烂代码多?”

    不知你会怎么回答,我先来说说我的答案。

    好代码还是很少

    2010 年,我跳槽到了一家总部位于北京五道口的大型互联网公司。

    加入这家公司前,我只在十人规模的小公司待过,因此,我对新公司在各方面都有着很高的期待,尤其是软件质量方面。当时,我心里想的大概是这样:“这可是支撑了有着千万用户量的产品的‘大’项目,代码质量跟之前那些比,肯定有质的飞跃吧!”

    等到在新公司工作了一周后,我才发现自己实在是错得离谱。所谓“大”项目的代码质量同我的预期相去甚远。打开 IDE,数百行的函数和神秘的数字字面量比比皆是,开发任何一个小需求都难如登天。

    后来,在待过更多公司,接触了更多软件项目后,我总结出一个道理:不论公司多大、项目多牛,在实际工作中遇见好代码,仍然是小概率事件。

    好代码有哪些要素?

    话说回来,到底怎样的代码才算是好代码?在这方面,Martin Fowler 有一句话常被大家引用:

    “Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”

    “任何傻瓜都能写出计算机能理解的代码。优秀程序员写人类能理解的代码。”

    我认为它可以作为评价好代码的原点:好代码一定是可读、易读,且容易理解的。写出好代码的第一原则,就是把人类读者放在第一位。

    除了可读性以外,评价代码好坏还有许多其他维度:

    • 贴合编程语言:是否使用了当前编程语言的推荐写法?语言特性和语法糖,使用程度是否恰到好处?
    • 易于修改:代码设计是否考虑了未来的需求变更,当变化发生时,代码是否容易随之修改?
    • API 设计合理:API 设计是否合理,易于使用?好的 API 在简单场景下使用方便,在高级场景下又可以随需求扩展。
    • 性能够用:代码性能是否满足当前业务需求,同时为未来保留了一定提升空间?
    • 避免过度设计:代码是否存在过度设计、过早优化的毛病?

    总而言之,对于任何层级的程序员来说,好代码都不是什么唾手可得的东西。要写出好代码,需要在许多维度上反复权衡、精心设计,最后再加以持续打磨。

    既然如此,假如想尽快掌握写代码这门手艺,有捷径吗?

    写好代码的捷径

    在许多层面上,我认为编程和写作非常相似。二者都是使用文本和符号来表达思想,只是方式略有不同。

    谈到写作,我想问一个关于作家的问题:“你听说过不读书的作家吗?你有没有听到过某位作家说,他从来不读其他人的作品,只读自己的东西?”。我猜答案应该是否定的吧。

    如果你去查阅相关资料,你会发现许多职业作家的日常生活,就是阅读和写作两件事在不断循环。他们每天会花大量时间阅读各类文字,然后再写作。

    同样是“文字工作者”,程序员们就很少重视阅读。但要想快速提升编程能力,阅读正是不可或缺的重要一环。除了日常工作接触到的项目以外,我们应该更多地阅读那些经典软件项目,从中学习 API 设计、模块架构和代码编写的技巧。

    不光代码和技术文档,最好再定期读一些计算机方面的专业书,保持阅读书籍的习惯。在这方面,我认为 Jeff Atwood 在 15 年前写的文章 "Programmers Don't Read Books -- But You Should(都说程序员不读书——但你应该读)",如今读来仍不过时。

    提升编程能力的捷径,就藏在“阅读 <-> 编程”这个无尽循环里。

    “一个好的程序员应该做什么?”

    2. 编程的精髓是“创造”

    在程序员的日常工作中,有很多事情会让人充满成就感,甚至情不自禁地感叹“编程真美好”。比方说,修复了一个极难定位的 Bug,用新算法将代码性能提升了一倍,等等。但在所有的这类事情当中,没有任何一件,能和“亲手创造出一件东西”相比。

    当你在编程时,创造新事物的机会实际上随处可见。因为并非只有发布一个新软件,才称得上是“创造”。写一个可复用的工具函数、设计一套清晰的数据模型,全都可以归入“创造”的范畴。

    身为程序员,保持对“创造”的热情至关重要。因为它可以帮我们:

    • 更高效地学习:学习一门新技术,最高效的方式就是用它开发一个真实项目,在创造的过程中学习,效果最好。
    • 有机会邂逅了不起的东西: 许多改变世界的开源软件,最初都是作者纯粹出于兴趣所创造,比如 Linus Torvalds 和 Linux,Guido van Rossum 和 Python。
    1989 年的圣诞假期,荷兰人 Guido van Rossum 敲下了 Python 语言的最初几行代码,Python 最初仅被期望作为 ABC 语言的继承者,但后来“吞噬”了全世界

    虽然“创造”好处多多,程序员们也有大把机会去做,但许多人常常缺少一种身为“创造者”的觉悟。就像那个广为流传的小故事所说:一位哲学家询问正在砌砖的工人,有人清楚地知道自己是在建造一座大教堂,有人却认为自己只是在砌砖。很多程序员正是“只见砖块,不见教堂”。

    将自己定位成创造者后,看待事物的方式就会发生天翻地覆的变化。举个例子,同样是给 API 增加报错提示文字,创造者们就能跳出“快速完成需求就好”的思维陷阱,向前一步,追问自己一些更重要的问题:“我想为用户创造什么样的产品体验?怎样的报错文字,更能帮助我达成该目标?”

    就像任何一个有用的编程模式一样,“创造者思维”也能成为你的职业生涯的一道巨大推进力。因此,现在就试着问自己一个问题吧——“我的下一份创造会是什么?”

    3. 打造高效试错的环境至关重要

    我曾参与开发过一个互联网产品,它设计精美,功能丰富,每天都有大量用户使用。

    但就是这么一个从市场角度看颇为成功的产品,工程质量却非常糟糕。如果你打开它的后端项目,把所有目录翻个底朝天,都找不到任何一行单元测试代码,其他自动化测试流程也是无从谈起。而业务逻辑偏偏又十分复杂,最后,项目代码间的意料耦合多如牛毛,开发一个新特性,很容易把旧功能给搞挂。

    “在忙啥呢?” “试着修复我之前修一个问题时搞出来的问题,那问题是我之前解决另一个问题搞出来的,而那个问题又是我……”

    因此,项目每次发布时,开发和产品同学全都得严阵以待,氛围十分紧张。整个发布过程也很刺激,紧急回滚时有发生。一个人在这样的环境中工作,技术成长抛开不谈,心理素质肯定能得到极大锻炼。

    编程原本是一件充满乐趣的工作,但为这样的项目编程,乐趣根本无从谈起。究竟是什么夺走了编程的乐趣?

    理想的编程体验≈“刷题”

    LeetCode 是一个著名的编程学习网站,上面提供了许多覆盖各个难度的编程题,大部分与算法相关。用户可以选择自己感兴趣的题目,直接在浏览器上编写代码(支持十几种编程语言)并执行。如果通过了全部的测试用例,则算作解答成功。

    在 LeetCode 上做题

    在 LeetCode 刷题很像在玩游戏,富有挑战性,同时也很有趣。整个做题过程,实际完美展现了一种理想化的编程体验:

    • 关注点分离:每道题目都是一个独立个体,同一时间内,开发者可以完全沉浸在一道题目中;
    • 快速获得精准反馈:开发者每次调整代码后,能通过自动化测试快速获得结果反馈;
    • 零成本试错:写出的代码语法有错误、逻辑有问题,没有任何不良后果,心理负担小。

    不过,屏幕前的你很可能觉得我在说些废话。

    “不然呢?解算法题、写小脚本,不就是这样的体验吗?有啥特别值得说的?”你很可能会继续补充道,“你知道我们公司的项目有多复杂吗?规模超大,模块巨多,你懂我意思吗?每天服务 ××× 万人,光数据库就好几套,消息队列都有三种,开发起来当然要麻烦一点咯!”

    确实,全世界的软件千差万别,开发起来不可能都像在 LeetCode 上刷题一样轻松愉快。但这并不意味着,我们不应该努力改善自己身处的编程环境,哪怕只有一点点。

    要通过改善环境来提升编程体验,可用的理念和工具包括:

    • 模块化思想:妥善设计项目中的每一个模块,降低耦合,提升正交性
    • 设计原则:微观层面上,应用那些经典的设计原则和模式,比如“SOLID”原则
    • 自动化测试:编写规范的单元测试,必要时使用 Mock 技术,用自动化测试覆盖业务关键路径
    • 缩短反馈回路:切换编译速度更快的工具,优化单测性能,竭尽全力缩短从“改完代码”到“获得反馈”的等待时间
    • 微服务架构:必要时,将大单体拆分为多个职责各异的微服务,分散复杂度
    • ……

    关注编程环境,刻意创造出允许高效试错的“代码乐园”,让工作像刷题一样轻松愉快。是经验丰富的程序员能为自身团队做出的最好贡献之一。

    4. 避开代码完美主义陷阱

    在代码质量上精益求精是好事,但也要注意别掉进完美主义的陷阱。因为编程不是艺术创作,不鼓励人们无限度地追求极致。作家大可花上数年打磨一本传世之作,但程序员在代码上钻牛角尖就很有问题。

    世间没有完美的代码。大多数时候,你的代码只要能满足当前需求,又为未来扩展留了一些空间就够了。有那么几次,我在简历上看到候选人给自己打着“代码强迫症”标签。隔着屏幕,我虽能感受到 TA 对代码质量的那份重视,但在我心底,其实更期望 TA 早已将完美主义陷阱远远甩在了后头。

    5. 技术很重要,但“人”也许更重要

    在软件开发领域,“单一职责原则”(全称为 Single responsibility principle,后简称为 SRP)是一条非常著名的设计原则。它的定义很简单,一句话就可以概括:“每个软件模块应该只有一个被修改的理由”。

    单一职责原则:能做到,并不意味着你就该这么做

    要掌握 SRP 原则,关键在于搞清楚“被修改的理由”为何物。很显然,程序是没有生命的,它自身不能也不需要主动去改变。任何修改程序的理由,都来自与之相关的人,人是导致修改的“罪魁祸首”。

    举个简单的例子。看看下面这两个类,其中哪一个违反了 SRP 原则?

    1. 一个字典数据类,支持两类操作:存数据、取数据;
    2. 一个员工资料类,支持两类操作:更新个人信息、渲染一张用户资料卡片图。

    在大多数人眼里,第一个例子没问题,但第二个例子却明显违反了 SRP 原则。要得出该结论,好像无需任何严格的分析和证明,运用一丁点直觉即可。但假如做一些正经分析,第二个例子的可疑之处,在于能为其轻松找出两个不同的修改理由:

    1. 管理员认为资料中的“个人电话”字段不能有非法号码,需增加简单的校验逻辑
    2. 某员工认为资料卡片图上的“名字”部分太小,希望加大字体

    ”It is people who request changes. And you don’t want to confuse those people, or yourself, by mixing together the code that many different people care about for different reasons.” ——“The Single Responsibility Principle”

    “是人在要求软件变更。你绝不想把那些不同人出于不同原因所关心的代码混在一起,这样只会把他们和你自己搞糊涂。”——“单一职责原则”

    理解 SRP 原则的关键,在于先理解人以及人在软件开发中所扮演的角色。

    再举一个例子。微服务架构是近些年很火的一个技术话题。但许多人在讨论它时,往往只关注技术本身,却忽视了微服务架构与人之间的关系。

    将微服务架构风格与其他东西区分开的关键,在于将大单体拆分为独立的微服务后,不同模块间的边界可以变得更清晰。跟数百人的团队一同维护着一个大单体比起来,许多小组织各自维护着独立的微服务,明显拥有更高的运作效率。

    如果缺少了特定的组织规模(也就是“人”)作为前提,空谈微服务的各种技术优势和那些花活,纯属本末倒置。

    技术当然很重要。身为技术人员,那一张张瑰丽的架构图和独具匠心的代码细节,天然吸引着我们的注意力。但是,也请千万不要对软件开发里的另一个重要因素“人”视而不见。必要时,转换一下看事情的角度(从“技术”转向“人”),那样对你大有裨益。

    6. 求知若渴是好事,但也要注意方法

    如今人人都在说“终身学习”,而程序员是一个尤其需要终身学习的职业。因为计算机技术的迭代更新非常快,某个三年前流行的框架或编程语言,很可能一个月前已经过时。

    一分钟之内会发生什么事情?Netflix 观看时间增长 70,000 小时;Snapchat 上有三百万视频被观看;Google 新增两百四十万次搜索;一个 JS 新框架被发明(这条不是真的 🤓)

    要在工作中表现得游刃有余,程序员们需要学习的东西非常多,涵盖各个层面。拿我比较熟悉的后端领域举例,一位合格的后端工程师至少需要掌握以下这些:

    一种或多种后端编程语言 / MySQL 等关系数据库 / Redis 等常见存储组件 / 设计模式 / 用户体验 / 软件工程 / 编译原理 / 操作系统 / 网络基础 / 分布式系统 / …

    虽然要学很多,但据我观察,大部分程序员其实都挺爱学习(至少不排斥),因此心态不是问题。不过有的时候,光有“求知若渴”的心态并不够,学习时,我们尤其需要关注“性价比”。

    关注学习性价比

    下面这张图,展示了学习成效和投入之间的关系。

    学习成效与投入关系图,横轴为学习投入,纵轴为学习成效

    从图中可以看到,在学习的初级阶段,投入较少时,所获得成效增长飞快。但当成效超过某个阈值后,之后再想继续提升,所需要的学习投入就会呈指数级增长。

    正因如此,我建议你在学习任何一项新事物时,先在脑海中想清楚一个问题:“我应该在上图中的哪个位置停下来?”,而不是闷头猛学。

    知识的海洋浩瀚无边,有些东西需要我们成年累月的持续学习,不断精进。也有些东西,蜻蜓点水般学到一些皮毛已绰绰有余。准确判断并分配自己有限的学习精力,甚至比努力学习本身更重要。

    挑选合适的学习资料

    有了学习目标后,下一步就是寻找合适的学习资料。在这方面,我想分享一次自己的失败经历。

    有段时间,我突然对产品交互设计产生了浓厚的兴趣,认为自己应该在这方面有所精进。于是,我精心挑选了一本领域内非常经典的专业书:《About Face 4: 交互设计精髓》,将其买回家中,满怀信心地认为自己的交互设计能力可以迅速获得提升。

    但事与愿违,当我捧着那本经典著作时,发现自己连第一章都无法顺利读完——那句老话说的没错:“隔行如隔山”。

    从这次失败中,我总结出了一点经验。那就是学习某项新东西时,我们最好挑选那些更易读,更适合“门外汉”的学习资料,不要“眼睛大,嘴巴小”,只知道奔着最经典、最权威的资料而去。

    回顾之前的经历,我觉得以下几本书非常适合门外汉学习使用,性价比极高:

    也许每个人的内心,都想成为一个博学的人,无所不知,无所不晓。但可供分配的时间的精力总是有限,我们不能,也不需要在所有领域都成为专家。

    7. 越早开始写单元测试越好

    我非常非常喜欢单元测试,我认为写单测这件事,对我的编程生涯影响极大。夸张点说,如果以“开始写单元测试”作为分界线,把我的职业生涯分割成两段,后面那段远比前面那段精彩得多。

    写单测的好处很多,比如单测可以驱动你改善代码的设计、可以作为代码的一种文档,等等。此外,完善的单元测试还是构建前面提到的“高效犯错的环境”的关键。

    我已经写过几篇关于单测的文章,比如《有关单元测试的 5 个建议》《游戏“蔚蓝山”教我的编程道理》。所以在这儿,我不打算再重复一遍。只说一句:如果到目前为止,你从未试过写单元测试,或从没重视过测试,我建议你从明天就开始写起来。

    一般情况下我不测试我的代码,但假如测的话,我在生产环境测

    8. 程序员最大的敌人是什么?

    在大多数程序员段子里,产品经理经常作为反派角色出现。他们口中的项目需求总是变个不停,一天冒出一个新想法,搞得程序员苦不堪言。

    客户每天都在不停修改需求,所以,我们决定在下次发布前,把这些需求“冻结”起来

    在这些段子的烘托下,不断修改需求的产品经理,仿佛真成了程序员们最大的仇敌。似乎只要产品不乱改需求,大家的工作环境马上就会成为乌托邦。

    虽然偶尔吐槽一两句产品经理很有意思,但我还是想一本正经的说一句:产品经理不是敌人。

    因为从某种角度来说,软件生来就是准备被修改的(不然你猜,软件为什么叫“软”件?)。这样看来,开发软件和修建房子完全不同。因为没人会在建好一栋大楼后说:“让我们把它推倒重建一遍吧!一样的楼,但是用的钢筋和水泥比之前少 30%!”

    所以,产品经理以及不稳定的需求不是程序员的敌人。并且,能否写出易于修改、适配变化的代码,是区分普通程序员和优秀程序员的重要标准之一。

    那么,程序员们最大的敌人又是什么呢?

    复杂度是最大的敌人

    就像《代码大全2》中所说:软件开发的核心问题是管理复杂度。失控的复杂度就是程序员最大的敌人。

    来看看那些导致项目复杂度不断增长的要素:

    • 不断增加的新功能: 更多的功能等于更多的代码,更多的代码通常意味着更高的复杂度
    • 对高可用的需求: 为了实现高可用,消息队列等额外的技术组件和代码被引入
    • 对高性能的需求: 为了提升性能,缓存和相关模块代码被引入,部分模块被拆分后,换成高性能语言重写
    • 一再被推迟的重构:因项目排期过于紧张,迫在眉睫的重构被一再推迟,技术债越积越多
    • 忽视自动化测试: 没人写单元测试,也没人关心测试

    终有一天,当项目的复杂度增长到一定程度后,空中会传来一声巨响。“咚!”,一个大家不愿改、不敢改的“大坑”凭空出现在了所有人的 IDE 中。

    猜猜看,究竟是谁挖下了这个坑?

    那些在降低复杂度上投入时间的团队,所负责的软件项目更容易成功

    减缓复杂度增长的过程

    虽然复杂度总是会不可避免地持续增长,但有许多实践可以减缓该过程。如果每个人都能做到以下这些事,复杂度就有可能被长期控制在合理范围内:

    • 精通当前编程语言与工具,写整洁的代码
    • 使用合适的设计模式和编程模式
    • 对重复代码零容忍,抽象库和框架
    • 适当运用整洁架构、领域驱动设计思想
    • 编写详尽的文档和注释
    • 编写规范有效的单元测试
    • 分离那些变动的与不变的

    要求看上去很多,但总结起来,核心其实就是一句话:写更好的代码

    写在最后

    2020 年,我在小组内做了一个分享,当时的 PPT 标题是《编程十年后的十个感触》。将资料分享在公司内网后,有位同事看到,评论说光看 PPT 不过瘾,希望我能将其扩展成一篇文章,我回复说没问题。如今 3 年过去了,我总算是兑现了自己的承诺。

    当年准备分享材料时,我做完整个 PPT,最后一页实在不知道该放些啥。于是灵机一动,搞了个纯白色的背景,中间打了一行黑体大字:“十年很短,编程很难”。如今,第二个十年也已快行至中途,而这句话的后半部分好像对我仍然适用——长进不大,继续加油 😅。


    文末插入一个小广告:如果你喜欢阅读这篇文章,也欢迎了解我的书:《Python 工匠:案例、技巧与工程实践》 。它专注于基础编程素养与 Python 高级技巧的结合,是一本适合许多人的 Python 编程进阶书。

    京东购买 | 豆瓣书评

    ❌
    ❌