鄢倩

(conj clojurians me)

函数式编程是什么

函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。 – wiki

例子一 累加运算

1
2
3
4
5
6
7
8
9
10
11
12
13
// sum
List<Integer> nums = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 10);

public static Integer sum(List<Integer> nums) {
int result = 0;
for (Integer num : nums) {
result += num;
}

return result;
}

sum(nums); // -> 46

同样的代码用 Java8 Stream 实现

1
Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 10).stream().reduce(0, Integer::sum);

同样的代码用 Clojure 实现

1
2
(apply + [0 1 2 3 4 5 6 7 8 10]) ; -> 46
#_(reduce + [0 1 2 3 4 5 6 7 8 10])

例子二 fabonacci数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// java
public static int fibonacci(int number) {
if (number == 1) {
return 1;
}
if(number == 2) {
return 2;
}

int a = 1;
int b = 2;
for(int cnt = 3; cnt <= number; cnt++) {
int c = a + b;
a = b;
b = c;
}
return b;
}
1
2
3
4
5
6
// java8
Stream.iterate(new int[]{1, 1}, s -> new int[]{s[1], s[0] + s[1]})
.limit(10)
.map(n -> n[1])
.collect(toList())
// -> [1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
1
2
3
4
5
// clojure
(->> (iterate (fn [[a b]] [b (+ a b)]) [1 1])
(map second)
(take 10))
; -> (1 2 3 5 8 13 21 34 55 89)

比起命令式的语言,函数式语言更加关注执行的结果,而非执行的过程。

函数式编程的历史

从Hilbert 23个数学难题谈起

1900年,Hilbert 提出了数学界悬而未决的10大问题,后续陆续添加成了23个问题,被称为著名的 Hilbert 23 Problem。针对其中第2个决定数学基础的问题——算术公理之相容性,年轻的哥德尔提出了哥德尔不完备定理,解决了这个问题形式化之后的前两点,即数学是完备的吗?数学是相容的吗?哥德尔用两条定理给出了否定的回答。所谓不完备,即系统中存在一个为真,但是无法在系统中推导出来的命题。比如:U说:“U在PM中不可证”。虽然和说谎者很类似,但其实有明显的差异。我们可以假设U为可证,那么可以推出PM是矛盾(不相容)的;但是假设U不可证,却推导不出PM是矛盾的。U的含义是在M中不可证,而事实上,它被证明不可证,所以U是PM中不可证的真命题。基于第一条不完备定理,又可以推导出第二条定理。如果一个(强度足以证明基本算术公理的)公理系统可以用来证明它自身的相容性,那么它是不相容的。

而最后一个问题,数学是确定的吗?也就是说,存在一个算法判定一个给定的命题是否是不确定的吗(Entscheidungsproblem 确定性问题)?这个问题引起了阿隆佐·邱奇和年轻的阿兰·图灵的兴趣。阿隆佐·邱奇的lambda calculus和图灵的图灵机构造出了可计算数,图灵的那篇论文 ON COMPUTABLE NUMBERS, WITH AN APPLICATION TO THE ENTSCHEIDUNGSPROBLEM 的意义不在于证明可计算数是否可数,而在于证明可判定性是否成立。在1936年他们对判定性问题分别独立给出了否定的答案。也就是现在被我们熟知的图灵停机问题:不存在这样一个程序(算法),它能够计算任何程序(算法)在给定输入上是否会结束(停机)。图灵借此发明了通用图灵机的概念,为后来的冯·诺依曼体系的计算机体系提供了理论基础。

Lambda Calculus

Lambda Calculus

Lambda 表达式包含三个要素

  1. 变量
  2. lambda 抽象
  3. lambda 应用
    据此我们可以用函数给出布尔值的定义
    1
    2
    3
    4
    5
    6
    7
    8
    data BOOL = FALSE | TRUE
    TRUE = λx.λy.x
    FALSE = λx.λy.y

    not = λb.b FALSE TRUE
    and = λb1.λb2.b1 b2 FALSE
    or = λb1.λb2.b1 TRUE b2
    xor = λb1.λb2.b1 (not b2) b2
    自然数的定义
    1
    2
    3
    4
    5
    6
    7
    8
    data NAT = Z | S NAT
    0 = λf.λs.s
    1 = λf.λs.f s
    2 = λf.λs.f f s

    succ n = λf.λs.f (n f s)
    zero? n = n (λb.FALSE) TRUE
    add = succ n1 n2

函数式编程语言的发展

在这之后,随着通用计算机的产生,人们发觉使用机器码写程序太没有效率。所以1956年左右,John Buckus发明了Fortran(FORmula TRANslating 的缩写)语言,如果对编译原理有了解,那么对BNF范式就不陌生了。与此同时,John McCarthy 发明了Lisp语言,现代的Clojure就是Lisp的方言之一。1966年,Niklaus Wirth发明了Pascal。1969年,Ken Thompson和Dennis Ritchie发明了C语言,过程式语言由于其高效和可移植性迅速崛起。1973年,Robin Milner 发明了ML(Meta Language),后来演变成了OCaml和Stardard ML。1977年,John Buckus在其图灵奖的演讲中创造了 Functional Programming 这个词。1990年,惰性求值的函数式编程语言 Haskell 1.0 发布。

编程语言发展历史

神奇的 Y Combinator

1
2
3
4
5
(def Y (fn [f]
((fn [x] (x x))
(fn [x]
(f (fn [y]
((x x) y)))))))

Lisp、ML以及Haskell的关系

Lisp是动态语言,使用S表达式
ML和Haskell都是静态强类型函数式语言
ML是第一个使用Hindley-Milner type inference algorithm的语言
Lisp和ML都是call-by-value,但是Haskell则是call-by-name
Lisp和ML都是不纯的编程语言,但是Haskell是side effect free的

函数是一等公民

函数是一等公民,指的是你可以将函数作为参数、返回值、数据结构存在,而且不仅可以用函数名引用,甚至可以匿名调用。

1. 作为参数

1
(map inc [1 2 3 4 5]) ;-> (2 3 4 5 6) ;; inc is an argument

2. 作为返回值

1
2
3
4
5
6
7
8
(defn add [num] 
(fn [other-num] (+ num other-num))) ;; as return-value
(def add-one (add 1))
(add-one 2) ;-> 3

(defn flip [f] ;; as argument and return-value
(fn [x y]
(f y x)))

3. 数据结构

1
2
3
(def dictionary {:a "abandon"}) ;; map is also a function, data is code.
(dictionary :a) ;-> "abandon"
(:a dictionary) ;-> "abandon"

4. 匿名函数

1
2
3
4
5
6
((fn [x] (* x x))
2) ;-> 4

(map
(fn [num] (+ 1 num)) ;; anonymous function
[1 2 3 4 5]) ;-> (2 3 4 5 6)

5. 模块化

在面向对象中,对象是一等公民。所以我们处处要从对象的角度去考虑计算问题,然后产生一种共识——数据应该和它相关的操作放到一起,也就是我们所说的封装。确实没错,但是我们得知道封装的意义在哪里?功能内聚好理解(分块)和局部性影响(控制可变性)。函数式编程同样考虑这些,功能内聚不一定要用类的方式(考虑一下JS的prototype,也是一种面向对象),只要模块做得好,一样能达到效果。局部性影响,其本质是封装可变因素以避免其扩散到代码各处。函数式给出了自己的答案,消除可变因素。

高阶函数和惰性求值也非常有利于模块化。

纯函数和不可变性

纯函数是指执行过程中没有副作用的函数,所谓副作用是说超出函数控制的操作,比如在执行过程中操作文件系统、数据库等外部资源。纯函数还具有引用透明性的特点,也就是同样的输入导致同样的输出,以至于完全可以用函数的值代替对函数的调用。

引用透明

举个例子:

1
2
3
4
(inc 1) ; -> 2

(= (inc (inc 1)
(inc 2))) ; -> true

你们可能就会问,这种东西究竟有什么用呢?纯函数可以很方便地进行缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(defn fibonacci [number]
(if (or (zero? number) (= 1 number)) 1
(+
(fibonacci (dec number))
(fibonacci (- number 2)))))
(fibonacci 30) ; -> "Elapsed time: 185.690208 msecs"

(def fibonacci
(memoize (fn [number] ;;
(if (or (zero? number) (= 1 number)) 1
(+
(fibonacci (dec number))
(fibonacci (- number 2)))))))
(fibonacci 30) ; -> "Elapsed time: 0.437114 msecs"

不可变计算

谈到不可变性,我们做个游戏。统计在座的一共有多少人数。我们都知道从某个人开始依次报数,最后得到的数字就是总人数,其实这就是一种不可变计算的游戏,为什么这么说呢?因为报数其实一个计算的过程,第一个人计算出1这个数,传递给第二个人。然后第二个人拿着前面的1进行加一操作,然后把结果2传递给后面的人做加法,以此类推。为了提高统计的效率,我也可以进行分组,然后每组自行报数,最后统计结果。但是如果我在白板上写个数字1,然后让大家来过来该这个数字,很大可能会出现错误,因为这个数字成为了竞态条件。在多并发的情况下,就得用读写锁来控制。所以不可变性特别利于并发。
不可变性

不可变的链式结构

好了,现在我们有个新的需求,设计一个不可变列表收集大家的名字。每个节点存储一个姓名的字符串,并且有个指针指向下一个节点。但是这也打破了列表的不可变性。怎么办?我们可以把新的节点指向旧有的列表,然后返回一个新的列表。这就是不可变列表实现的机制。随便一提,这也是区块链不可变特征的由来。

不可变的链式结构

Clojure的创造者Rich Hickey扩展了Ideal Hash Tree数据结构,实现了Persistent Vector。由于此处的叶子节点可以扩展成32个,所以可以大量存储数据。利用Ideal Hash Tree的特点可以快速索引出数据,与此同时,数据的“增删改”也能做到近常数化的时间,并且总是产生新的数据结构替换原有的数据结构,即一种不可变的链式存储结构。
Clojure Persistent Vector

不可变的树状结构

Zipper数据结构类似于文本编辑器中的 gap buffer,编辑文本时,光标左边和右边分别是独立的buffer,光标处也是单独的buffer,这样便可以方便地添加文字,也很方便删除左右buffer中的文字;移动光标会涉及buffer之间的拷贝。基本上能在常数时间内完成编辑。Zipper数据结构模仿了这种方式,能在常数时间内完成树的编辑工作,也能很快地重新构建一棵树。
不可变的树状结构

递归

可计算很大问题就是得实现递归功能。

1
2
3
4
(defn reverse-seq [coll]
(when-let [elem (first coll)]
(concat (reverse-seq (rest coll)) [elem])))
(reverse-seq [1 2 3]) ; -> (3 2 1)

和循环无异的尾递归

1
2
3
4
5
(defn gcd [& nums]
(reduce #(if (zero? %2)
%
(recur %2 (mod % %2))) nums))
(gcd 8 16) ; -> 8

生成式测试

生成式测试会基于输入假设输出,并且生成许多可能的数据验证假设的正确性。

1
2
3
4
5
6
7
8
9
10
11
(defn add [a b]
(+ a b))
;; 任取两个整数,把a和b加起来的结果减去a总会得到b。
(def test-add
(prop/for-all [a (gen/int)
b (gen/int)]
(= (- (add a b) a) b)))


(tc/quick-check 100 test-add)
; -> {:result true, :num-tests 100, :seed 1515935038284}

测试结果表明,刚才运行了100组测试,并且都通过了。理论上,程序可以生成无数的测试数据来验证add方法的正确性。即便不能穷尽,我们也获得一组统计上的数字,而不仅仅是几个纯手工挑选的用例。

抽象是什么

抽取共性,封装细节,忘记不重要的差异点。这样的好处是可以做到局部化影响和延迟决策。
抽象屏障

命名

命名就是一种抽象,重构中最重要的技法就是重命名和提取小函数

1
2
3
4
5
6
(* 3 3 3)
(* x x x)
(* y y y)
->
(defn cube [x]
(* x x x))

延迟决策

例如:我们定义数对 pair

1
2
3
pair:: (cons x y)
first pair -> x
second pair -> y

那么它的具体实现会是这样的

1
2
3
4
5
6
7
8
(defn cons [x y]
(fn [m]
(cond (= m 0) x
(= m 1) y)))
(defn first [z]
(z 0))
(defn second [z]
(z 1))

也可以是这样的,还可以是其它各种各样的形式。

