小土刀

一个满怀热爱的手艺人。无论是文字还是代码,我都想写点不一样的。

【程序员修炼之道】读书笔记

之前囫囵吞枣看完了这本书,现在再回首细细看,经过一个学期的历练,的确有完全不一样的感悟。这里是整本书的笔记,具体的感悟会另外再写。

    • 注重实效的程序员的特征

  • 第 1 章 注重实效的哲学 A Pragmatic Philosophy

  • 第 2 章 注重实效的途径 A Pragmatic Approach

  • 第 3 章 基本工具 The Basic Tools

  • 第 4 章 注重实效的偏执 Pragmatic Paranoia

  • 第 5 章 弯曲,或折断 Bend, or Break

  • 第 6 章 当你编码时 While You Are Coding

  • 第 7 章 在项目开始之前 Before the Project

  • 第 8 章 注重实效的项目 Pragmatic Projects

  • 提示汇总

编程是注重实效(pragmatism)的,不应该局限于任何特定的技术,而是应该拥有足够广博的背景和经验基础,让你能在特定情况下选择好的解决方案。背景源自对计算机科学的基本原理的理解,经验来自广泛的实际项目,理论与实践的结合使你强大起来。

调整方法,以适应当前情形与环境。判断对项目有影响的所有因素的相对重要性,并利用经验制定适宜的解决方案。注重实效的程序员不仅要完成工作,而且要完成得漂亮。

注重实效的程序员的特征
  • 早期的采纳者/快速的改编者。具有技术和技巧上的直觉,喜爱试验各种事物。给你一样新东西,你很快就能掌握,并把它与你的知识的其余部分结合在一起。自信源自经验。

  • 好奇。喜欢提问,是收集小知识的林鼠(pack rat),每一条小知识都可能会影响今后几年的某项决策。

  • 批判的思考者。不会不首先抓住事实而照搬别人的说法。

  • 有现实感。设法理解每个问题的内在本质。这样的现实主义给你良好的感知能力:事情有多困难,需要多长时间?让自己了解某个过程会有困难,或是要用一点时间才能完成,能够给予自己坚持不懈的毅力

  • 多才多艺。尽力熟悉广泛的技术和环境,并且努力工作,以与时俱进。尽管你现在的工作也许只要求你成为某方面的专才,你却总是能够转向新的领域和新的挑战

我们,采集的只是石头,却必须时刻展望未来的大教堂。 ——采石工人的信条

持续做出许多小改进,这是一个持续的过程。

第 1 章 注重实效的哲学 A Pragmatic Philosophy

注重实效的程序员在寻求解决方案时,能够越出直接的问题去思考,总是设法把问题放在更大的语境中,总是设法注意更大的图景。另外,他们也会对他们所做的每件事负责。

在所有的弱点中,最大的弱点就是害怕暴露弱点

如果你确实统一要为某个结果负责,就应该切实负起责任。当你犯错误或是判断失误时,诚实地承认它,并设法给出各种选择。不要责备别人或别的东西,或是拼凑借口。

欲求更好,常把好事变糟

如果你不懂得何时止步,所有的辛苦劳作就会遭到毁坏。如果你一层又一层、细节复细节地添加,绘画就会迷失在绘制之中。不要因为过度修饰和过于求精二毁损完好的程序。

知识上的投资总能得到最好的回报

管理知识资产与管理金融资产非常相似:

  1. 严肃的投资者会将定期投资作为习惯

  2. 多元化是长期成功的关键

  3. 聪明的投资者在保守的投资和高风险、高回报的投资之间平衡他们的资产

  4. 投资者设法低买高卖,以获取最大回报

  5. 应周期性地重新评估和平衡资产

一些具体的建议

  • 每年至少学习一种新语言

  • 每季度阅读一本技术书籍

  • 也要阅读非技术类书籍

  • 上课

  • 参加本地用户组织

  • 试验不同环境

  • 跟上潮流

  • 上网

我相信,被打量比被忽略要好

  • 知道自己想说什么

  • 了解你的听众

  • 选择时机

  • 选择风格

  • 让文档美观

  • 让听众参与

  • 做倾听者

  • 回复他人

第 2 章 注重实效的途径 A Pragmatic Approach

系统中每一项知识都必须具有单一、无歧义、权威的表示