1
2
3
4
5
6
7
(defn cons [x y]
(fn [b]
(b x y))
(defn first [z]
(z (fn [x y] x)))
(defn second [z]
(z (fn [x y] y)))

高阶函数

高阶函数就是可以接收函数的函数,高阶函数提供了足够的抽象,屏蔽了很多底层的实现细节。比如Clojure中的map高阶函数,它接收(fn [v] ...),把一组数据映射成另外一组数据。

过程抽象

1
(map inc [1 2 3 4 5]) ; -> (2 3 4 5 6)

这些函数抽象出映射这样语义,除了容易记忆,还能很方便地重新编写成高效的底层实现。也就是说,一旦出现了更高效的map实现算法,现有的代码都能立刻从中受益。

函数的组合

函数组合之后会产生巨大的能量

神奇的加法

1
(((comp (map inc) (filter odd?)) +) 1 2) ; -> 4

怎么去理解这个函数的组合?我们给它取个好听的名字

1
2
3
4
5
6
7
(def special+ ((comp (map inc) (filter odd?)) +))
(special+ 1 2) ; -> 4

; <=> 等价于
(if (odd? (inc 2))
(+ 1 3))
1)

这个未必是个好的组合方式,但是不可否认的是,我们可以用这些随意地将这些函数组合到一起,得到我们想要的结果。

transducer

1
2
3
(def xf (comp (filter odd?) (take 10)))
(transduce xf conj (range))
;; [1 3 5 7 9 11 13 15 17 19]

这里直接将求值延迟到了transduce计算的时候,换句话说,xf定义了一种过程:filter出奇数并取出前10个元素。同等的代码,如果用表达式直接书写的话,如下:

1
2
3
(->> (range)
(filter odd?)
(take 10))

这里的问题就是我们没能使用高阶函数抽象出过程,如果把 conj 换成其他的reduce运算,现在的过程无法支撑,但是tranducers可以!

1
(transduce xf + (range)) ;-> 100

我们再看一个tranducer的神奇使用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
(defn log [& [idx]]
(fn [rf]
(fn
([] (rf))
([result] (rf result))
([result el]
(let [n-step (if idx (str "Step: " idx ". ") "")]
(println (format "%sResult: %s, Item: %s" n-step result el)))
(rf result el)))))

(def ^:dynamic *dbg?* false)

(defn comp* [& xforms]
(apply comp
(if *dbg?*
(->> (range)
(map log)
(interleave xforms))
xforms)))

(binding [*dbg?* true]
(transduce
(comp*
(map inc)
(filter odd?))
+
(range 5))) ;; -> 9

Step: 0. Result: 0, Item: 1
Step: 1. Result: 0, Item: 1
Step: 0. Result: 1, Item: 2
Step: 0. Result: 1, Item: 3
Step: 1. Result: 1, Item: 3
Step: 0. Result: 4, Item: 4
Step: 0. Result: 4, Item: 5
Step: 1. Result: 4, Item: 5

之所以会出现上述的结果,是因为interleave xforms(map inc)以及(filter odd?)和logs进行了交叉,得到的结果是(comp (map inc) (log) (filter odd?) (log)),所以如果是偶数就会被filter清除,看不见log了。

首先一定得理解:每个tranducer函数都是同构的!
形式如下

1
2
3
4
(defn m [f]
(fn [rf]
(fn [result elem]
(rf result (f elem)))))

这意味着(m f)的函数都是可以组合的,组合的形式如下:

1
(comp (m f) (m1 f1) ...)

展开之后

1
2
3
4
5
6
7
((m f) 
((m1 f1)
((m2 f2) ...)))
->
(fn [result elem]
(((m1 f1)
((m2 f2) ...)) result (f elem)))

所以可以看到第一个执行的一定是 comp 的首个 reducing function 参数。故:

  1. xform 作为组合的前提
  2. 执行顺序从左到右;
  3. + 作为 reducing function 最后执行;

Monad

什么是Monad呢?A monad is just a monoid in the category of endofunctors.

  1. Identity—For a monad m, m flatMap unit => m
  2. Unit—For a monad m, unit(v) flatMap f => f(v)
  3. Associativity—For a monad m, m flatMap g flatMap h => m flatMap {x => g(x) flatMap h}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    // java8 实现的 9*9 乘法表
    public class ListMonad<T> {
    private List<T> elements;

    private ListMonad(T elem) {
    this.elements = singletonList(elem);
    }

    private ListMonad(List<T> elems) {
    this.elements = elems;
    }

    public <U> ListMonad<U> flatmap(Function<T, ListMonad<U>> fn) {
    List<U> newElements = new ArrayList<>();
    this.elements.forEach(elem -> newElements.addAll(fn.apply(elem).elements));
    return new ListMonad<>(newElements);
    }

    public <X> ListMonad<X> uint(X elem) {
    return new ListMonad<>(elem);
    }

    public <U> ListMonad<U> apply(ListMonad<Function<T, U>> m) {
    return m.flatmap(this::map);
    }

    public <U> ListMonad<U> map(Function<T, U> fn) {
    return flatmap(t -> uint(fn.apply(t)));
    }

    public static void main(String[] args) {
    ListMonad<Integer> m = new ListMonad<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));
    ListMonad<Integer> m1 = new ListMonad<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9));

    ListMonad<Integer> list = m.apply(m1.map(x -> y -> x * y));
    // [1...81]
    }
    }

表达式优于语句

S表达式

  1. 原子,或者;
  2. 形式为 (x • y) 的表达式,其中x和y也是S表达式。