如果两个或更多事物中的一个发生变化,不会影响其他事物,这些事物就是正交的。非正交系统的改变与控制更复杂是其固有的性质。当任何系统的各组件互相高度依赖时,就不再有局部修正(local fix)这样的事情。如果你编写正交的系统,两个主要好处是:提高生产力与降低风险。

可以将下面的技术用于维持正交性

  • 让代码保持解耦。不会没有必要地向其他模块暴露任何事情,也不依赖其他模块的实现

  • 避免使用全局数据。一般而言,如果你把所需的任何语境(context)显式传入模块,你的代码就会更易于理解和维护

  • 避免编写相似的函数。养成重构的好习惯

如果某个想法是你唯一的想法,再也没有比这更危险的事情了

曳光弹行之有效,是因为它们与真正的子弹在相同的环境、相同的约束下工作。它们快速飞向目标,所以枪手可以得到即时的反馈。同时,从实践的角度看,这样的解决方案也更便宜。

原型制作生成用过就扔的代码,曳光弹代码虽然简约,但却是完整的,并且构成了最终系统的骨架的一部分。

任何带有风险的事物,以前没有试过的事物,或是对于最终系统极端关键的事物;任何未被证明的,实验性的,或有疑问的事物;任何让你觉得不舒服的事物,都应该制作原型:

  • 架构

  • 已有系统中的新功能

  • 外部数据的结构或内容

  • 第三方工具或组件

  • 性能问题

  • 用户界面设计

语言的界限就是一个人的世界的界限 ——维特根斯坦

通过学习估算,并将此技能发展到你对事物的数量级有直觉的程度,就能展现出一种魔法般的能力,确定它们的可行性

  • 多准确才足够准确

  • 估算来自哪里

  • 理解提问内容

  • 建立系统的模型

  • 把模型分解为组件

  • 给每个参数指定值

  • 计算答案

  • 追踪你的估算能力

在被要求进行估算时说什么:我等会儿回答你。如果放慢估算的速度,并花一点时间仔细检查各个步骤,几乎总能得到更好的结果

第 3 章 基本工具 The Basic Tools

工具放大你的才干,你的工具越好,你越是能更好地掌握它们的用法,你的生产力就越高。从一套基本的通用工具开始,随着经验的获得,随着你遇到一些特殊需求,你将会在其中添加新的工具。要与工匠一样,想着定期增添工具,要总是寻找更好的做事方式。如果遇到某种情况,你觉得现有的工具不能解决问题,记得去寻找可能会有帮助的其他工具或更强大的工具。

对于操纵文本文件的程序员,工作台就是命令 shell。当然,最好精通一种编辑器,并将其用于所有编辑任务,一些基本能力:

  • 可配置

  • 可扩展

  • 可编程

  • 其他特性:语法高亮,自动完成,自动缩进,初始代码/文档样板,与帮助系统挂接,类 IDE 特性(编译调试等)

进步远非由变化组成,而是取决于好记性。不能记住过去的人,被判重复过去

源码控制系统追踪你在源码和文档中做出的每一项变动。

要接受事实:调试就是解决问题,要据此发起进攻

最容易欺骗的人是一个人自己

当你遇到让人吃惊的 bug 时,除了只是修正它以外,你还需要确定先前为什么没有找出这个故障。考虑你是否需要改进单元测试或其他测试,以让它们有能力找出这个故障

被动代码生成器减少敲键次数。它们本质上是参数化木板,根据一组输入生成给定的输出形式,例如:

  • 创建新的源文件

  • 在编程语言之间进行一次性转换

  • 生成查找表及其他在运行时计算很昂贵的资源

主动代码生成器可以取某项知识的一种表示形式,将其转换成为你的应用需要的所有形式。无论何时你发现自己在设法让两种完全不同的环境一起工作,你都应该考虑使用主动代码生成器。

第 4 章 注重实效的偏执 Pragmatic Paranoia

当每个人都确实要对你不利时,偏执就是一个好主意

如果有一个错误,就说明非常非常糟糕的事情已经发生了

在自责中有一种满足感,当我们责备自己时,会觉得没人有权责备我们

关于异常的问题之一是知道何时使用它们。异常很少应作为程序的正常流程的一部分使用,异常应保留给意外事件

第 5 章 弯曲,或折断 Bend, or Break

好篱笆促成好邻居

把你的代码组织成最小组织单位(模块),并限制它们之间的交互,如果随后出于折中必须替换某个模块,其他模块仍能够继续工作

德墨忒尔法则:某个对象的任何方法都应该只调用属于以下情形的方法:

  • 它自身

  • 传入该方法的任何参数

  • 它创建的任何对象

  • 任何直接持有的组件对象

循序德墨忒尔法则缩小了调用类(calling class)中的响应集的规模,结果以这种方式设计的类的错误也往往更少

再多的天才也无法胜过对细节的专注

我们想让我们的系统变得高度可配置。不仅是像屏幕颜色和提示文本这样的事物,而且也包括诸如算法、数据库产品,中间件技术和用户界面风格之类更深层面的选择。这些选择应该作为配置选项,而不是通过集成或者工程(engineering)实现。

我们不只是想把元数据用于简单的偏好,我们想尽可能多地通过元数据配置和驱动应用。我们的目标是以声明方式思考(规定要做什么,而不是怎么做),并创建高度灵活和可适应的程序。我们通过采用一条一般准则来做到这一点:为一般情况编写程序,把具体情况放在别处——在编译的代码库之外。

我们想要推迟大多数细节的定义,直至最后时刻,并且尽可能让细节保持『软和』——尽可能易于改动,通过精心制作允许我们快速做出变更的解决方案。

我们需要容许并发,并考虑解除任何时间或次序上的依赖。这样做,我们可以获得灵活性,并减少许多开发领域中的任何基于时间的依赖:工作流分析、架构、设计还有部署

  • 为并发进行设计

  • 更整洁的接口

第 6 章 当你编码时 While You Are Coding

我们应当避免靠巧合编程——依靠运气和偶然的成功——而要深思熟虑地编程

周遭所见,皆是变易与腐败

无论代码具有下面哪些特征,都应该考虑重构代码;

  • 重复

  • 非正交的设计

  • 过时的知识

  • 性能

怎样进行重构:

  • 不要试图在重构的同时增加功能

  • 在开始重构之前,确保你拥有良好的测试。尽可能经常运行这些测试。这样,如果你的改动破坏了任何东西,你就能很快知道

  • 采取短小,深思熟虑的步骤

第 7 章 在项目开始之前 Before the Project

完美,不是在没有什么需要增加,而是在没有什么需要去掉时达到的

你必须挑战任何先人之见,并评估它们是否是真实的,必须遵守的约束。问题不在于你是在盒子里面思考,还是在盒子外面思考,而在于找到盒子——确定真正的约束。

一定有更容易的方法!遇到问题时问问自己:

  • 有更容易的方法吗?

  • 是在设计发解决真正的问题,还是被外围的技术问题转移的注意力?

  • 这件事为什么是一个问题?

  • 是什么使它如此难以解决?

  • 它必须以这种方式完成吗?

  • 它真的必须完成吗?

第 8 章 注重实效的项目 Pragmatic Projects

使项目级活动保持一致和可靠的一个最重要的因素是使你的各种工作流程自动化。

质量是一个团队问题。最勤勉的开发者如果被派到不在乎质量的团队里,会发现自己很难保持修正琐碎问题所需的热情。

认为项目的各种活动——分析、设计、编码、测试——会鼓励发生,是一个错误。

文明通过增加我们不假思索就能完成的重要操作数目而取得进步

项目编译是意见应该可靠可重复进行的琐碎工作,包括:

  • 生成代码

  • 回归测试

  • 构建自动化

需要进行的测试的主要类型有:

  • 单元测试

  • 集成测试

  • 验证和校验(validation and verification)

  • 资源耗尽、错误及恢复

  • 性能测试

  • 可用性测试

在现实中,项目的成功是由它在多大程度上满足了用户的期望来衡量的。不符合用户语气的项目注定是失败的,不管交付的产品在绝对的意义上有多好。

如果我们在负责一项设计,或是一段代码,我们是在做可以引以自豪的工作。

提示汇总

这些提示描述了什么才是注重实效的程序员

[1] Care About Your Craft 关心你的技艺

除非你在乎能否漂亮地开发出软件,其他事情都是没有意义的

[2] Think! About Your Work 思考!你的工作

对每一项决策进行评估,不间断思考,实时批判你的工作

[3] Provide Options, Don’t Make Lame Excuses 提供各种选择,不要找蹩脚的借口

不要说事情做不到,要说明能够做什么来挽回局面。不要害怕提出要求,也不要害怕承认你需要帮助