举个例子,递增一组数据,过滤奇数,然后进行排序,最终取出第一个。如果取不到,返回:not-found

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(-> [1 2 3] 
(->> (map inc)
(filter odd?)
(sort)
(first))
(or :not-found))
; -> 3
(-> [1 1 3]
(->> (map inc)
(filter odd?)
(sort)
(first))
(or :not-found)
; -> :not-found

当然你也可以写成

1
2
3
4
(if-let [r (first (sort (filter odd? (map inc [1 1 1]))))] 
r
:not-found)
; -> :not-found

其实两者都是S表达式,但是下面的写法更加偏向于语句。从串联起来读来讲,前者明显是由于后者的。这要是放在其他函数式语言上,效果更加显著。比如下面重构if-else控制语句到Optional类型。

if-else -> Optional

1
2
3
4
5
6
7
8
9
10
11
12
Optional<Rule> rule = ruleOf(id);
if(rule.isPresent()) {
return transform(rule.get());
} else {
throw new RuntimeException();
}

public Rule transform(Rule rule) {
return Rule.builder()
.withName("No." + rule.getId())
.build();
}

这是典型的语句可以重构到表达式的场景,关键是怎么重构呢?
第一步,调转if

1
2
3
4
5
6
7
Optional rule = ruleOf(id);

if(!rule.isPresent()) {
throw new RuntimeException();
}

return transform(rule.get());

第二步,Optional.map函数

1
2
...
return rule.map(r -> transform(r)).get();

第三步,inline transform函数

1
2
3
4
...
rule.map(r -> Rule.builder()
.withName("No." + r.getId())
.build()).get();

第四步,Optional.orElseThrow函数

1
2
3
4
5
...
rule.map(r -> Rule.builder()
.withName("No." + r.getId())
.build())
.orElseThrow(() -> new RuntimeException());

第五步,注if释语句中的throw new RuntimeException()

1
2
3
if(!rule.isPresent()) {
// throw new RuntimeException();
}

这时候发现语句中为空,即可将整个语句删除。可以考虑inline rule

1
2
3
4
ruleOf(id).map(r -> Rule.builder()
.withName("No." + r.getId())
.build())
.orElseThrow(() -> new RuntimeException());

完毕。

我们认识事物的方式

  1. 把几个简单的想法合并成一个复合概念,从而创造出所有复杂的概念。
  2. 简单的或复杂的两种思想融合在一起,并立即把它们联系起来,不要把它们统一起来,从而得到它所有的关系思想。
  3. 把他们与其他所有陪伴他们的真实存在的想法分开:这就是所谓的抽象,因此所有的一般想法都是被提出来的。

推荐的书籍

  1. 逻辑的引擎
  2. 函数式编程思维
  3. 算机程序的构造和解释
    推荐的书籍

参考资料

  1. 图灵停机问题
  2. 康托尔、哥德尔、图灵 - 永恒的金色对角线
  3. Y combinator in Clojure
  4. 希尔伯特的23个问题
  5. 再谈哥德尔不完备定理
  6. wiki 函数式编程
  7. lambda 演算
  8. History of functional programming
  9. 函数式编程的早期历史
  10. 走进计算机文化史
  11. SICP
  12. Lambda Calculus and the Decision Problem

人类互相交流的欲望从未减弱过。

作者和书

这本书是汤姆·斯坦迪奇(Tom Standage)于1997年完成,1998年9月第一次出版。中文版是后浪|江西出版社于2017-8年出版。作者毕业于牛津大学,主修工程学和电脑科技,在《经济学人》杂志科技版担任主编。出版过的图书作品有《从莎草纸到互联网》、《六个瓶子里的世界史》等,本书是其代表作和畅销之作,已被拍成纪录片。

电报简史

脑图见文末。本书的概述请见简友半目李《当时的明月换拨人看》

我们沿着时间之线溯回到故事源头。1746年法国著名科学家、修道院院长诺莱做了一个电流传导的实验,成功证明了电流可以完成长距离即时传播。可是在那之后漫长的一百年,电流传播信息却并未得到发展,反而是查普发明了感观电报(一种通过望远镜观察电报塔机械臂获取信息的技术)和“电报”(telegraphe 远方的书写者)这个词。1793年法国建成第一座电报塔。次年,法国国家电报系统的第一条支线巴黎-里尔落成,法国的观感电报网由此形成。

时势造英雄。1832年,从欧洲回美国的萨利号船上,塞缪尔·F.B.摩尔斯听到同船的科学家提到了电流可以即时传播,灵感之下,想到了电报的无限可能,短短数日,就创造出摩尔斯电码,而这,几乎是整个电报行业的基石。四年之后,1836年,大洋彼岸的威廉姆·福塞吉尔·库克同样意识到电报的巨大前景。随后的两年,两个人在电学科学家的帮助下,都独立解决了流量远距离传导的难题。而摩尔斯更进一步改进了摩尔斯电码,让它可以直接编码字母。技术难题攻克后,1837年,库克和惠斯通合作建立第一条依附于铁路公司的电报线;1844年,摩尔斯说服政府建立了一条华盛顿-巴尔的电报线。1845年,几乎同时,摩尔斯和库克各自建立了磁力电报公司和电力电报公司。自此欧洲各国和北美的电报网日益发达起来。

各国自家的电报网四通八达之后,国与国之间沟通的诉求日益迫切。1849年3月份,普鲁士和奥地利签订了第一个国际电报互联协议,虽然基于类似香港海关的别扭形式,但是维也纳和柏林这两座城市终于可以互通电报了。随着水下铺设电报线技术的成熟,隔着英吉利海峡相望的英国和法国于1852年完成第一封从伦敦到巴黎的电报通信。而随之富贵的是那些生产古塔胶的公司,因为水下铺设的电线需要古塔胶的保护。1858年8月5日,历史性的时刻来临了。长约3280公里的大西洋海底电缆铺设完成,这意味着欧洲和北美洲的电报网络第一次连接成功。尽管由于技术问题,这条线路服役不到一个月就完全瘫痪。1866年,经过充分科学的试验和前车之鉴的经验积累,跨大西洋电报网络再次连接。这次,它真的可以在两个大洲之间传递各色信息,包括商业贸易,军事情报还有新闻等。

不过很快,电报在超载的信息面前开始显露出疲态。由于需要发送的电报太多,发报员根本忙不过来,导致大量电报堆积,以至于人们惊讶地发现在同城使用电报,通信的速度竟然不如信差。为了节省电报的带宽,英国的发明家克拉克发明了基于蒸汽的气动传送管,利用蒸汽的推力将装有电报卡的小盒子快速地发射到目的电报中心。这看似滑稽的设计,却实实在在地解决了电报带宽不足的问题。19世纪70年代,全世界都在兴建电报网络。维多利亚时代(1837-1901)的互联网在这段时间里初具规模,它混杂着各国的电报网络,海底光缆,气动管和跑腿信差。

英雄时代也会落幕的。1871年10月,美国举办了摩尔斯电报大游行,庆祝这位80岁高龄的老人为全人类通信所做的贡献。我们的电报之父——摩尔斯庄严地用摩尔斯电码敲下了自己的名字,正式告别电报界。次年逝世。

大师的陨落,宛若宣告一个时代的终结。电报以及整个电报行业开始退出历史的舞台。1872年,波士顿的约瑟夫·B.斯特恩斯发明了双工器,可以用一条电报线完成收和发的动作。1874年,博多机将一条线路的容量扩展到原来的12倍。同年,爱迪生发明了四工电路。1876年,亚历山大·格雷厄姆·贝尔通过对谐波电报的研究,发明了电话机并申请了专利。19世纪80年代,电学热持续升温,电报行业以肉眼可见的速度衰退。1903年,英国发明家唐纳德·莫里发明了电传打字机,这几乎宣判了电报员的“死刑”。自此之后,电报就消失在了人们的视野里。2006年,美国的西部联合公司宣布停止办理一切电报业务,以后恐怕只能在博物馆里才能追忆到那段辉煌的历史。

电报的思考

到底是技术带来了社会范式的改变,还是人类的诉求本身?

开篇提到过人类总是渴望互相交流,查普发明感观电报绝非偶然,本质上是察觉到人想和远距离的其他人即时交流的诉求。所以是诉求本身带动了技术创新。当技术创新成功发展之后,首先是富人阶层最先享受到成果,资本的罪恶本质是压榨了普罗大众的劳动时间得来了剩余价值。这些剩余价值让富人免去工作,把自己的时间消耗在这些技术创新激发的原始诉求上。然后,商人出于无利不起早的心态,会极力追求更低成本的技术创新,在市场竞争下,用低廉的价格吸引穷人进入资本家新一轮的资本累积当中。商业从根本上就是趋利避害的。

当技术的创新在一些方面节约了人们的时间,人们必然会将这部分时间浪费在其它方面或者干脆全部沉浸式地浪费原来那部分事情上,比如:网上聊天。我们之所以会有社会范式改变的感觉,就是由于我们自己原来的生活节奏被打乱了。以前是不得已而为之,现在是得已而不为之或者为之,突然一下拥有了选择的自由,算是很合情合理的设定。用系统性思考模式,人类及其活动是个大系统,本身就具有自组织的特征,所谓自组织就是让自己变得复杂的能力。远距离的交流变得快捷,意味着系统内部的信息连接更多更紧密,原因是速度变快了,同样的时间内可以和更多人建立关系,同时交流也会更加频繁。这样的带来的影响是什么呢?那就是人类群体变得更加复杂,外在行为更加诡异,也越发难以被消灭。

回到问题本身,这个问题就不应该用线性思考方式思考。人类的诉求激发了技术创新,技术创新又改变了人类的生活,激发了更进一步的诉求。说到底就是形成了一种增强回路,社会范式不过是增强回路自然而然表现出的群体的一致的外在行为模式。

电报网络和20世纪末的互联网有什么关系?

抛开技术本身不谈,电报网络和互联网都承载了同样的目标——让人类交流更加便利,本质上没有区别。常说电子邮件是互联网上的杀手级应用,我看不见得,170年前的电报早就具备了这样的能力,甚至“邮箱地址”这种也早就出现了。我觉的互联网真正的厉害之处,在于极大地丰富了信息的表现方式,利用超媒体链接技术,实现了多种类型信息(文本,照片和视频)的互联,还有基于这些信息上层抽象——服务(保险,云服务等等)。再加上计算机和移动设备这类载体的普及,用户群暴增。时下,如果不用互联网可能会被归为异类。

下一个爆发的是什么网呢?

我们分析一下互联网是个什么东西。互联网的要素有虚拟的人,虚拟的团体或公司,信息,网络,数字货币等。互联网上除了通讯和智库这些基本的要素外,还有由此构建的各种商业服务,如:阿里巴巴的电子商务帝国。当然还有游戏这种天然电子消费品。信息交互的速度足够快了,信息的载体也非常丰富,设备也在不断革新贴近人类。那么还有什么可以改善的呢?从历史发展的角度看问题,真正革新的技术从来都不是拿着颠覆什么商业模式的口号打出名声的,它的出发点一定是帮助解决人类的原始诉求。

思考中…

答案会是区块链吗?

于2018年2月4日

相关资料

维多利亚时代的互联网


[1]从莎草纸到互联网:社交媒体2000年
[2]六个瓶子里的世界史

维多利亚时代的互联网.png

世界是普遍联系、永恒发展的,这是我很欣赏的一名大学马原老师奉为圭臬的话,也是给我很大触动的金玉良言。世界是一个大系统,其中有纷繁复杂的事物,用独特的行为方式互相影响,或直接或间接,要么直截了当因果相连,要么兜兜转转蝴蝶效应。如果持不可知论,世界将永存混沌。系统总是比看上去复杂,但是其中玄妙又遵循因果。依照系统思考的基本原则,系统的行为总是由系统的结构决定。我们不愿意看到的很多现象,归根结底都是系统性问题,是系统的内部结构决定的行为特征。这是一个很重要的问题。我们只有正视并承认这一点才有重塑系统的勇气和可能。

系统结构是不胜枚举的,但是我们总可以抽象出模式(系统基模 archetypes)提取特征。不论是还原论还是整体论,只要能帮助我们分析问题都是好的理论。在分析过程中,使用还原论分解系统的元素,然后把这些元素放回原位,互相关联起来,组合勾画出系统的反馈回路。从整体论的视角重塑系统,思考反馈回路又会产生怎样的行为。系统思考的研究者将特定的、会引发特定行为的系统结构成为系统基模,常见的8种系统基模陷阱分别是政策阻力、公地悲剧、目标侵蚀、竞争升级、富者愈富、转嫁负担、规避规则和目标错位

可我们不禁想问:系统思考究竟是什么?要回答上面的这个问题,首先得搞清楚系统是什么。系统是一组相关联的事物——在一定的时间内,以特定的行为相互影响。系统可能会受到外力的影响,对此产生的反馈方式就是系统的特征。

系统思考的观察方式并不是唯一的解释系统的方式。就像康德说的,人都是戴着有色眼镜生存的,不同的观察方式或许可以突破这一层有色眼镜,使用投影的方式在多个维度综合塑造起系统的真实模样。我甚至希望自己看待同一件事物的视角是相互矛盾的,那样我才会感受到自己的认知是多么有限,这个世界是多么伟大、在有限的生命中充满怎样无限的可能。

从还原论的视角认知的世界是由基本要素构成的,但是系统思考则不同。它的基本理论是,系统由要素、连接,功能或目标组成。一支笔是系统,它的组成要素有笔芯、油墨、笔筒和笔头。笔筒套着笔芯,笔芯镶着笔头,油墨会沿着笔芯和笔头流淌,这就是连接。一支笔的功能可能是写字或画画,然而这些功能是很难从系统内部联想到的,必须让一只手攥住它运作起来,让油墨勾连出运动的线条,力透纸背,入木三分,才能观测出实际的功能。有时候,系统的要素是显而易见的,总能应需要分解,但是分解的粒度却是极难把握的,再加上要素可能是无形的,那么就极有可能找不出所有的要素。这个时候,把注意力转向连接则是更为明智和恰当的选择。系统连接的表现形式多种多样,可能是物质流、信息流,准入条件或约束规则,交易、交谈。在现实中,连接多表现在系列动作上,球员之间更加信任、老师给学生打分等等。如果系统中的连接发生改变,系统会受到很大的影响,此后表现出的行为和原来的行为大相径庭。而犹胜这个的,便是系统的目标或者功能发生改变。因为系统有自组织的特点,目标发生的改变会强制系统内部要素应激,最终会导致结构发生改变,从而表现出令人咂舌的行为。

当我们理解系统不仅仅是要素的集合,而且包含内在连接和功能或目标了之后。接下来就得接受系统是动态变化的这一事实。存量是所有系统的基础,存量是任何时候都能观察、感知、计数和测量的系统要素。存量是对系统中变化量的一种历史记录。那么也就是说存量总是会随着时间变化而改变的,而使存量发生变化的就是流量,流量可以看做瞬时的存量。一旦这样理解系统的结构,系统的动态性也就不言而喻了。存量的改变决定了系统的动态变化速度,也让系统具备了延迟性的特点——在任何环境下,系统都不会马上受影响;即使想要改变系统的行为,也需要一定的时间等待它缓慢生效。对存量的改变是通过控制流量做到的。进入的流量大于出去的流量,系统的存量就会增加;反之,会减少;最理想的情况是存量维持在一个动态的平衡状态。如果我们想要改变系统的行为,就需要找到系统流量的控制点,促进或者削弱控制点(手段),以此达成我们想要系统表现出来的行为或者趋势。

系统根据存量的多少,又可以分为单存量和多存量系统。单存量系统因为只有一个存量,控制点数量较少,系统内在连接较少,所以控制起来不是太难。但是即便如此,我们也要意识到由于客观规律的约束,造成流量的不恒定,系统最终态或多或少会偏离预定的值。而复杂的多存量系统,则会因为存量之间会相互施加对控制点(包括自己的控制点)的影响,变得错综复杂,难以理出头绪。所以更好的梳理方式是观察现在系统结构包含哪些行为,以及触发这些行为的条件。这有点类似知果索因的探究方式:行为就是对控制点施加的影响,而条件则是改变存量的外在表现。比如喝咖啡这样的行为。触发这个行为的条件是我困了。困了是体内能量低于正常水平的外在表现。而能量在这里可以看做存量。

系统思考的3大特征

承认系统是美的,这是我们研究系统的动力。假如一个系统整体是良好的,那么每个部分都是好的。

系统具有适应力(Resilience)的特征。适应力指的是系统在多变的环境下保持自身存在和运作的能力,与之相对的是脆弱性或刚性。或者用KK的话说,适应力就是反脆弱性。面对周遭环境的不确定性,系统会表现出短期振荡、阶段性发作和周期性兴衰,适应力会参与其中让系统振荡收敛,复原。在系统正常运行的情况下,适应力是很难被察觉到的,而系统的稳定程度则比较容易统计出来。适应力在系统超出限度,调节回路被破坏,要素被分解的情况下才能被观察。这也就要求我们在设计系统的时候,不仅要考虑到系统正常运行时的指标,也要考虑到极端情况下,系统自我恢复的能力。系统的适应力不是凭空产生的,它是多个调节回路共同影响之下出现的结果。而且复杂的系统一般都有元调节回路(meta-resilience),甚至是元元调节回路(metameta-resilience),这就让系统具备了很强的自组织能力。

系统的第二个特征就是自组织(Self-orgnization)。自组织指的是系统让自己更为复杂化的能力。自组织往往伴随着被扼杀的动作,主要原因是自组织具有不可预料的特质,引导系统发展出全新的行为模式和系统结构。面对可能的不确定性,现在的人们会感到恐慌,其结果采取打压的态度。但是庆幸的是适应力和自组织是系统的基本特征,不可能被消灭。和适应力类似,可能是很多简单的规则逐步产生系统的自组织能力的。比如,现实中的雪花分形,还有生命游戏(game of life)。

系统的第三个特征是层次性(Hierarchy)。层次性指的是系统和子系统之间包含和生成的关系。子系统能够维持自身,并发挥一定的功能,并服务于一个更大系统的需求,而更大的系统负责调节和强化各个子系统的运作,那么就可以产生并保持相对稳定、有适应力和效率的结构。系统的层次性一般是自下而上进化的,上层的目的是服务于下层的目的,而不是牺牲多数人的目的以维护少数人的目的。层次结构要求整体优化,不能让某个子系统的目的占据上风,也不能有太多中央控制。这就意味着层次结构必须平衡整体系统和子系统的关系。

系统思考的6大障碍

  1. 别被表象迷惑
    不要太关注事件本身,而是得关注系统的长期行为趋势,和触发这些行为的条件,这有助于帮助我们梳理出系统结构。而系统结构是行为的根源。
  2. 在非线性的世界里,不要用线性的思维模式
    线性系统可以被模块化,但是非线性系统通常是不可解、不可拆分的。非线性关系之间的相对优势发生改变,会导致不同回路的主导地位发生改变,导致千奇百怪存量的改变。
  3. 恰当地定义边界
    世界万物是互相联系的,不存在孤立的系统。所以边界的划分也应该依据我们的需求和目的。过窄导致对影响因素分析不足,而过宽要致使信息噪声过大,反而难以找出关键要素。
  4. 看清各种限制因素
    限制是客观存在的,它会限制系统的输入和输出。而且限制本身还是动态的,系统也可能是自身的限制因素。从某种程度上说,找到限制或约束力最大的因素是系统得以生存的基础。可能我们从来没让系统进化的更好,只是不断地打破限制因素,系统自己的特征决定了进化方向。
  5. 无所不在的时间延迟
    系统中的延迟无处不在,为了更好地处理问题,一定的预见性必不可少。不然会错过解决问题的黄金时间。
  6. 有限理性
    我们不得不承认即使知道了全部信息,我们也无法做出完全合理的决策。只有快速构建反馈,可视化结果,让人知道做了决策之后的后果,行为就能发生转变。

系统思考的8大陷阱

  1. 政策阻力
    其中政策阻力表现的是哪儿不痛快堵哪里,病急乱投医,治标不治本。
  2. 公地悲剧
    公地悲剧表现在对公有资源的滥用,由于个体遭受的损失由全体承担。
  3. 目标侵蚀
    目标侵蚀是说关注在系统的坏的表现上,产生降低目标、减少修正行为进而加剧的恶性循环。
  4. 竞争升级
    竞争升级就是囚徒困境中的先发制人策略的概述,直到一方退出或者资源耗竭为止。
  5. 富者愈富
    富者愈富指的是为富不仁的道理,富人会刻意不去承担责任。
  6. 转嫁负担
    转嫁负担是指上瘾行为,依赖外部干预者,而导致系统自身解决问题的能力变弱。
  7. 规避规则
    规避规则是说上有政策,下有对策。
  8. 目标错位
    而目标错误最严重,在错误的目标下,任何回路都只会导致无用的结果。

改变系统的12杠杆点

  1. 数字
    关注系统参数是一种低效的杠杆点,可能短期内有效,但是长期看来不起决定性作用。
  2. 缓冲器
    存量意味着系统比较稳定,但是换个角度看,也可能是一种浪费。而且因为是物理实体,通常不易调节。
  3. 存量-流量结构
    类似于缓冲器。不过这是系统的内在结构(不仅包含存量),因为是物理实体调节起来不太容易。
  4. 时间延迟
    承认系统具有延迟的特点,时间延迟无法消除,也就要求我们顺势而为,配合系统的改变节拍, 放慢增长速度。
  5. 调节回路
    为了改善系统的自我矫正能力,需要增强调节回路的力量。比如:保持信息公开透明。
  6. 增强回路
    减少增强回路的产生成果,可能是更加有力的杠杆点。
  7. 信息流
    从人心的角度看,人们避免对自己的决策负责。信息流的缺失是系统功能不良的常见原因之一。恢复信息流,是比重建系统结构更加经济的方式。
  8. 系统规则
    系统的规则会让系统的行为产生翻天覆地的变化,如果规则倾向于维护一小部分人的利益,减少了来自其他人的反馈,就会触发“富者愈富” 的陷阱,导致自我毁灭。
  9. 自组织
    自组织是进化的机制,而它需要一些原材料,即多样性。消除了多样性,世界将会陷入灾难。
  10. 目标
    很多人身处系统之中,也无法得知系统的目标。系统的目标是一个高杠杆点,而它的目标很大程度上是让自己无限增长。那就需要一个更大的系统把维持平衡作为自己的目标。
  11. 社会范式
    社会公认的观念就是社会范式,这些观念不需要特地强调就能被人所认同。改变社会范式的难度是巨大的,但是你可以积极接触那些拥抱新范式的人,参与宣传当中,避免接触反对这些新范式的人。而且,往往在构建系统模型的时候更容易改变范式,因为你跳出了系统本身,把它作为一个整体来观察了。
  12. 超越范式
    意识到范式也是人类看待这个世界的一种模型罢了。不要纠结真理是否存在,假设所有的东西都可能是错的,为了完成目标,只要去选择合适的手段就好了。

系统的15大生存法则

  1. 跟上系统的节拍
    先去了解系统的真实状况,让事实说话,然后用系统性思考动态地分析问题。
  2. 把你的心智模式展现在阳光下
    用科学的方式检验自己的心智模式,包括价值观。
  3. 相信、尊重并分享信息
    信息是系统运作的重要连接 ,控制信息甚至就是控制了整个系统。比如:政治家办报。
  4. 谨慎使用语言,并用系统的概念去丰富语言
    只有可以被谈论了,那东西才会火(发展),比如:微服务。
  5. 关注重要的,而不总是可以衡量的
    容易衡量的东西一般不会被忘记;但是不容易衡量的东西,比如:质量、幸福感等等就很容易被遗漏,如果设计时不去考虑这些,人们就不会关注,更不会改进。
  6. 为反馈系统制定带有反馈功能的政策
    把学习融入管理当中,构建调整回路的回路。
  7. 追求整体利益
    铭记层级组织存在的目的是服务于底层的。不要过度放大任何部分的重要性。
  8. 聆听系统的智慧
    存在的系统自然有它存在的理由,发挥它现有的自我运行的力量和结构是最好的方式。
  9. 界定系统的职责
    职责不是功能。也不是软件设计方法中单一职责表达的变化。职责就是责任,怎么增强系统的内在责任,就必须让每次决策及其结果之间建立起反馈回路。让决策者很快看到后果,意识到结果的严重程度,这才是优化决策最直接的方式。
  10. 保持谦逊,做一名学习者
    不要假装自己是专家,虚张声势是无法改进自己的方式,因为它只能隐藏问题。面对不确定性,我们要做的不是假装自己知晓一切,还是应该“拥抱失误”,这意味着我们需要搜索、使用和分享“我们到底在哪里失误了”这样的信息。
  11. 庆祝复杂性
    这是一种思考方式的转变。系统的复杂性从来不是问题,没有谁可以控制它,它恰是多样性和统一性的条件,所以这个世界才如此异彩纷呈。正如土地理论(Land ethic)所说“当某件事情倾向于保护生物群落的一致性,稳定性和自然之美,它就是对的,否则就是错的”。鼓励自组织、无序、变异和多样性才是我们应该做的。
  12. 扩展时间的范围
    学习过去的经验,面向未来解决现在的问题,不然会很快会崩溃。
  13. 打破各种清规戒律
    系统思考要求“跨领域”思考,正如区块链技术融合分布式原理、加密学理论、博弈论中的理性经济人那样,跨领域的思考模式会产生颠覆性的发明。
  14. 扩大关切的范围
    世界是普遍联系的,没有任何系统是孤立存在的。你关切的东西,不应该仅仅是自己。“各扫自己门前雪,何管他人瓦上霜”是短视的表现。
  15. 不要降低“善”的标准
    世界上充斥着奇葩猎奇的新闻,因为这些新闻是大都涉及偏离现有的社会范式的故事,人们对这个世界上的坏消息总是更容易相信些,这些坏消息成为了“目标侵蚀”的佐证,从而很轻易地丧失自己自幼学习的普世价值观,转而“同流合污”。

系统思考的方式能引导我们看清问题的本质,这已经是莫大的帮助了。至于做或者不做,这是个人的选择权利,在人类精神的系统关照下,我们应当知道什么该做,什么必须去做。

特征

  1. 日常语言描述
  2. 捕获系统行为
  3. 个数有限

在故事基础部分,我提到用户故事通常是日常或者商务语言写成的句子,这些句子描述了用户在其工作职责范围内想要达成的某个目的以及达成该目的需要的功能(手段)。所以书写用户的故事的句式一般都是:As(用户的角色)… I Want(功能或手段)… So That(目的)。根据用户故事的 INVEST 划分原则中 N (Negotiated 可协商的) 原则,故事包含的是对需求的简短描述,具体的细节需要沟通产出,产出物表现为验收条件。

换言之,验收条件是在开发前的分析阶段输出的,它的作用是补充需求细节。更进一步,验收条件其实有力地消除了用户和开发人员之间的沟通鸿沟。为什么这么说呢?因为验收条件具备两点很重要的特征:

  • 日常语言描述
  • 捕获系统行为

这两点特征促进了参与各方在需求点上快速反馈,如下图:
验收条件-反馈环
在敏捷活动中高效地沟通一直被反复强调,因为不高效的沟通造成的信息误导和返工是精益生产活动中应当极力消除的,所以任何能够促进沟通的方式方法都值得提倡。

除此之外,有限的个数 (2-8 ACs/story) 也是验收条件的一个特征,这也是 INVEST 原则中 E (Estimable 可评估的) 所要求的。所以,也引出了验收条件的一个简明定义——用户故事的 DoD (Definition of Done)。也有人说,一组验收条件定义了用户故事的边界(Boundary)。如果任由用户故事自然“疯长”,范围无限放大,交付怕是遥遥无期。

验收条件会作为业务活动描述的一部分存在于用户故事中,一般会在开发之前准备就绪。在敏捷活动 kick-off 时,由业务分析师(BA)和开发人员(Dev),也可叫上质量保证师(QA)一起逐条澄清验收条件,以便保证开发之前达成共识,减少返工和浪费。在其它敏捷活动如:desk-checks, customer sign-off, UI testing, BDD 中也会重度参与。

格式

Given/When/Then

#Title
Given 用户触发操作之前处于的系统状态
When 触发系统结果的操作
Then 系统预期返回的结果

Verifiable checklists

e.g. [PhoneNumber] 只能包含0-9, +

反模式

模棱两可的陈述

1
2
3
4
5
Given: that I have the search page loaded

When: I perform a search

Then: the search results come back within a reasonable period of time

这里的 reasonable period of time 就是不可测试的,因为没有人可以决定什么才是 reasonable.

合理的改法是:

1
2
3
4
5
Given: that I have the search page loaded

When: I perform a search

Then: the search results come back within 5ms

5ms 之内,这是一个标量,完全可以衡量。

非系统交互

1
2
3
4
5
6
7
Given I have pulled my car up to the valet area

When I hand over my keys to the valet person

Then I receive a paper ticket from the valet person

And I am instructed to hold on to it so I can use it to retrieve my car later.

这里的所有描述都是对现实场景的描述,和系统并无关系,对于开发人员构建系统几乎没有丝毫帮助。这种反模式的修正方法是剔除那些非系统的验收条件,重新梳理用户故事。

非系统输出

1
2
3
4
5
6
7
Given: that I am on the home page

And: I am logged in

When: I navigate to account preferences

Then: I can see my account preferences

这里的 I can see my account preferences 是无法进行断言的,因为这是系统无关的,说得极端些,假如用户闭上眼睛,这个功能就没法通过验收了。

合理的改法是:

1
2
3
4
5
6
7
Given: that I am on the home page

And: I am logged in

When: I navigate to account preferences

Then: my account preferences are displayed

这个时候,我可以检查系统展示了我的用户页面。

非系统异常

1
2
3
Given 我选择了一份图表模板
When 我跳转到显示界面
Then 我发现图表不是我想要的

这里的问题涉及了非系统输出没法进行测试等问题,还有一个显著的毛病是误认为用户自身的误操作也必须反映到用户故事中。这里出错的点是用户自己选错了模板,发现产生的图表不是自己想要的格式,系统是自动无法判别选择模板的正确性的。

when 隐藏到 given

1
2
3
4
5
6
7
Given: that I am on the homepage

And: I navigate to the search

When: I look at the page

Then: the search options are displayed

基本上,这是团队编写 AC 时最容易犯的错误,操作出现在前置条件中,when 中反而不是系统行为了。

合理的改法是:

1
2
3
4
5
Given: that I am on the homepage

When: I navigate to the search

Then: the search options are displayed

最佳模式

1. 可读的

我们希望业务人员审阅和修正验收条件,如果写的内容只有开发人员能懂,我们就失去了获得反馈的机会。使用上述书写格式,可以提高可读性。

1
2
3
4
5
6
7
8
9
Given: that my mobile phone is switched on

And: I have sufficient signal to make a call

When: I dial a number

Then: I am connected to the person I want to talk to

And: incoming calls are diverted to my voicemail

2. 可测试的

参见反模式中合理改法。

3. 实现无关的

验收条件应该是实现无关的,它和用户故事一样,是给业务和开发人员提供交流凭证的一种工具,所以它应该聚焦于功能,而不是功能的展现形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
Given: that I am on the home page

And: I am logged in

When: I navigate to advanced search

Then: the advanced search web page must be displayed

And: a text box labelled "Name" is displayed

And: a text box labelled "Description" is displayed

And: a command button named "Search" is displayed

这里已经框死了必须要使用 text box 实现展示功能,而实际上其背后真正的意图是通过属性字段进行搜索,隐藏了业务含义的验收条件是不可取的。

合理的改法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
Given: that I am on the home page

And: I am logged in

When: I navigate to advanced search

Then: the advanced search is displayed

And: an option to search by name is displayed

And: an option search by description is displayed

And: the advanced search is displayed in accordance with the attached wireframe

换句话说,验收条件本身不应该关注于展现形式,当然,为了便于理解,wireframe 是提供直观素材的更好的方式。

练习

用户故事

1
2
3
作为一名管理员
我想要把一名员工加入系统中
以便管理他们的权限

分析步骤

1. 定义边界

  • 触发添加员工操作
  • 输入员工的详情
  • 验证遗漏或者错误的字段
  • 保存

2. 提炼和细化

  1. 触发添加员工操作
    1
    2
    3
    假如我进入了员工管理系统
    当我进入员工的浏览页
    之后添加员工的操作出现在页面上
    2. 输入员工的详情
    1
    2
    3
    4
    假如添加员工的操作出现在浏览页
    当我调用了添加员工的操作
    那么我可以输入员工的姓名和出生日期
    并且出现了保存操作
    3. 验证遗漏的字段
    1
    2
    3
    4
    假如我没有填写员工的姓名和/或生日
    当我尝试保存
    那么保存不会成功
    并且会有消息显示遗漏的字段
    4. 验证错误的生日日期
    1
    2
    3
    4
    5
    6
    7
    8
    假如我正在添加一名员工的详情
    并且我输入了未来或者早于1900年的日期,或者错误的日期格式
    当我尝试保存
    那么保存不会成功
    并且会有消息显示输入的生日日期无效

    验证列表:
    [日期格式] yyyy/MM/dd
    5. 保存
    1
    2
    3
    4
    5
    6
    假如我正在添加一名员工的详情
    并且我输入了有效的生日和姓名
    当我尝试保存
    那么会有消息显示保存成功
    并且包含该员工详情的页面会呈现
    并且详情中的生日和姓名和之前输入的一致

警告

验收条件并不是唯一澄清和约束用户故事的方式!任何可以提升理解和降低沟通成本的方式方法都值得尝试。比如:用户偏好 —— 希望使用下拉框而不是复选框,往往可以通过添加一条记录在故事中补充这部分信息。另外,一个完整的故事最好能附上线框图,一图胜千言。


进一步阅读
[1] 敏捷团队工作流

站会

站会中的内容是每天工作的开始,也是对昨天工作的回顾。一般会由团队的某位成员主持,这位主持人有责任让电子系统上的story卡片和看板上的保持一致。站会上,大家依看板从右至左依次更新自己负责story的状态,如果遇到阻碍,应该在站会上及时提出,团队之中的成员如果能提供帮助,应该在站会之后,组织解决方案的讨论。

站会反模式

  1. 站会上由组长一人发言、分配任务;
  2. 一人长篇阔论占据大半时间(合理的发言时间为1-2分钟,总的时间控制在15分钟左右);
  3. 站会上谈论和story卡片无关的内容。

迭代计划会议 (IPM)

迭代计划会议是项目组成员在每一个迭代开始时,聚到一起共同决定这个迭代的 backlog (代办事项) 需要交付的故事卡。
这个会议的产出物包含:

  1. 迭代的 backlog 和用户故事的验收条件
  2. 这个迭代的总体业务目标
  3. 故事点数,也即开发测试人员对用户故事的评估及承诺

参与者包含:

  1. Product Owner
  2. Scrum Master
  3. 开发团队
  4. 其他干系人

日程:

  1. 团队确立这个迭代可以完成的工作量(以点数计),一般从历史迭代中获取
  2. 团队根据定义的顺序讨论故事卡,对于每一张故事卡,需要讨论的内容包含:
    1. 衡量故事的相对大小(以点数计),可能会进行故事卡的拆分
    2. 沟通验收条件
    3. 根据已经讨论出来故事卡的价值、时间和风险,PO重新确定故事卡的优先级
    4. 一旦预先设定的工作量到达,就停止本轮迭代会议
  3. 团队就迭代的业务目标达成共识

用户故事 kickoff 会议

Story kick off,指的是启动一个 Stroy 进入开发阶段。Story kick off的时候,通常需要三种角色参与:BA、QA 以及开发 Story 的 Dev。

会议内容:

  • DEV 自己先完整地过一遍 Story 的描述;
  • DEV 给 BA 和 QA 去讲这个 Story 的功能以及 AC;
  • 要能够清晰的讲出来,并且三者达成一致,如果有疑惑,需要当场得到解决;
  • DEV 开始开发 Story,并自行将 Story 拆分成多个子任务列表,开工。

我在ThoughtWorks中的敏捷实践

Desk Check / Shoulder Check

Desk Check 发生在开发人员确信自己已经完成了故事卡,需要另外的人(BA、QA、Dev)坐到旁边一起帮忙验证是否确实完成。

Desk checklist:
参与者:BA、QA、Dev 或者其他感兴趣的人

  • 故事卡测试覆盖率够吗?
  • 自己本地测过功能么?
  • 自己本地跑过全量的测试么?
  • 所有的 AC 是否都满足了?
  • 做故事卡的过程中是否遇到了理解上的问题?

Defect Prevention Using Agile Techniques

Tech Huddle

开发人员聚到一起就项目中遇到的技术展开讨论
前提条件:
会议之前确定并逐条列出会议讨论的技术主题
产出物:

  1. 一个或多个行动方案,合并到用户故事
  2. 方案的优缺点
  3. 方案的责任人

迭代回顾会议

在每个迭代结束时,Scrum Master会主持该会议,目的是为了确定哪些改变可以提升下个迭代的生产效率。在这个会议当中,每个人都可以开诚布公地提出自己的建议,有利于管理者从反馈中获取团队的现状。

最高指导原则 (Prime Directive):

Regardless of what we discover, we understand and truly believe that everyone did the best job he or she could, given what was known at the time, his or her skills and abilities, the resources available, and the situation at hand.
无论我们发现了什么,考虑到当时的已知情况、个人的技术水平和能力、可用的资源,以及手上的状况,我们理解并坚信:每个人对自己的工作都已全力以赴。

内容包含:

  • 上个迭代中做的好的有哪些?
  • 上个迭代中做的不好的有哪些?
  • 我们可以做些什么改善哪些不好的地方?(改善建议)

参与者包含:

  1. Product Owner
  2. Scrum Master
  3. 开发团队
  4. 其他干系人

高效回顾会议的七步议程

迭代review会议 (Showcase)

Showcase 就是开发团队把开发好的功能给客户的 Product Owner(以下简称PO)等业务相关人员演示,以获取他们对所开发系统的反馈,是敏捷开发流程中的一个实践,一般的频率是一个迭代一次,也可以根据项目具体情况做调整。

内容包含:

敏捷实践Showcase的七宗罪
Retro 破冰游戏

用户故事

故事基础

用户故事是指在软件开发和项目管理中用日常语言或商务用语写成的句子。这个句子反映一个用户在其工作职责的范围内要达到的某个目的, 以及此目的所需要的功能。

例子

为了避免遗漏附件延误工作
作为邮件发送者
我希望邮件系统能够在我忘记带附件的时候提醒我

格式

1
2
为了<某个目的或价值>, 作为<某类利益相关者>, 我想要<某个功能>
作为<某类利益相关者>, 我想要<某个功能>, 以便<达到某个目的或获得某种价值>

要点

重点描述商业的价值不只是功能

  • 帮助团队了解需求背后的意图,利于开发团队协同客户、业务部门设计出更好的解决方案

用业务语言描述

  • 利于客户、业务部门理解并区分优先级

利益相关者不仅包含用户

  • 系统拥有者的角度也是需求的重要来源

CCC 组件 (The three ‘C’s)

Card (卡片)

  • 业务价值 remainder
  • 做计划和沟通业务时的 token

Conversation (交流)

  • 用于在做计划和沟通业务时引发沟通,制造共同话题(收敛)
  • 需求文档的生成方式

Confirmation (确认)

  • 将细节以验收测试的方式检测功能的完整性和准确性

划分原则 INVEST

Independent (独立性)

故事和故事之间尽量保持独立,互相依赖的故事对于估算工作量、确定优先级和安排计划都带来很多不便。通常我们可以通过组合和拆分的方式减少依赖性(去除重复)。
独立性更多的指的是实现要完整。前后端拆分通常不是很好的拆分方法

Negotiated (可协商的)

一个故事是可以协商的,故事卡不是合同,它只是包含对一个需求的简短描述。具体的细节在沟通阶段产出,以验收测试的方式。如果带有太多的细节,反而限制了和用户的沟通。

Valuable (有价值的)

每个故事都必须对客户有价值(无论是用户还是客户)。一个让用户故事有价值的好办法就是客户写下它们。

Estimable (可评估的)

开发团队需要去估计一个用户故事以便确定优先级,工作量,安排计划。如果难以估计故事的时间, 意味着:

  • 领域知识的缺乏: 这种情况下需要更多的沟通
  • 技术实现的模糊: 这种情况下要做试验, 做原型
  • 或者故事太大了: 这时需要把故事切分成小些的
  • 还有对其它团队的依赖…

Small (短小的)

一个好的故事在工作量上应该是尽量短小的,至少确保能在一个迭代或 Sprint 中能够完成。用户故事越大,在估算、计划安排等方面的风险就越大。

Testable (可测试的)

一个用户的故事必须是可被测试的,以便它是可以完成的。如果一个故事无法测试,那么就无法知道它何时可以完成。一个不可测试的例子:为了节省时间,作为用户,我希望软件是易用的。

判断标准 (经验准则)

  • 可以写在 Release Notes 里
  • 值得讲给其它行业的人听
  • 可以写在市场宣传材料中

验收条件

捕获预期的行为

一般验收条件都会在开发之前准备好,用于捕获预期的系统行为,同时作为故事卡业务描述的一部分,定义了故事卡的 DoD(Definition of Done)。

格式

  • Given 前置条件,告诉我们在进行操作之前,需要设置和完成什么;
  • When 触发结果的操作
  • Then 操作之后的预期结果

最佳实践模式

1. 可读的

我们希望业务人员审阅和修正验收条件,如果写的内容只有开发人员能懂,我们就失去了获得反馈的机会。使用上述书写格式,可以提高可读性。

1
2
3
4
5
6
7
8
9
Given: that my mobile phone is switched on

And: I have sufficient signal to make a call

When: I dial a number

Then: I am connected to the person I want to talk to

And: incoming calls are diverted to my voicemail

2. 可测试的

反模式1 - 模棱两可的陈述
1
2
3
4
5
Given: that I have the search page loaded

When: I perform a search

Then: the search results come back within a reasonable period of time

这里的 reasonable period of time 就是不可测试的,因为没有人可以决定什么才是 reasonable.

合理的改法是:

1
2
3
4
5
Given: that I have the search page loaded

When: I perform a search

Then: the search results come back within 5ms

5ms 之内,这是一个标量,完全可以衡量。

反模式2 - 非系统输出
1
2
3
4
5
6
7
Given: that I am on the home page

And: I am logged in

When: I navigate to account preferences

Then: I can see my account preferences

这里的 I can see my account preferences 是无法进行断言的,因为这是系统无关的,说得极端些,假如用户闭上眼睛,这个功能就没法通过验收了。

合理的改法是:

1
2
3
4
5
6
7
Given: that I am on the home page

And: I am logged in

When: I navigate to account preferences

Then: my account preferences are displayed

这个时候,我可以检查系统展示了我的用户页面。

反模式3 - when 隐藏到 given
1
2
3
4
5
6
7
Given: that I am on the homepage

And: I navigate to the search

When: I look at the page

Then: the search options are displayed

基本上,这是团队编写 AC 时最容易犯的错误,操作出现在前置条件中,when 反而不是系统行为了。

合理的改法是:

1
2
3
4
5
Given: that I am on the homepage

When: I navigate to the search

Then: the search options are displayed

3. 实现无关的

验收条件应该是实现无关的,它和用户故事一样,是给业务和开发人员提供交流凭证的一种工具,所以它应该聚焦于功能,而不是功能的展现形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
Given: that I am on the home page

And: I am logged in

When: I navigate to advanced search

Then: the advanced search web page must be displayed

And: a text box labelled "Name" is displayed

And: a text box labelled "Description" is displayed

And: a command button named "Search" is displayed

这里已经框死了必须要使用 text box 实现展示功能,而实际上其背后真正的意图是通过属性字段进行搜索,隐藏了业务含义的验收条件是不可取的。

合理的改法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
Given: that I am on the home page

And: I am logged in

When: I navigate to advanced search

Then: the advanced search is displayed

And: an option to search by name is displayed

And: an option search by description is displayed

And: the advanced search is displayed in accordance with the attached wireframe

换句话说,验收条件本身不应该关注于展现形式,当然,为了便于理解,wireframe 是提供直观素材的更好的方式。

代码评审

代码评审是团队针对今天一天提交过的代码的评审会议。这样做的目的是去除代码的坏味道,减少累积的技术债。

团队成员使用版本控制工具,轮流展示自己一天的编码成果并且说明代码的用途。这期间,其他的成员不仅要评审代码的逻辑是否正确(满足验收条件),还必须思考是否有更优雅的方式实现这段功能。大家可以随意表达自己对这些代码的好恶,代码所有者也可以据理力争,所有者一旦被说服就必须无条件地按意见修改。为防止遗忘,可以使用便笺纸记录下来。

结对编程

通俗地讲,结对编程就是两个人同时工作在同一个 Story 上,一起讨论 Story 的解决方案,并编写代码实现功能,一个人敲键盘,一个人看屏幕,穿插着进行。Pair的双方在快速敲击键盘的时候会伴随一些交流。能力相当的两人,可以一人写测试,一人写实现代码。

测试驱动开发 (TDD)

TDD,即测试驱动开发,强调的是测试先行,跟我们先实现功能代码后添加测试的过程恰恰相反。测试驱动开发是一种驱动代码实现和设计的过程。我们说要先有测试,再去实现;保证实现功能的前提下,重构代码以达到较好的设计。整个过程就好比演绎推理,测试就是其中的证明步骤,而最终实现的功能则是证明的结果。

实践方法:

  • Given:初始状态或前置条件
  • When:行为发生
  • Then:断言结果

单元测试和测试驱动开发

测试的价值表现在两个方面:

  1. 防止已有的功能被破坏
  2. 驱动出功能实现

从测试金字塔描述的层级来看,单元测试是基座。这类测试数量应该是非常多的,而且还有一个显著特征——运行速度特别快。一般违反快速这个标准的,基本上可以肯定这不是单元测试。

基于以往带团队的经验,我给出一些写单元测试时常犯的错误和好的实践:

常犯的错误

  1. 覆盖率100%,却没有任何断言
  2. 使用Java反射setAccessible(true)访问私有属性
  3. 依赖某些地方产生的数据才能运行
  4. 单元测试运行速度超级慢
  5. 开始测试之前,准备巨量的数据
  6. 测试之间必须以特定顺序运行
  7. 上帝类(God Object)导致数以千行的测试代码

好的实践

  1. 绝对不测试私有的方法
  2. 当方法没有返回值,可以通过测试异常或者其委托对象的方式
    1
    2
    1. @Test(expected = RuntimeException.class) 
    2. Mockito.verify(mockObj).doSomething(args…)
    3. 静态方法调用是强耦合的信号。需要重构待测类依赖新接口,新接口定义的方法由子类实现,实现方法中包装这些静态方法。

什么时候写单元测试?

  1. 新的需求,首先写单元测试(保证功能实现)
  2. 已经存在的代码,补充单元测试 (保证功能不被破坏)
  3. 发现Bug,首先加测试(保证缺陷不再出现)

自动化测试

测试金字塔和 DOT(Depth of Test)

持续集成/持续交付

再次强调一下,持续集成是一项团队务必遵守的实践。

开发过程中,我们鼓励小步提交的代码控制方式。即当开发人员对story的部分功能编码完成之后,如果确认可以提交到代码仓库(如果是分布式的Git,可以考虑提交到本地仓库),应该尽早提交。开发人员的每日工作可以遵循7步提交法,如下:

  1. 更新代码;
  2. 本地编码;
  3. 本地构建(使用mvn test);
  4. 再次更新代码;
  5. 本地构建;
  6. 提交到代码仓库;
  7. CI上构建。

既然是实践(规矩),就有几项有效的纪律需要遵守:

  • CI红的时候不能提交代码
  • CI红着不能过夜(不能等到第二天才去修复)
  • 任何人提交后CI失败,无论原因,都有责任跟进修复
  • 在提交代码前运行所有测试
  • 不注释掉或删除失败的测试

这也就意味着开发人员需要密切关注CI的状态。CI持续反馈着软件的可工作状态,所以团队应该把CI的健康状况列为项目的最高优先级,甚至高于开发新特性。


友情链接

  1. ThoughtWorks的敏捷开发-XR)

Clojars 介绍

Clojars 是一个为开源 Clojure 类库打造的仓库,截止2017年9月17日,大概有19831个项目发布在上面。整个网站也是用 Clojure 编写的。

发布 Clojure library

1. 注册 clojars

前往 clojars 注册

2. lein 部署

1
$ lein deploy clojars # lein deploy [repository name], here the repo name is clojars.

如果不想加上 clojars 参数,则需要在当前项目下的 project.clj 添加如下内容

1
2
{:deploy-repositories [["releases" :clojars]
["snapshots" :clojars]]}

这里注意一点:
这里使用 :deploy-repositories 而非 :repositories,原因是 :repositories 除了用于部署还会作为依赖源被项目使用。所以,如若必要,还是职责单一点好。这样,也可以加入 :user profile (~/.lein/profiles.clj) 供所有本地项目发布使用。

这个时候可以执行

1
2
3
4
5
6
$ lein deploy
No credentials found for snapshots(did you mean `lein deploy clojars`?)
# 这里应该是 leiningen 的 bug,提示信息 `lein deploy clojars` 显得很奇怪,可以忽略之。
See `lein help deploying` for how to configure credentials to avoid prompts.
Username: username
Password: password

如上,这时会弹出用户名和密码输入框。为了节省时间,避免每次输入,最好把用户凭证 (credentials) 以文件的形式存放到用户范围的目录下,即*~/.lein/credentials.clj*,并做加密处理。