[4] Don’t Live with Broken Windows 不要容忍破窗户

一旦发现低劣的设计、错误的决策或是糟糕的代码,就赶紧修复

[5] Be a Catalyst for Change 做变化的催化剂

人们发现,参与正在发生的成功要更容易。让他们瞥见未来,就能让他们聚集在你周围

[6] Remember the Big Picture 记住大图景

要持续不断地观察周围发生的事情,而不只是你自己在做的事情

[7] Make Quality a Requrements Issue 使质量成为需求问题

如果你给用户某样东西,让他们及早使用,他们的反馈常常会把你引向更好的最终解决方案

[8] Invest Regularly in Your Knowledge Portfolio 定期为你的知识资产投资

这是所有指导方针中最重要也是最简单的,持续投入非常重要

[9] Critically Analyze What You Read and Hear 批判地分析你读到的和听到的

拥有大量知识资产,并把批判的分析应用于你将要阅读的技术出版物的洪流,你将能够理解复杂的答案

[10] It’s Both What You Say and the Way You Say It 你说什么和你怎么说同样重要

交流越有效,你就越有影响力

[11] DRY - Don’t Repeat Yourself 不要重复你自己

这是注重实效的程序员的工具箱里最重要的工具之一

[12] Make It Easy to Reuse 让复用变得容易

你所要做的是营造一种环境,在其中找到并复用已有的东西,比自己编写更容易。如果不容易,大家就不会去复用。而如果不进行复用,你们就会有重复知识的风险

[13] Eliminate Effects Between Unrelated Things 消除无关事物之间的影响

我们想要设计自足(self-contained)的组件:独立,具有单一、良好定义的目的(内聚, cohesion)

[14] There Are No Final Decisions 不存在最终决策

要把决策视为是写在沙滩上的,而不要把它们刻在石头上。大浪随时可能到来,把它们抹去

[15] Use Tracer Bullets to Find the Target 用曳光弹找到目标

为了在代码中获得这样的效果,我们要找到某种东西,让我们能快速,直观和可重复地从需求出发,满足最终系统的某个方面要求

[16] Prototype to Learn 为了学习而制作原型

原型制作是一种学习经验。其价值并不在于所产生的代码,而在于所学到的经验教训,那才是原型制作的要点所在

[17] Program Close to the Problem domain 靠近问题领域编程

无论是用于配置和控制应用程序的简单语言,还是用于指定规则或过程的更为复杂的语言,都应该考虑让你的项目更靠近问题领域。通过在更高的抽象层面上编码,你获得了专心解决领域问题的自由,并且可以忽略琐碎的实现细节

[18] Estimate to Avoid Surprises 估算,以避免发生意外

某种程度来说,所有的解答都是估算。只不过有一些要比其他的更准确

[19] Iterate the Schedule with the Code 通过代码对进度表进行迭代

必须帮助他们了解团队、团队的生产率、还有环境将决定进度

[20] Keep Knowledge in Plain Text 用纯文本保存知识

保证不过时,杠杆作用(计算世界的每一样工具,从源码管理到编译器环境,再到编辑器及独立的过滤器,都能在纯文本上进行操作),更易于测试

[21] Use the Power of Command Shells 利用命令 shell 的力量

去熟悉 shell,你会发现自己的生产率迅速提高

[22] Use a Single Editor Well 用好一种编辑器

选一种编辑器,彻底了解它,并将其用于所有的编辑任务

[23] Always Use Source Code Control 总是使用源码控制

即使团队只有一个人,项目只有一周,确保每样东西都处在源码控制之下

[24] Fix the Problem, Not the Blame 要修正问题,而不是发出指责

bug 是你的过错还是别人的过错,并不是真的很有关系。它仍然是你的问题

[25] Don’t Panic 不要恐慌

这是调试的第一准则,要总是设法找出问题的根源,而不只是问题的特定表现

[26] “Select” Isn’t Broken “Select” 没有问题

如果你『只改动了一样东西』,系统就停止了工作,那样东西很可能就需要对此负责——直接地或间接地,不管那看起来有多牵强。

[27] Don’t Assume it - Prove It 不要假定,要证明

不要因为你『知道』它能哦你工作而轻易放过与 bug 有牵连的例程或代码,证明它。

[28] Learn a Text Manipulation Language 学习一种文本操纵语言