3. 设置全局的 credentials map

首先,把如下的 credentials map 写入 ~/.lein/credentials.clj

1
{#"https://clojars.org/repo" {:username "username_on_clojars" :password "password_on_clojars"}}

其次,使用 gpg 加密该文件

1
2
$ gpg --default-recipient-self -e \
~/.lein/credentials.clj > ~/.lein/credentials.clj.gpg

加密后,即可删除原文件 ~/.lein/credentials.clj。然后在:deploy-repositories map 中指定 :creds :gpg

1
2
3
4
{:deploy-repositories [["releases" :clojars
:creds :gpg]]
["snapshots" :clojars
:creds :gpg]]}

完成上述,lein deploy 的时候即可解密 ~/.lein/credentials.clj.gpg,从中获取对应仓库的usernamepassword注:为了便于索引查找,credentials 使用正则表达式 #”https://clojars.org/repo“ 作为 key)

Error: gpg agent timeout

有时候,deploy 时会出现 gpg agent 超时的错误

1
2
3
4
5
6
$ lein deploy
gpg: problem with the agent: Timeout
gpg: decryption failed: No secret key
Could not decrypt credentials from /Users/qianyan/.lein/credentials.clj.gpg
nil
See `lein help gpg` for how to install gpg.

仔细搜索文档会发现下面这句很重要的话

Due to a bug in gpg you currently need to use gpg-agent and have already unlocked your key before Leiningen launches, but with gpg-agent you only have to enter your passphrase periodically; it will keep it cached for a given period.

大意是,leiningen 需要用到 gpg-agent,而且在 lein deploy 之前,就应该解锁密钥。

不实际操作的话,还是很难弄懂这句话具体的指代。我们不妨思考一下。

1. 看看后台是否有个进程叫做 gpg-agent?

1
2
$ ps -ef |grep gpg
501 87095 1 0 8:27PM ?? 0:00.00 gpg-agent --daemon

嗯,还真有!

2. gpg 直接解密 credentials.clj.gpg

1
2
3
4
5
6
$ gpg --decrypt ~/.lein/credentials.clj.gpg

You need a passphrase to unlock the secret key for
user: "Yan Qian (lambeta) <qianyan.lambda@gmail.com>"
2048-bit RSA key, ID E13DFD8A, created 2016-05-14 (main key ID 3C5030FF)
# 接下来,漫无止境的等待中...

这奇怪的等待让我不安,所以我使出了杀手锏 kill -9,直接把 gpg-agent 干掉。

3. 重新 gpg –decrypt

1
2
3
4
5
6
7
8
9
$ gpg --decrypt ~/.lein/credentials.clj.gpg

You need a passphrase to unlock the secret key for
user: "Yan Qian (lambeta) <qianyan.lambda@gmail.com>"
2048-bit RSA key, ID E13DFD8A, created 2016-05-14 (main key ID 3C5030FF)
# 这里要输入 passphrase
gpg: encrypted with 2048-bit RSA key, ID E13DFD8A, created 2016-05-14
"Yan Qian (lambeta) <qianyan.lambda@gmail.com>"
{ #"https://clojars.org/repo" {:username "username_on_clojars" :password "password_on_clojars"}}

终于可以输入 passphrase 了,解密完成。这大概就是上面引文所说的 unlock your key before Leiningen launches.

4. 重新部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ lein deploy
Created /Users/qianyan/Projects/clojure-pr/clj-moco/target/clj-moco-0.1.0-SNAPSHOT.jar
Wrote /Users/qianyan/Projects/clojure-pr/clj-moco/pom.xml
Retrieving clj-moco/clj-moco/0.1.0-SNAPSHOT/maven-metadata.xml
from https://clojars.org/repo/
Sending clj-moco/clj-moco/0.1.0-SNAPSHOT/clj-moco-0.1.0-20170917.122837-4.jar (10k)
to https://clojars.org/repo/
Sending clj-moco/clj-moco/0.1.0-SNAPSHOT/clj-moco-0.1.0-20170917.122837-4.pom (3k)
to https://clojars.org/repo/
Retrieving clj-moco/clj-moco/maven-metadata.xml
from https://clojars.org/repo/
Sending clj-moco/clj-moco/0.1.0-SNAPSHOT/maven-metadata.xml (1k)
to https://clojars.org/repo/
Sending clj-moco/clj-moco/maven-metadata.xml (1k)
to https://clojars.org/repo/

没有出现 gpg-agent timeout 的错误,部署完成。


提示
1
2
3
--default-recipient-self
Use the default key as default recipient if option --recipient is not used and don’t ask if this is a valid one.
The default key is the first one from the secret keyring or the one set with --default-key.

参考链接
[1] Leiningen Deployment
[2] GPG: How to change the configuration

太长不读篇

  1. issues tracker 上注册
  2. 创建 issues
  3. 配置 build.gradle
  4. gpg 生成 key pair 以便签名
  5. 上传 Release Archive
  6. 关闭并验证 Staging 环境的 Archive
  7. 发布 Archive
  8. 通知 issue 管理员开启同步

细读篇

1. 注册

Maven Central 网站并不提供注册的功能,你需要到 Sonatype 网站上进行注册。而事实上,Sonatype 网站也没有直接提供一个注册链接。真正的注册入口在 issues tracker 上。一旦完成注册后,你需要创建包含待发布包信息的 issue。

2. 创建 issue

创建 issues
在 Sonatype 的 dashboard 上点击创建按钮,根据弹出框的提示,填写简介、描述、GroupId、Project URL、SCM url 以及你在 jira 上的用户名。创建完毕后,会被自动跳转到该 issue 的详情页并分配一个唯一的ID,如:OSSRH-33944。余下的时间只需要等待,一般在两个工作日之内,Sonatype 的工作人员就会着手处理,然后他会在该 issue 底下的评论区留言。

创建成功的 issue

如果代码是托管在 github 上,按照惯例,GroupId 应该取 github 上的域名,比如:com.github.qianyan。不过,这里我预备上传的包的 GroupId 是 com.lambeta,这是我购买的域名。审核者对此有所顾虑,所以很贴心地留言如下:

Do you own the domain lambeta.com? If not, please read:
http://central.sonatype.org/pages/choosing-your-coordinates.html
You may also choose a groupId that reflects your project hosting, in this case, something like io.github.qianyan or com.github.qianyan

在回复这个域名确实为我所有之后,工作人员就贴出不同环境的仓库地址。

Configuration has been prepared, now you can:
Deploy snapshot artifacts into repository https://oss.sonatype.org/content/repositories/snapshots
Deploy release artifacts into the staging repository https://oss.sonatype.org/service/local/staging/deploy/maven2
Promote staged artifacts into repository ‘Releases’
Download snapshot and release artifacts from group https://oss.sonatype.org/content/groups/public
Download snapshot, release and staged artifacts from staging group https://oss.sonatype.org/content/groups/staging
please comment on this ticket when you promoted your first release, thanks

最后一句很重要,说的是,当我第一次正式发布的时候,需要留言告知工作人员,以便他们开启中央仓库的同步,这样我的包才会在 Maven Central 仓库中可见。

3. 配置项目的 build.gradle

拿到仓库地址,我们就需要在自己的项目中进行一些必要的配置,包含:jar、sourcesJar、javadocJar 以及对这些产物的 signing(签名)。

maven 插件

1
apply plugin: 'maven'

maven 插件提供了 uploadArchives task,我们需要在这个 task 中配置仓库地址,以及 pom 的相关信息,因为上载到 maven 仓库的包必须要有 pom 文件,否则无法查找或被依赖。具体配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
uploadArchives {
repositories {
mavenDeployer {
beforeDeployment { deployment -> signing.signPom(deployment) }

repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") {
authentication(userName: ossrhUsername, password: ossrhPassword)
}

snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots") {
authentication(userName: sonatypeUsername, password: sonatypePassword)
}

pom.project {
name project.name
packaging 'jar'
description 'underscore string in java'
url 'https://github.com/qianyan/underscore.string.java'

scm {
url 'https://github.com/qianyan/underscore.string.java'
connection 'https://github.com/qianyan/underscore.string.java.git'
developerConnection 'git@github.com:qianyan/underscore.string.java.git'
}

licenses {
license {
name 'MIT Licence'
url 'https://raw.githubusercontent.com/qianyan/underscore.string.java/master/LICENSE'
distribution 'repo'
}
}

developers {
developer {
id 'lambeta'
name 'Yan Qian'
email 'qianyan.lambeta@gmail.com'
}
}
}
}
}
}