使用它们快速构建实用程序,为你的想法建立原型

[29] Write Code That Writes Code 编写能编写代码的代码

代码生成器有两种主要类型:被动代码生成器(只运行一次来生成结果),主动代码生成器(每次需要其结果时被调用)

[30] You Can’t Write Perfect Software 你不可能写出完美的软件

把它视为生活的公理,接受它,拥抱它,庆祝它

[31] Design with Contracts 通过合约进行设计

注意前条件、后条件和类不变项。继承和多态是面向对象语言的基石,是合约可以真正闪耀的领域。

[32] Crash Early 早崩溃

尽早检测问题的好处之一是你可以更早崩溃

[33] If It Can’t Happen, Use Assertions to Ensure That It Won’t 如果它不可能发生,用断言确保它不会发生

无论何时你发现自己在思考『但那当然不可能发生』,增加代码检查它,最容易的方法是使用断言

[34] Use Exceptions for Exceptional Problems 将异常用于异常的情况

异常表示即时的、非局部的控制转移——这是一种级联的(cascading) goto。那些把异常用作其正常处理的一部分的程序,将遭受到经典的诶意大利面条式代码的所有可读性和可维护性问题的折磨。这些程序破坏了封装;通过异常处理,例程和它们的调用者被更紧密地耦合在一起。

[35] Finish What You Start 要有始有终

对于资源分配和解除分配的处理,这是一个简单的提示

[36] Minimize Counpling Between Modules 使模块之间的耦合减至最小

通过编写尽可能遵守德墨忒尔法则的『羞怯』代码

[37] Configure. Don’t Integrate 要配置,不要集成

要用元数据(metadata)描述应用的配置选项:调谐参数,用户偏好(user preference),安装目录等等

[38] Put Abstractions in Code. Details in Metadata 将抽象放进代码,细节放进元数据

这样做的好处是:

  • 它迫使你解除你的设计的耦合,从而带来更灵活,可适应性更好的程序

  • 它迫使你通过推迟细节处理,创建更健壮、更抽象的设计——完全推迟到程序之外

  • 无需重新编译应用,你就可以对其进行定制。你还可以利用这一层面的定制,轻松绕开正在运行的产品系统中的重大 bug

  • 与通用的编程语言的情况相比,可以通过一种大为接近问题领域的方式表示元数据

  • 甚至可以用相同的应用引擎——但是用不同的元数据——实现若干不同的项目

[39] Analyze Workflow to Improve Concurrency 分析工作流,以改善并发性

可以使用动作图,通过找出本来可以,但却没有并行执行的动作,使并行度最大化

[40] Design Using Services 用服务进行设计

实际上我们创建的不是组件,而是服务——位于定义良好的,一致的接口之后的独立,并发的对象

[41] Always Design for Concurrency 总是为并发进行设计

一旦你设计了具有并发要素的架构,对许多并发服务的处理进行思考就会变得更容易:模型变成了普遍的

[42] Separate Views from Models 使视图与模型分离

通过松解模型与视图/控制器之间的耦合,用低廉的代价为自己换来了许多灵活性

[43] Use Blackboards to Coordinate Workflow 用黑板协调工作流

可以用黑板协调完全不同的事实和因素,同时又使各参与方保持独立,甚至隔离

[44] Don’t Program by Coincidence 不要靠巧合编程

怎样深思熟虑地编程:

  • 总是意识到你在做什么

  • 不要盲目地编程,视图构建你不完全理解的应用,或是使用你不熟悉的技术,就是希望自己被巧合误导

  • 按照计划行事

  • 依靠可靠的事物,不要依靠巧合或假定

  • 为你的假定建立文档,『按合约编程』有助于澄清你头脑中的假定,并且有助于把它传达给他人

  • 不要只是测试你的代码,还要测试你的假定,不要猜测,要实际尝试它。编写断言测试你的假定

  • 为你的工作划分优先级,把时间花在重要的方面

  • 不要做历史的奴隶,不要让已有的代码支配将来的代码

[45] Estimate the Order of Your Algorithm 估算你的算法的阶

如果不能确定,就试着运行它,变化输入的数量,把结果画成图,来找到答案

[46] Test Your Estimates 测试你的估算

如果要获得准确的计时很棘手,就用代码剖析器(code profiler)

[47] Refactor Early. Refactor Often 早重构,常重构