对应生成的 pom.xml 大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<name>underscore.string.java</name>
<description>underscore string in java</description>
<url>https://github.com/qianyan/underscore.string.java</url>
<licenses>
<license>
<name>MIT Licence</name>
<url>https://raw.githubusercontent.com/qianyan/underscore.string.java/master/LICENSE</url>
<distribution>repo</distribution>
</license>
</licenses>
<developers>
<developer>
<id>lambeta</id>
<name>Yan Qian</name>
<email>qianyan.lambeta@gmail.com</email>
</developer>
</developers>
<scm>
<connection>https://github.com/qianyan/underscore.string.java.git</connection>
<developerConnection>git@github.com:qianyan/underscore.string.java.git</developerConnection>
<url>https://github.com/qianyan/underscore.string.java</url>
</scm>

注意 authentication(userName: ossrhUsername, password: ossrhPassword)authentication(userName: sonatypeUsername, password: sonatypePassword),这里的用户名和密码其实就是在 Sonatype 上注册的用户名和密码。为了让 gradle 脚本顺利执行,需要在当前工程下的 gradle.properties 文件中设置对应的变量,如下:

1
2
3
4
sonatypeUsername=
sonatypePassword=
ossrhUsername=
ossrhPassword=

这份文件会作为源代码的一部分提交,所以聪明的我们不会傻傻地把自己的用户名和密码 push 到 github 上面。和大部分 *nix 系统上的工具类似,gradle 也有本地配置,我们可以新建一份 gradle.properties 文件到 ~/.gradle/gradle.properties,然后把用户名和密码写入其中。这样,实际运行时,本地配置就会覆盖项目下对应的这些变量值。

4. 设置 gpg 以签名 Archive

gpg 生成的 key pair 主要是供签名使用的。假定本机已经安装 gpg,首先使用 gpg 生成 key pair。

1
$ gpg --gen-key

然后,查找你的 keyId:

1
2
3
4
$ gpg --list-keys
# ->
pub 2048R/XXXXXX 2017-09-14 [expires: 2018-05-14]
uid [ultimate] Yan Qian (lambeta) <qianyan.lambeta@gmail.com>

其中 XXXXXX 就是你的 keyId。接下来必须发布你的公钥:

1
$ gpg --keyserver hkp://pgp.mit.edu --send-keys XXXXXX

验证公钥已经发布成功:

1
$ gpg --keyserver hkp://pgp.mit.edu --search-keys qianyan.lambeta@gamil.com # user email address

当然,上述操作都可以使用 gpg tools 在 UI 上完成。

signing 插件

完成上述步骤之后,我们需要在 build.gradle 中添加 signing 插件及其配置,如下:

1
2
3
4
5
6
apply plugin: 'signing'

signing {
required { gradle.taskGraph.hasTask("uploadArchives") }
sign configurations.archives
}

上面的配置明确了 gradle task 的 DAG 中必须含有 uploadArchives,之后针对 archives 进行签名。

signing 插件如何同 gpg 生成 key pair 交互呢?这就需要在~/.gradle/gradle.properties再声明三个变量,如下:

1
2
3
signing.keyId=XXXXXX
signing.password=your_key_pair_password
signing.secretKeyRingFile=/Users/your_name/.gnupg/secring.gpg

还剩下最后的一步,归档 Jar,sourceJar(源代码)和javadocJar(API 文档),这些是需要签名的对象。

archive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apply plugin: 'java'

task sourcesJar(type: Jar) {
classifier = 'sources'
from sourceSets.main.allSource
}

task javadocJar(type: Jar, dependsOn: javadoc) {
classifier = 'javadoc'
from 'build/docs/javadoc'
}

artifacts {
archives jar
archives sourcesJar
archives javadocJar
}

归档产物最终都会被签名,生成以 .asc 为后缀的签名文件。

1
2
3
4
5
6
7
build/libs
├── underscore.string.java-0.0.1-javadoc.jar
├── underscore.string.java-0.0.1-javadoc.jar.asc
├── underscore.string.java-0.0.1-sources.jar
├── underscore.string.java-0.0.1-sources.jar.asc
├── underscore.string.java-0.0.1.jar
└── underscore.string.java-0.0.1.jar.asc

这些产出物在最终发布的时候,需要经过验证,如果验证失败,比如:缺少 javadoc 或者某个 *.asc 文件,则不被允许发布。

5. 上传 Release Archives

根据的 maven 的标准,日常开发我们会使用 snapshot 版本,如:0.0.1-SNAPSHOT;发布时,去掉后缀-SNAPSHOT,即:0.0.1。而 maven 会根据这个特点,机智地辨识是上传到 snapshotRepository 还是 releaseRepository 的。因为我们得发布包,所以修改版本为 0.0.1 之后,我们只需要简单地执行如下命令:

1
$ gradle uploadArchives # or `gradle uA` this is a shortcut.

6. 关闭并验证 Staging 环境上的 Archive

staging Repo

登录 Sonatype 的 Nexus Repository Manager,然后点击左边侧边栏的 Staging Repositories,搜索comlambeta (GroupId 去掉中间的’.’)。接下来查看 Content tab,重点检查 pom 或者签名文件是否遗漏!当确认无误后,即可关闭 (Close) 这个 Repo。关闭过程中,Nexus 会逐项检查产物是否合规,如果出现验证错误,则在 Activity tab 中显示具体失败的步骤及原因。

7. 发布 Archive

如果上面的验证通过,上面本来不可用的 Release 按钮会变为可用。点击 Release 按钮,直接发布包。

8. 通知 issue 管理员开启同步

发布包之后,就可以通知管理员开启同步。我在原来的 issue 的评论区留言:

I have a first release version 0.0.1 for this library.

很快地,管理员就回复同步已经开启:

Central sync is activated for com.lambeta. After you successfully release, your component will be published to Central, typically within 10 minutes, though updates to search.maven.org can take up to two hours.

不过,由于当时所用 gradle2.1 的版本,导致了上传时 pom 文件被遗漏,在 search.maven.org 中搜索不到。管理员很热心地解释了这个现象:

search.maven.org needs a valid POM file to be a part of your uploaded artifacts. Browsing Maven Central directly:
http://repo1.maven.org/maven2/com/lambeta/underscore.string.java/0.0.1/
it appears that a POM file is missing.

遂升级到 3.1 版本,重新上传之后就能在 search.maven.org 中看到。

9. 检查同步成功

除了通过 search.maven.org 检查同步是否成功之外,查询mvnrepository也是常用的搜索方式。不过,值得一提的是,mvnrepository 相较于 search.maven.org 同步会更慢点,原因是 mvnrepository 引用了 central.maven.org 仓库。而 central.maven.org == repo1.maven.org,两个域名对应的 IP 是一样的,而这个 repo1.maven.org 就是默认的 Maven central repository,也就是 search.maven.org 的仓库。
所以,你可以在以下两个仓库看到发布包:
http://central.maven.org/maven2/com/lambeta/underscore.string.java/
http://repo1.maven.org/maven2/com/lambeta/underscore.string.java/


参考链接
[1] simple library publishing with gradle
[2] release deployment

Underscore.string.java 是什么?

underscore.string.java 是一个Java中方便操作字符串的类库,提供了众多帮助方法。

起源

写过 Javascript 代码的人,估计没有几个不知道 underscore 这个类库的,因为它太好(有)用了,尽管现如今由于实现上不够优雅的缘故,已经被lodash所取代。而我想介绍的是 github 上 star 3000+的 underscore.string,它原本是 underscore 的扩展,不过现在已经演变成独立的库。顾名思义,它的作用就是弥补 Javascript 本身对于字符串操作支持的匮乏。
举个例子:

1
2
3
4
var slugify = require("underscore.string/slugify");
slugify("Hello world!");

-> hello-world

slugify是一种规整字符串的操作,常用于把url中的非法字符规整成 word-word 的模样。比如,我的这篇 blog 是通过 hexo 生成的 hexo new "Underscore.string.java",它会自动帮我转换成 2017-09-06-Underscore-string-java 这样的亲和url的格式。

想法

接触这个类库的时候,我正在使用 google guava,惊讶于这里头的各类操作,包括 ImmutableList、CharMatcher、Strings 等等。一来准备针对 guava 练练手,二来确实想学习一下 underscore.string 的操作,所以就开始着手写起了 underscore.string.java 这个项目。编写的过程中确实学到了不少平常不太可能接触到的设计方法。

项目介绍

这个类库已经正式发布在 Maven Central Repository. 最新版本是 0.2.0.

前置条件

  • java >= 1.6
  • guava 18.0

安装

gradle

1
2
3
4
5
6
7
repositories {
mavenCentral()
}

dependencies {
compile 'com.lambeta:underscore.string.java:0.2.0'
}

maven

1
2
3
4
5
<dependency>
    <groupId>com.lambeta</groupId>
    <artifactId>underscore.string.java</artifactId>
    <version>0.2.0</version>
</dependency>

支持的特性

  • capitalize
  • slugify
  • count
  • trim
  • ltrim
  • rtrim
  • repeat
  • decapitalize
  • join
  • reverse
  • clean
  • chop
  • splice
  • pred
  • succ
  • titleize
  • camelize
  • dasherize
  • underscored
  • classify
  • humanize
  • quote
  • unquote
  • surround
  • numberFormat
  • strRight
  • strRightBack
  • strLeft
  • strLeftBack
  • toSentence
  • truncate
  • lpad
  • rpad
  • lrpad
  • words
  • prune
  • isBlank
  • replaceAll
  • swapCase
  • naturalSort
  • naturalCmp
  • dedent
  • commonPrefix
  • commonSuffix
  • chopPrefix
  • chopSuffix
  • screamingUnderscored
  • stripAccents
  • pascalize
  • translate
  • mixedCase
  • collapseWhitespaces
  • ascii
  • chomp
  • startsWith
  • endsWith
  • levenshtein
  • hamming
  • longestCommonSubstring

New Features in 0.2.1-SNAPSHOT

gradle

1
2
3
4
5
6
7
8
9
10
repositories {
maven {
url 'https://oss.sonatype.org/content/groups/public'
}
}

dependencies {
compile ("com.lambeta:underscore.string.java:0.2.1-SNAPSHOT")
}

maven

1
2
3
4
5
6
7
8
9
10
11
12
13
<repositories>
<repository>
<id>my-repo</id>
<name>sonatype</name>
<url>https://oss.sonatype.org/content/groups/public</url>
</repository>
</repositories>

<dependency>
    <groupId>com.lambeta</groupId>
    <artifactId>underscore.string.java</artifactId>
    <version>0.2.1-SNAPSHOT</version>
</dependency>
  • replaceZeroWidthDelimiterWith

实现步骤

  • 测试类标注@ActiveProfiles(resolver = ProfilesResolver.class)
  • 自定义类 ProfilesResolver 实现接口 ActiveProfilesResolver,并实现接口中唯一的方法resolve(Class<?> targetClass)
  • maven-surefire-plugin 插件中配置
    1
    2
    3
     <systemPropertyVariables>
    <spring.profiles.active>${spring.profiles.active}</spring.profiles.active>
    </systemPropertyVariables>

实现如下:

1. 标注启用

1
2
3
4
5
6
7
8
9
10
11
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {PetstoreApp.class}, // 我们的 application 名为 PetstoreApp
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles(resolver = ProfilesResolver.class)
public abstract class BaseResourceTest {
@BeforeClass
public static void setUp() {
String activeProfiles = System.getProperty("spring.profiles.active");
System.out.println(String.format("Resource Tests resolved profiles are [%s]", activeProfiles));
}
}

这个类存在的意义就是为了让其它类别的 ResourceTest 继承它,并在一次启动当中运行完所有的集成测试。避免每个 ResourceTest 都初始化启动 Application,造成运行速度变慢。

注意abstract关键字
如果不使用abstract关键字,那么maven-surefire-plugin就会抛出如下错误:

Tests in error:
BaseResourceTest.initializationError » No runnable methods

2. 实现自定义类 ProfilesResolver,如下:

1
2
3
4
5
6
7
8
9
10
import org.springframework.test.context.ActiveProfilesResolver;

public class ProfilesResolver implements ActiveProfilesResolver {
@Override
public String[] resolve(Class<?> aClass) {
String activeProfiles = System.getProperty("spring.profiles.active");

return new String[] {activeProfiles != null ? activeProfiles : "local"};
}
}

这里表示我们会从系统变量当中读取spring.profiles.active,但是这个变量从什么地方来呢?
我首先想到的是 maven 的 profiles 中设置 properties,如下:

1
2
3
4
5
6
<profile>
<id>local</id>
<properties>
<spring.profiles.active>local</spring.profiles.active>
</properties>
</profile>

如此,当我们在命令行中运行mvn test -Plocal的时候,就表明启用了 local 这个 profile。相应地,在 maven 的上下文当中,spring.profiles.active变量的值就是local

但是运行测试的时候,我们 ProfilesResolver 中的 System.getProperty("spring.profiles.active")返回的始终是null。其实道理很简单,maven 中定义的 properties 全是给 maven 自己(包含各类插件)用的,它并不会传递给应用程序使用。

注意:


properties 中定义的 spring.profiles.active 其实主要是给插件 maven-resources-plugin 使用的,具体请参看备注。


3. 定义systemPropertyVariables

所以我们需要定义systemPropertyVariables,顾名思义,这是系统变量的定义,在应用程序中就可以使用System.getProperty("spring.profiles.active")获得。

放在哪里合适呢?跑测试的插件中最合适!

所以,我们有如下的配置:

1
2
3
4
5
6
7
8
9
10
11
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<runOrder>alphabetical</runOrder>
<systemPropertyVariables>
<spring.profiles.active>${spring.profiles.active}</spring.profiles.active>
</systemPropertyVariables>
</configuration>
</plugin>

结合上面 properties 的配置,当我们再次运行mvn test -Plocal的时候,就会得到一个名为spring.profiles.active的系统变量,它的值由${spring.profiles.active}决定。此处,就是local。


备注

properties 中 spring.profiles.active 的另外用途
只要 maven 的 properties 中定义了 spring.profiles.active ,运行mvn spring-boot:run -Plocal的时候,spring boot 就会启用applicaiton-local.yml profile 文件。

为什么会这样的呢?按常理推断,应该是spring-boot-maven-plugin的配置项自动读取了我们设置的 properties spring.profiles.active,但是只要看一眼这个插件的文档就会发现,除非显式地在插件的configuration下配置了profiles参数或者手动传入run.profiles系统变量example,否则插件本身(可以像我一样扫一眼插件的源码)并无法感知到底启用 spring 的哪个 profile!所以这个假设不成立。

答案在bootstrap.yml当中!
以下是resources/config/bootstrap.yml中的内容

1
2
3
4
5
6
7
8
9
spring:
application:
name: petstore
profiles:
# The commented value for `active` can be replaced with valid Spring profiles to load.
# *注意底下这句话*
# Otherwise, it will be filled in by maven when building the WAR file
# Either way, it can be overridden by `--spring.profiles.active` value passed in the commandline or `-Dspring.profiles.active` set in `JAVA_OPTS`
active: #spring.profiles.active#

这里的注释很有用,明确地告诉我们在构建 WAR 包的时候,maven 会帮我们把#spring.profiles.active#替换成真正的值。

这又是怎么做到的呢?一切归功于maven-resources-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>${maven-resources-plugin.version}</version>
<executions>
<execution>
<id>default-resources</id>
<phase>validate</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>target/classes</outputDirectory>
<useDefaultDelimiters>false</useDefaultDelimiters>
<delimiters>
<delimiter>#</delimiter> <!-- 看这里 -->
</delimiters>
<resources>
<resource>
<directory>src/main/resources/</directory>
<filtering>true</filtering>
<includes>
<include>**/*.xml</include>
<include>**/*.yml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources/</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/*.xml</exclude>
<exclude>**/*.yml</exclude>
</excludes>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>

这个插件除了简单的 copy 功能之外,还能进行 Filtering 操作

Filtering
Variables can be included in your resources. These variables, denoted by the ${…} delimiters, can come from the system properties, your project properties, from your filter resources and from the command line.

大意是说,你可以在 resources 文件定义自己的变量,这些变量可以来自系统属性、maven 工程属性,你过滤的 resources 文件和命令行。

说白了,就是在 copy 资源文件的时候,同时帮你把文件中的变量(占位符)替换成真实的值。而这里就是通过<delimiter>#</delimiter>来规定变量格式的!换句话说,在文件中只要是以#开头和结尾的字符串都会被替换掉(变量有定义的情况下;否则保持原样)。

这里,由于绑定了生命周期——validate,可以直接运行mvn validate -Plocal这样的命令进行快速验证。得到的bootstrap.yml内容如下:

1
2
3
4
5
6
7
8
spring:
application:
name: petstore
profiles:
# The commented value for `active` can be replaced with valid Spring profiles to load.
# Otherwise, it will be filled in by maven when building the WAR file
# Either way, it can be overridden by `--spring.profiles.active` value passed in the commandline or `-Dspring.profiles.active` set in `JAVA_OPTS`
active: dev # 替换成功

回到最开始的疑问,为什么只要 maven 的 properties 中定义了 spring.profiles.active ,运行mvn spring-boot:run -Plocal的时候,就可以spring boot 就会启用applicaiton-local.yml profile 文件呢?

因为,maven 在运行命令之前已经做了 copy-resources 的操作,那时候就已经把bootstrap.yml中的spring.profiles.active替换成 local 了,所以启动 springboot application 的时候,它会启用spring.profiles.active代表的值,此处就是 local,那么启用的文件自然就是application-local.yml

这是Clojure好书《Clojure for the Brave and True》作者 Daniel Higginbotham 写于2017年4月16日的博文。从作者的丰富的经验来看,本文非常具有指导意义。

学习编程语言是一种技巧:做好了,你会感受到掌握新事物之后的快感(dopamine:多巴胺);做不好,就会接二连三的沮丧,甚至放弃。下面这些学习编程语言的最佳技巧是我从多年的著书写作演讲培训中总结出来的。这里头很多技巧来源于对高效学习前沿研究做了解释的书籍。你可以在 Community Picks Learn Programming 中找到那些书(还有其它牛x的编程书籍)。

持续测试自己以抵抗胜任力错觉

最不济的学习方法中的一种就是重读或者重看材料。这种重复会给你一种感觉——似乎不用花什么气力,你就理解了话题所涵盖的内容。研究者们把这种现象称作胜任力错觉。

更好的一种方式(你可以掌握的最佳技巧之一)则是持续地测试自己。不要重读一个函数、类或者一个对象是什么,而是让自己定义这些概念或者把它们用到短小的程序当中——强迫你用某种方式显示自己的理解。这一过程常常很不舒服的,但是对形成长期记忆非常有效。更进一步,你可以在阅读材料之前先去行测试,举个例子,尝试在阅读一个章节之前做做练习。值得一提的是,这也被证明有助于记忆的形成。

测试对于学习的显著影响被称为测试效用,下面是一些具体方法可以利用:

  • 在阅读章节或者看视频之前,尝试猜测你将要学习的东西,并写下来。
  • 在阅读章节之前先做做这个章节的练习题。
  • 一直做练习,即使是最难的那些。暂时(永远)放弃一个练习也是可以的,不过至少要尝试一遍。(下个章节会详细谈到)
  • 阅读短小的程序并且尝试不看源码重新写一个。或者,再小一些,写个函数也行。
  • 在学习了对象、类、方法或者高阶函数等新概念之后,立即编码做示例。
  • 创建阐述这些概念的图示,以及这些概念之间的区别和联系。
  • 把你刚刚学到的概念写成博客。
  • 尝试把概念解释给非技术的朋友听。(在写《Clojure for Brave and True》的时候,我常常这么干。这样能够以外行的话阐述一个想法,进而迫使你深入理解想法本身。)

这些技巧的大部分都要规约到编写代码上!说到编程,由于程序都是重文本和概念性的,所以很容易以为我们仅仅通过阅读(代码)就在学习。但是程序同时也是一门手艺,就像其它手艺一样你得操练才能娴熟。编码是暴露你对程序作有错误假设的最佳方式。你越快地这么做,就会越快地纠正错误和提升技能。

如果你想了解更多测试效应的事情,敬请查看坚持:成功学习的科学(make it stick: The Science of Successful Learning)

花时间放空自己

如果你纠结在一个问题上,或者对刚才读到的东西不能理解,就去散散步甚或洗个澡 —— 只要能进入一种舒缓、放空的状态就行。解除障碍的最佳方式之一就是歇一会儿,这可能听上去有点反直觉,但确实如此。

问题是,当全神贯注解决问题时,我们很容易陷入思维障碍(mental blinder)。我的意思是,这差不多就是“关注”(字面上)的意思。不过,全神贯注会导致我们只能一直探索解决方案空间的一小部分。一旦放空,我们的潜意识就可以探索并联结我们经验中的广泛领域。

对我来说,这就像当你试图在纸质地图上找到一个目的地(是否还记得?)。你不用刻意就确信你想抵达的城市应该就在这里!在地图的左上角的区域,所以你看了一遍又一遍,都没有成功。然后你放下地图,做了深呼吸并让目光游离了一会儿。当你重新看地图时,确切的地点立马映入眼帘。

我们曾经都有过这样的经验,在洗澡的时候突发灵感。现在你对于为什么这么做有了更好的了解,那么也就能刻意地使用这个技巧。个人来讲,如果纠结在某事上,我真的会洗个澡,这个技巧的功效显著。另一方面,我又是多么干净(注:洗澡这件事)。

如果你想多学一些关于思考的关注和分散模式,敬请查看A Mind for Numbers: How to Excel at Math and Science (Even If You FLunked Algebra)

别浪费时间沮丧

和上一个章节相关:别浪费时间为代码沮丧了。沮丧会让我们做一些愚蠢的事情,比如重新编译或者重刷浏览器,期望这次会有所不同。

把沮丧看作你的知识有差距的信号。一旦你意识到自己沮丧了,它可以帮你后退一步,清晰地识别问题。如果你写的代码不起作用,坦率地向自己或者别人说明你期望的结果。使用科学的方法,就非预期行为的根因提出一个假说。然后测试你的假说。再次试验后,如果依然解决不了,就把这个问题放到一边,待会儿回来。

在一些似乎没法解决的问题上,我不知有多少次恼怒地扔掉了自己的笔记本电脑。隔天再看,一个显而易见的解决方案立马跳入脑海。甚至上周就发生过。

确认你正在处理语言的哪个方面

个人观点,我觉得记住这些是有用的——当学习一门编程语言的时候,你实际上正在学四件事情。

  • 怎么写代码:语法、语义以及资源管理
  • 语言的范式:面向对象,函数式,逻辑等
  • 产出物的生态圈:如何构建、运行可执行文件以及如何使用库
  • 工具:编辑器,编译器,调试器,代码质量检测器(linter)

这四项很容易搞混,不幸的结果是,当你遇到问题最终完全找错了地方。

举个例子,某些完全的编程新手,可能准备开始构建iOS应用。他们可能会试着让自己的应用在朋友的手机上运行,只看得到有关需要开发人员证书或其他信息的消息。这是产出物生态圈的一部分,不过小白可能将此视为编写代码的问题。他们可能会浏览自己写的每行代码来尝试解决问题,尽管问题和代码没有半毛钱关系。

如果我系统地处理这些方面,我会发现学习一种语言会更加容易。我将在其它的博客文章中罗列一些待回答的宽泛问题的列表,应该能帮助你学习任何语言。

明确目的,外部模型,内部模型

任何时候你学习使用新工具,明确学习的目的,外部模型和内部模型都是十分有用的。

当你了解了工具的目的时,你的大脑会加载有用的上下文细节,使你更容易吸收新知识。这就好比拼图:当你看到完整拼图之后,更容易把各部件拼到一起。这个道理适用于语言本身以及语言库。

工具的外部模型就是它呈现出来的接口以及它想让你思考问题解决的方式。Clojure 的外部模型就是一个 Lisp,它想让你把编程当做大部分以数据为中心,不可变的转换过程。Ansible 希望你把服务器的整备工作想成定义最终状态,而不是定义抵达那种状态所要采取的步骤。

工具的内部模型就是如何将输入到其接口转换成一些底层的抽象。Clojure 把 Lisp 转换成 JVM 的字节码。Ansible 把任务定义转换成了 shell 命令。在一个理想国中,你不需要理解工具的内部模型,但事实上,理解内部模型总是有用的,因为在某些看上去迷惑或者矛盾的部分,它可以让你有个统一视图。举个例子,当 DNA 双螺旋模型被发现的时候,它帮助科学家们了解更高层次的现象。从我的角度来讲,当然,这篇博文也是历来所有伟大科学成就之一。

很多教程经常混淆工具的外部和内部模型,使学习者感到困惑。意识到这点可以帮你轻松辨别何时你会感到沮丧。

间隔重复帮助记忆

间隔重复被证明是长期记忆中新信息编码的最佳方法之一。 这个想法是以不断增加的时间间隔来测验自己,使用最少重复次数来最小化记忆衰减。 卫报写了一篇很好的介绍性文章

睡眠和练习

保重身体!身体可不仅仅是你脑袋的载体。如果你想保持专注和高效的学习,就要足够的睡眠和练习,而不是(原文:beats the pants off)狂饮咖啡因和能量饮料。

更多小贴士

如果你还有其它有用的技巧,请不吝评论!如果你想知道更多关于学习编程的优质资源,敬请查看Community Picks: Learn Programming,这是一个社区策划编程学习书籍的收集活动,内容广泛,包含入门编程,工艺以及关于软技能和面试方面的书籍。


原文链接
[1] Techniques for Efficiently Learning Programming Languages

0%