追踪需要重构的事物,如果你不能立刻重构某样东西,就一定要把它列入计划。确保受到影响的代码的使用者知道该代码计划要重构,以及这可能会怎样影响他们。

[48] Design to Test 为测试而设计

当你设计模块,或是单个例程,应该既设计其合约,也设计测试其合约的代码

[49] Test Your Software, or Your Users Will 测试你的软件,否则你的用户就得测试

测试是技术,但更是文化

[50] Don’t Use Wizard Code You Don’t Understand 不要使用你不理解的向导代码

重点在于理解,否则没有能力维护,在调试时也会遇到很大困难

[51] Don’t Gather Requirements - Dig for Them 不要搜集需求——挖掘它们

需求是对需要完成的某件事情的陈述

[52] Work with a User to Think Like a User 与用户一同工作,以像用户一样思考

开采需求的过程也是开始与用户群建立和谐的关系,了解他们对你正在构建的系统的期许和希望

[53] Abstractions Live Longer than Details 抽象比细节活得更长久

需求不是架构,需求不是设计,也不是用户界面。需求是需要,看远一些

[54] Use a Project Glossary 使用项目词汇表

如果用户和开发者用不同的名称指代同一事物,或是用同一名称指代不同事物,这样的项目很难取得成功

[55] Don’t Think Outside the Box - Find the Box 不要在盒子外面思考——要找到盒子

面对棘手问题时,列出所有在你面前的可能途径。不要排除任何东西,不管听起来多么无用或愚蠢

[56] Listen to Nagging Doubts - Start When You’re Ready 倾听反复出现的疑虑——等你准备好再开始

相信自己的直觉,可能没有办法明确指出问题所在,但是觉得不对劲,就要停下来好好想想

[57] Some Things Are Better Done than Described 对有些事情『做』胜于『描述』

倾向于把需求搜集、设计以及实现视为同一个过程——交付高质量的系统——的不同方面

[58] Don’t Be a Slave to Formal Methods 不要做形式方法的奴隶

盲目采用任何技术,而不把它放进你的开发实践和能力的语境中,肯定会出问题

[59] Expensive Tools Do Not Produce Better Designs 昂贵的工具不一定能制作出更好的设计

应该不断提炼和改善你的开发过程,决不要把方法学的呆板限制当做你世界的边界

[60] Organize Around Functionality, Not Job Functions 围绕功能,而不是工作职务进行组织

[61] Don’t Use Manual Procedure 不要使用手工流程

人的可重复性并不像计算机那么好,我们也不应该期望人们能那样

[62] Test Early. Test Often. Test Automatically 早测试,常测试,自动测试

一旦我们有了代码,我们就想开始进行测试。bug 被发现得越早,进行修补的成本就越低。

[63] Coding Ain’t Done ‘Til All the Tests Run 要到通过全部测试,编码才算完成

首先,代码从不会真正完成,更重要的是,在它通过所有可用的测试之前,你不能声称它已经可供使用

[64] Use Saboteurs to Test Your Testing 通过『蓄意破坏』测试你的测试

故意引入 bug,并证实测试能抓住它们

[65] Test State Coverage. Not Code Coverage 测试状态覆盖,而不是代码覆盖

即使具有良好的代码覆盖,你用于测试的数据仍然会有巨大的影响,而且,更为重要的是,你遍历代码的次序的影响可能是最大的

[66] Find Bugs Once 一个 bug 只抓一次

一旦测试人员找到了某个 bug,这应该是测试人员最后一次发现这个 bug。应该对自动化测试进行修改,从此每次斗殴检查那个特定的 bug

[67] Treat English as Just Another Programming Language 把英语当做又一种编程语言

把文档当做整个开发过程的完整组成部分加以接受

[68] Build Documentation in. Don’t Bolt It On 把文档建在里面,不要拴在外面

一般来说,注释应该讨论为何要做某事,它的目的和目标。代码已经说明了它是怎样完成的,所以再为此加上注释是多余的——而且违反了 DRY 原则

[69] Gently Exceed Your Users’ Expectation 温和地超出用户的期望

交流期望,管理期望,要让用户惊讶,而不是惊吓他们

[70] Sign Your Work 在你的作品上签名

过去时代的手艺人为能在他们的作品上签名而自豪,你也应该如此


评论
热度(8)

© 小土刀 | Powered by LOFTER