鄢倩

(conj clojurians me)

究竟要怎么做,才能热衷于某件事呢?才能打心底认定非此不可,并在这条路上勇往直前呢? —— 三浦紫苑《编舟记》

西冈想不明白,我也是。但有些人没去想便做了,小说主角马缔就是其中一个。

做过报刊的编辑工作,我深知其对词汇认知敏锐度和付梓校对细致程度的要求之高,何况是小说中编纂“浩瀚的词汇海洋”般的辞典。假如书中描述是主角依着敏锐的词汇嗅觉和认真的态度不屈不挠地编纂出一部《大渡海》,这顶多被我归入成功学之列。所幸不是,推荐语“究竟要燃烧什么,才能激发出真正的热情?”是驱使我购书的诱因,也是心中一直郁结的谜团,在这种状态下,这本书激发了我思考的动机。

为什么能如此全身心地投入一件事?

这个问题放到马缔身上,可以归结成两点原因:适材适所和背负责任。除了对词汇有着敏锐认知度之外,马缔算是一无是处,如果没有被辞典编辑部发掘,估计只能碌碌无为,虚度一生。在辞典编辑部,他个人的才华连同缺点一起大放异彩,很快跻身成为中流砥柱。若说才华得以施展是他内在的驱动力,那么前辈对其的全权托付则是这外在的鞭策。一个人将毕生的事业全权托付给另一个人,这里头的分量非同小可,马缔背负这样的责任是一种幸运。这两点在我看来就是他热情的源头。

小说是一种艺术,艺术源于生活而高于生活。生活是复杂的,人也是。而我习惯囿于一种怀揣真理以正向视角去解读任何人的方式,自认为应该如此。倘若热情果真如此言简意赅,为何我心中的郁结未减半分。又或者真应了那句台词:我们听过无数的道理,却仍旧过不好这一生?

暂时抛开人生哲理不谈,本书倒是有一个实用的指导意义——辞典中每个词都是被编辑认真释义,力求正确全面的。如此不妨模仿书中的行文方式,查查辞典中对热情这个词的描述。

【热情】指人参与活动或对待别人所表现出来的热烈,积极,主动,友好的情感或态度。热情的反义词是冷漠。如:冷漠的人干活无精打彩,热情的人工作起来象个小老虎。冷漠的人待人爱理不理,热情的人主动关心,主动帮忙,如火温暖。热情是与人生观,价值观有关联的,是一个人态度、兴趣的表现。与激情相比,热情更平稳稳定一些。激情是一种强烈的情感表现形式。往往发生在强烈刺激或突如其来的变化之后。具有迅猛、激烈、难以抑制等特点。人在激情的支配下,常能调动身心的巨大潜力。

马缔编辞典的热情,固然是他性格所致、兴趣所在。但是如何解释逾十五年,几乎独力完成编纂整部辞典这一事实呢?一件事失去了新鲜感,兴趣总归是减损的,这是客观事实。工作如此,尤其是以高薪技术标榜,极客达人自居的程序员一族,视快速学习新技术为本职,这也决定我辈好奇骛新的特点,追技术也似赶潮流,热情不过三分,把玩一番就浅尝辄止,如此反复,连猎奇的心情也冷却了,凡事都提不起劲儿,以懒自居,浑浑噩噩,陷入迷惘。可见兴趣并非热情之源,一定有别种更深刻的东西在持续地,稳定地释放着热情,如源头活水才是。

我崇拜的一位我司的前辈说过:“迷惘是因为你离目标太近”。看到这句话正是我入职满一年的时候。头一年里,从职场新人转变成老人,逐渐进入舒适区。等回过神,忽然发现每天过着朝九晚六的生活,年纪轻轻,人生就这么决定了,心有不甘,又无可奈何。究其原因,大概是大学之后,再无目标。犹记得当年毕业找到工作后,写就的一篇关于《信仰》的文章,文中探讨了信仰对于一个人面临人生选择时的重要作用,同时佐引了德国大哲学家康德说过一句话:“我们信仰,是因为我们需要信仰”。那时想法单纯,对这句话也只是断章取义,不乏嘲讽。现在看来,信仰的本身就是生活的需要,如同光和水一样必不可少。很多时候,我们所认为荒谬的道理其实并不一定如此荒谬,我们过得生活会在某处经历中告诉我们:事实如此。

可是究竟要信仰什么?

在世纪的转折点上,尼采大呼“上帝死了”,人们发现自己生活在一个毫不仁慈、无法救赎的世界中。这时,叔本华用一种消极的口吻告诉人们:生命本无意义,生命本质上是悲剧性质的,人生而痛苦,个人应当自觉否定生命意志快步走向灭寂。但是尼采爱人类,他肯定了生命的悲剧性质,却不屈服否定生命意志的消极面,毕竟哲学原初的目的就是让人幸福。所以他提出“面对痛苦、险境和未知的事物,精神愈加欢欣鼓舞”的酒神精神以及不断自我超越、支配的求强力的意志。所谓求强力的意志就是生命要不断奋力向上,从高于自己的东西那里寻找自身的意义和目的;其本身就是强力,其本质上就是求强力的过程。我认可这样的生命意志,我愿意用自己蓬勃兴旺的生命力去战胜苦难,像英雄一样在残酷的悲剧中自我肯定,坚强到足以把苦难当作快乐来感受,我要把它奉为我的信仰!

回头再细细评味“我们信仰,是因为我们需要信仰”这句话,我确实埋头驻足不知所措了好长一段时间,现在抬头了,瞭望久远的未来,选择了自己的信仰——我迫切地需要一条明路。至于马缔的信仰是什么我不得而知,不过我敢肯定他至少拥有健康的生命本能和严肃的精神追求,人物本身虽然属于小说虚构,但是他活得太真实了,生命和精神必须是兼具的,不然一个人活得不会真切。此间热情的源头想必存在生命意志当中吧,于马缔而言,是生命本能和某种精神追求的合力;于我而言,是信仰。

我很早就意识到人生中的三大幸事:有事做,有人爱,有所期待。

马缔十五年如一日地编辑辞典,这是有事可做;马缔背负着前辈的托付——尽早付梓出版《大渡海》这部辞典,渴望前辈和消费者的认可,这算是有所期待;那么马缔有人爱吗?必须有,而且小说中安排了一位“辉夜公主”给他。可是视编辑辞典为生命,生活又乱糟糟的无趣之人凭什么得到人家姑娘的爱?这种不合理的展开难道是作者个人的意淫么?如是想着,模仿马缔的笨拙,自然以笨拙收场,我自嘲。我探讨不来这种话题,却由衷欣赏他们两人之间的默契,都有自己热衷的事业并沉浸其中追求极致,所以才能互相理解,互相慰藉。这是两个独立人格之间自由的结合,谁也不会为谁放弃自我,这种体现自由又能体现人格的关系不是爱情还能是什么?这三个条件满足了,在我看来,马缔就是幸福的,此生不会再迷惘和孤独。

“人的内心,有时候对自己都是一个谜”。我想活得明白点,仅此而已。

尼采 在世纪的转折点上
编舟记

什么是字符集

ASCII(American Standard Code for Information Interchange)

1
2
man ascii
0 ~ 2^7 = 128

欧洲语系 ISO8859

  • ISO8859-1英语、法语、德语;ISO8859-5 俄语

中国的GB2312和GBK,以及中国台湾的Big5

多语系 Unicode

  • 最初的目的是把世界各地的语言都映射到16位空间
  • 8位转换为16位,称为UCS-2 (2 byte Universal Character Set), 也叫做Basic Multilingual Plane (BMP)
  • 16位到21位,有效编码区间0 ~ 0x10ffff

    The Unicode standard describes how characters are represented by code point

什么是编码方式

取“鄢”这个字的Unicode编码


1
2
echo "鄢" | native2ascii -encoding utf8
;; \u9122 这个是 code point

一段查看汉字编码的Java代码

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
import java.nio.charset.Charset;
import java.util.Arrays;

class Main {
public static void main(String[] args) throws Exception {
unicode("鄢", "utf8");
// How many bytes: 3 What are they: [-23, -124, -94]

unicode("鄢", "utf16");
// How many bytes: 4 What are they: [-2, -1, -111, 34]

unicode("鄢", "utf-16BE");
// How many bytes: 2 What are they: [-111, 34]

unicode("🐶", "utf-16BE");
// How many bytes: 4 What are they: [-40, 61, -36, 54]

p("🐶".length());
// 2
}

public static void unicode(String s, String encoding){
p("How many bytes: " + s.getBytes(Charset.forName(encoding)).length);
p("What are they: " + Arrays.toString(s.getBytes(Charset.forName(encoding))));
}

public static void p(Object s) {
System.out.println(s);
}
}

UTF-8变长编码方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
\u9122
1110xxxx 10xxxxxx 10xxxxxx

9 -> 1001
1 -> 0001
2 -> 0010
2 -> 0010

10010001 00100010

原码
11101001 10000100 10100010

取反
10010110 11111011 11011101

补码
10010111 11111100 11011110
16+4+2+1=23 127-3=124 64+16+8+4+2=94
[-23, -124, -94]
  • 从程序运行的结果来看,UTF-8中,“鄢”占据了3个字节,且在计算机中的补码表示分别为-23, -124, -94
  • \u9122占据2个字节,但是带入UTF-8编码提供的1110xxxx 10xxxxxx 10xxxxxx模板中就变成了3个字节,我们是从低位带入模板的,x的个数刚好是16位、2个字节;
  • UTF-8编码除了和ASCII码兼容部分(以0开头,0xxxxxxx),其余都遵循一个简单的标准:
    • 其模板遵循110xxxxx 10xxxxxx,高位1的个数即是总的字节数,这里110xxxxx中11表示总共有两个字节;
    • 低位的10xxxxxx始终以10开头;
    • 这样的编码方式,使得程序清楚知道那个地方是字符开始的地方,所以才说UTF-8的单字节的编码方式。

但是UTF-8也有自己的缺点

  • 浪费内存,几乎所有的汉字都占三个字节
  • 随机访问,同字符串的长度成正比

Tips:

正数的补码就是其本身
负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1

大端字节序和小端字节序(网络字节序和主机字节序)

我们知道UTF-16的目的和Unicode本来的目的是一致的 —— “把世界各地的语言都映射到16位空间”。
\u9122为例,我们运行程序的结果应该是2个字节,但是事实上是4个字节(*How many bytes: 4 What are they: [-2, -1, -111, 34]*),这多出来的2个字节是怎么回事?
这里就是字节序在作祟。在内存存储时,如果最低有效位在最高有效位的前面,则称小端序;反之则称大端序。大端字节序又称网络字节序。
举个例子

1
2
3
\u9122 ;;;->鄢
91是最高有效位
22是最低有效位

我们观察一下返回的4个字节中的前两个字节-2, -1,它们其实不是字符“鄢”的一部分,它们是Unicode中的大端字节序的标识BOM(Bytes Order Marks)

1
2
3
4
5
6
7
0xfeff
11111110 11111111
反码
10000001 10000000
补码
10000010 10000001
-2 -1

Unicode在对待字节序时,采取了大端和小端序共存的方式。0xfeff这个编码在对调字节序后得到的0xfffe在Unicode不存在,Unicode借助这个特点来判断读入的字符到底是大端序还是小端序。
另外,从程序中可知,Java会默认将读入的字符串当做大端序处理;所以当我们明确指定UTF-16BE后,程序就会返回2个字节的结果了。

UTF-16不足以表示世界所有的字符

正如前面提及的UCS-2 (2 byte Universal Character Set)不足以表示世界上所有的字符,比如“🐶”这种emoji的字符需要4个字节才能表示,两个字节的UTF-16就无能为力了。
Java默认采用了UTF-16编码,结果导致"🐶".length()不是返回1,而是2。不得已,Java引入了codePoint这种处理方式,有点无奈。

UCS和CSI

我们计算机的程序会采用两种方式来处理不同的字符集

  • UCS:(Universal Character Set,泛用字符集)程序输入输出的时候,需要将文本数据变成UCS,统一处理。

  • CSI:(Character Set Independent 字符集独立),不对文字集做变换,直接处理。

结论

  • 积极采用UTF-8。

在Gaslight,我们广泛地在工作中使用Ruby和Rails,也充分地经历了从JQuery到Ember再到AngularJS及其他所有关于JavaScript的一切。不过有时候我们为了学习和成长,需要脱离舒适区。我们如何赶上未来的趋势和机会呢?

那就是绝不停止学习。在过去的几个星期里,我们的开发团队决定作为一个小组去掌握Clojure语言。我们尝试构建了关于这门技术的主要知识点,所以当我们在一个客户项目中采用Clojure的时候,对自己技术的深度和熟悉程度很有自信。

为什么我们会选择Clojure呢?

填补了Ruby和Rail没有顾及的空白

我知道我们不可能永远用Ruby编程。另一个语言和环境一定会到来并取代它。我只是希望它真的比我们现在所拥用的更好。 —— Doug Alcorn

我们依然热爱Ruby,依然认为Rails通常是构建web应用的最佳方案。但是,它和其他工具一样,有长处,也有短处。

  1. Ruby的并发并不健壮。是的,你可以使用JRuby和Celluloid,但是可变性在根本上妨碍它走向并发。同时,很多你想要使用的库都不是线程安全的。
  2. Ruby和Rails几乎总是足够快的。几乎总是,但不尽然。
  3. 对象关系映射(ORM)不总是正确的。
  4. 面向对象的语言也不总是最佳的解决方案。

函数式编程是一种不同的思考方式

函数式编程是一种编程范式,一种构建计算机程序的结构和元素,它把计算看做数学函数的求值,同时禁止改变状态和可变的数据。——Wikipedia

函数式编程语言是一种有别于我们平常在Ruby和JavaScript中编程的编程范式。学习这一范式使得我们能学习崭新且不同的技术来解决问题。这些技术可能不太容易去学习,如果我们沉浸在自己舒适的命令式和面向对象的语言范式当中。

Clojure运行在服务器端

Clojure实现它的目标,通过:拥抱一个行业标准的开放平台——Java虚拟机;现代化一门古老的语言——Lisp;树立带有不可变持久化数据结构的函数式编程;同时通过软件事务内存以及异步代理提供内建的并发支持。这一结果是健壮的,顺手的和快速的。—— Clojure.org

Clojure运行在Java虚拟机(JVM)上并且利用了其提供的优势:高质量的垃圾回收,高性能的本地线程以及可扩展的移植。Clojure又在其上增加了高效的不可变持久化数据结构。Clojure带来了有趣的并发模型Core.Async。另外,这里还有很多其他有趣的库供开发,逻辑编程,模式匹配,optional typing和好多我们想要尝试并理解的东西。

Clojure运行在浏览器端

一个具有扩展,移植和多供应商支持的开发平台,一场在各个新设备上实现精巧工具的优化军备竞赛以及一个对更丰富且更精巧应用的呐喊。开发者们还有什么想要的呢?一个不同的语言,这就是了。—— ClojureScript Wiki

ClojureScript编译成JavaScript运行于浏览器。它提供了一种全新的方式去思考编写客户端应用程序。JavaScript并不是开发者社区里广泛青睐的语言。但是Clojure是设计良好,精密且具有很好异步支持的成熟的Lisp。它也可以和其他的JavaScript类库交互。所有这些都为我们提供了一个在客户端和服务端使用相同的语言却不需要做任何妥协的机会。

管理本来就具有状态的客户端应用是件尴尬的事,不过像React和Ember这些JavaScript框架正在着手解决这个问题,我们开始猜想如果一种更加基本的改变方式值得深入研究,那么是否可能有一种更好的语言和这其中的任一已经存在的框架配合来为我们提供一种途径使得可以更好地控制复杂性呢?Clojure允许我们朝着这个问题寻找答案。

我们不相信银弹,但是我们真的喜欢这样的事实,Clojure包含了客户端的需求。

朝冰球要去的地方滑

我追逐冰球要去的地方,而不是它所在的地方。—— Wayne Gretzky

Clojure会帮助我们填补创建特定类型的应用程序的沟壑。那些需要响应或者处理大规模数据的程序。我们已经身处多核CPU的纪元,但是传统的语言还未追赶上这些计算机的架构。函数式编程提供了一种新的编程范式允许我们更简单地使用这种架构写程序。Clojure提供允许我们高效地处理大规模数据的工具。它使得传递和转换数据接近实时。这些特性会使我们比从前更高效地解决问题。

不过不是Bleeding Edge

“什么会被认为是bleeding-edge的技术,”sez Lucas。 “没被证实的用法,高风险,只有早期采用成瘾者才感到舒服的东西”—— Thomas Pynchon, Bleeding Edge

此时,Clojure被发布超过5年之久并且似乎在开发者社区里获得了不错的想法分享。在新锐技术上掉坑的采纳曲线上这不算太早了。Clojure利用了已存在的技术如已经被开发超过15以上年头的java虚拟机(JVM)。它也可以利用很多已经存在的高质量的Java类库。

一些新东西让我们保持兴奋

自从学Ruby以来,还没有学习一门新技术让我如此激动! —— Michael Guterl

在Gaslight,我们想在行业中吸引和保留最好的技术。为了做到这一点,我们需要给大家提供学习和提高其技能的机会。Clojure帮助我们提供了这样的机会。它是一个带有一系列工具的崭新范式。不过最重要的是,它允许我们处理新的且有趣的问题。还没有什么能比解决有意思的问题更让我们高兴的呢。

Monad不就是个自函子范畴上的幺半群,这有什么难理解的(A monad is just a monoid in the category of endofunctors)
—— Phillip Wadler

自函子(Endofunctor)

什么是函数(Function)?
函数表达的映射关系在类型上体现在特定类型(proper type)之间的映射。

什么是自函数(Endofunction)?

1
identity :: Number -> Number

自函数就是把类型映射到自身类型。函数identity是一个自函数的特例,它接收什么参数就返回什么参数,所以入参和返回值不仅类型一致,而且值也相同。

接下来,回答什么是自函子(Endofunctor)之前,我们先弄清什么是函子(Functor)?

函子有别于函数,函数描述的是特定类型(proper type)之间的映射,而函子描述的是范畴(category)之间的映射。

那什么是范畴(category)?

我们把范畴看做一组类型及其关系态射(morphism)的集合。包括特定类型及其态射,比如Int、String、Int -> String;高阶类型及其态射,比如List[Int]、List[String]、List[Int] -> List[String]

接下来看看函子是如何映射两个范畴的,见下图:
范畴

图中范畴C1和范畴C2之间有映射关系,C1中Int映射到C2中的List[Int],C1中String映射到C2中的List[String]。除此之外,C1中的关系态射Int -> String也映射到C2中的关系List[Int] -> List[String]态射上。

换句话说,如果一个范畴内部的所有元素可以映射为另一个范畴的元素,且元素间的关系也可以映射为另一个范畴元素间关系,则认为这两个范畴之间存在映射。所谓函子就是表示两个范畴的映射。

澄清了函子的含义,那么如何在程序中表达它?

在Haskell中,函子是在其上可以map over的东西。稍微有一点函数式编程经验,一定会想到数组(Array)或者列表(List),确实如此。不过,在我们的例子中,List并不是一个具体的类型,而是一个类型构造子。举个例子,构造List[Int],也就是把Int提升到List[Int],记作Int -> List[Int]。这表达了一个范畴的元素可以映射为另一个范畴的元素。

List具有map方法,不妨看看map的定义:

1
2
f :: A -> B
map :: f -> List[A] -> List[B]

具体到我们的例子当中,就有:

1
2
f :: Int -> String
map :: f -> List[Int] -> List[String]

展开来看:

1
map :: Int -> String -> List[Int] -> List[String]

map的定义清晰地告诉我们:Int -> String这种关系可以映射为List[Int] -> List[String]这种关系。这就表达了元素间的关系也可以映射为另一个范畴元素间关系。

所以类型构造器List[T]就是一个函子。

理解了函子的概念,接着继续探究什么是自函子。我们已经知道自函数就是把类型映射到自身类型,那么自函子就是把范畴映射到自身范畴。

自函子是如何映射范畴的,见下图:
Identity自函子范畴

图中表示的是一个将范畴映射到自身的自函子,而且还是一个特殊的Identity自函子。为什么这么说?从函子的定义出发,我们考察这个自函子,始终有List[Int] -> List[Int]以及List[Int] -> List[String] -> List[Int] -> List[String]这两种映射。
我们表述成:

1
2
类型List[Int]映射到自己
态射f :: List[Int] -> List[String]映射到自己

我们记作:

1
2
3
F(List[Int]) = List[Int]
F(f) = f
其中,F是Functor.

除了Identity的自函子,还有其它的自函子,见下图:
自函子范畴

图中的省略号代表这些范畴可以无限地延伸下去。我们在这个大范畴所做的所有映射操作都是同一范畴内的映射,自然这样的范畴就是一个自函子的范畴。

我们记作:

1
2
3
List[Int] -> List[List[Int]]
List[Int] -> List[String] -> List[List[Int]] -> List[List[String]]
...

所以List[Int]、List[List[Int]]、...、List[List[List[...]]]及其之间的态射是一个自函子的范畴。


幺半群

[幺半群][1]是一个带有二元运算 : M × M → M 的集合 M ,其符合下列公理:
结合律:对任何在 M 内的a、b、c, (a
b)c = a(bc) 。
单位元:存在一在 M 内的元素e,使得任一于 M 内的 a 都会符合 a
e = e*a = a 。

接着我们看看在自函子的范畴上,怎么结合幺半群的定义得出Monad的。

假设我们有个cube函数,它的功能就是计算每个数的3次方,函数签名如下:

1
cube :: Number -> Number

现在我们想在其返回值上添加一些调试信息,所以返回一个元组(Tuple),第二个元素代表调试信息。函数签名如下:

1
f :: Number -> (Number,String)

结合前面所讲,我们很容易知道元组构造子(Number,String)是一个自函子。Number所在的范畴并不同于元组(Number,String)所在的范畴。换句话说,f的入参和返回值属于两个范畴。那么这会产生什么影响?我们看看幺半群的定义中规定的结合律。对于函数而言,结合律就是将函数以各种结合方式嵌套起来调用。我们将常用的compose函数看作此处的二元运算。

1
2
3
4
5
6
7
var compose = function(f, g) {
return function(x) {
return f(g(x));
};
};

compose(f, f)

从函数签名可以很容易看出,右边的f运算的结果是元组,而左侧的f却是接收一个Number类型的函数,它们是彼此不兼容的。

有什么好办法能消除这种不兼容性?假如输入和输出都是元组,结果会如何呢?

1
2
3
F :: (Number,String) -> (Number,String)

compose(F, F)

这样是可行的!在验证满足结合律之前,我们引入一个bind函数来辅助将f提升成F.

1
2
3
4
5
6
7
8
9
10
11
12
13
f :: Number -> (Number,String) => F :: (Number,String) -> (Number,String)

var bind = function(f) {
return function F(tuple) {
var x = tuple[0],
s = tuple[1],
fx = f(x),
y = fx[0],
t = fx[1];

return [y, s + t];
};
};

我们来实现元组自函子范畴上的结合律:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var cube = function(x) {
return [x * x * x, 'cube was called.'];
};

var sine = function(x) {
return [Math.sin(x), 'sine was called.'];
};

var f = compose(compose(bind(sine), bind(cube)), bind(cube));
f([3, ''])

var f1 = compose(bind(sine), compose(bind(cube), bind(cube)));
f1([3,''])
>>>
[0.956, 'cube was called.cube was called.sine was called.']
[0.956, 'cube was called.cube was called.sine was called.']

这里f和f1代表的调用顺序产生同样的结果,说明元组自函子范畴满足结合律。

那如何找到这样一个e,使得a*e=e*a=a

1
2
3
4
5
6
// unit :: Number -> (Number,String)
var unit = function(x) { return [x, ''] };

var f = compose(bind(sine), bind(cube));

compose(f, bind(unit)) = compose(bind(unit), f) = f

这里的bind(unit)就是e了。

Monads for functional programming一书中介绍说monad是一个三元组(M, unit, *),对应此处就是(Tuple, unit, bind).

参考链接:

  1. Translation from Haskell to JavaScript of selected portions of the best introduction to monads I’ve ever read
  2. 我所理解的monad
  3. Monads for functional programming

为什么我们说多用组合,少用继承?


继承具有强耦合(strog coupling)特点。根据里氏替换原则(Liskov Substitution Principle LSP),基类出现的地方,子类一定能透明地替代。本质上来讲,LSP的这种特性致使基类具有很强的病毒蔓延性(invasive),因为如何基类设计的不好,会侵蚀所有的子类。举个例子,基类是鸟,同时具有fly这样的方法,子类中如果有鸵鸟或企鹅,就必须继承fly方法,然而这显然是没有意义的。

继承因为强耦合,所以不好测试。举个例子,有一个RemoteXMLFile<Res>基类,它内部有个Res to()方法,旨在把远程的XML文件转换成具体的Response。在我们准备测试to方法的转换逻辑的时候,每个对应子类的测试类里都不得不准备丑陋冗余的XML字面值字符串,尤其是在Java里。之所以这样,是因为无法隔离出一个专门读取XML的抽象屏障,然后使用Mock隔离这层抽象。

组合的好处自然是松耦合(loose coupling)。如果严格遵循依赖倒置原则(Dependency Inversion Principle),那么类本身一定是高能聚,类之间是松耦合的。除此之外,测试起来,各层之间的逻辑清晰明了,易于隔离开来做单元测试(Unit test)。

谈谈聚合和组合的区别


UML里有很多表示关联的线段,有两个关系十分相近,它们是聚合(aggregation)和组合(composition)

聚合:表示两个对象之间是整体和部分的弱(弱拥有)关系,部分的生命周期可以超越整体。如电脑和鼠标。

组合:表示两个对象之间是整体和部分的强(强拥有)关系,部分的生命周期不能超越整体,或者说不能脱离整体而存在。组合关系的“部分”,是不能在整体之间进行共享的。

以上是网络上的标准解释,应该是没错。但是实践中依然让人困惑不已。汽车和引擎,这个还比较好理解,汽车报废了,引擎还是可以换给别的汽车用的嘛。这就实现了整体之间的共享——聚合无疑。但是值得注意的一点是:从未说过部分的生命周期小于整体,也就是一定是大于或者等于整体的生命周期的。所以千万别和别人争论,引擎报废了,汽车就没用了,因为在程序中一定是某个第三方供应商维护引擎的生命周期,所以引擎和汽车没有相互维持对方生命周期的联系。

但是比较困惑就是这个生命周期理论!IoC容器提供给我们使用的注入方法无非3种,接口注入、构造函数注入、setter方法注入。每一种无不是聚合,因为产生和管理对象的控制权移交到IoC容器了。那我们口口声声的说组合优于继承其实一直都是聚合优于继承。

那是不是说,有了IoC之后,组合就应该消失呢,当然不是的。例如,某个场景里,一个类确实需要分离出一部分职责交给另外一个类,比如Builder,这部分构建实例的职责就可以通过new Builder()的方式组合应用起来。

Builder_Pattern
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
public class Person {
private String name;
private int age;

public static builder() {
return new Builder();
}

public static Builder {
private String name;
private int age;

public Builder withName(String name) {
this.name = name;
}

public Builder withAge(int age) {
this.age = age;
}

public Person build(){
Person person = new Person();
person.name = name;
person.age = age;
return person;
}
}
}
>>>
Person person = Person.builder().withName('Ryan').withAge(18).build();

结论


  • 组合优于继承
  • 因为IoC的存在,上句话得改成聚合优于继承

ThoughtWorks培训

在过去的几年里,为了进一步提升ThoughtWorks在业界的影响力,扩张无疑是最为有效的策略。但是,以提供知识流程外包(KPO)作为主要业务的ThoughtWorks,人员的成长变成了业务增长的主要瓶颈。构建“学习型组织”,减少从0(新人)到1(能够提供专业服务)经历的时间变得迫在眉睫。

现状和思考

对于培训,公司投入了大量的人力物力,我们仍然在思考和摸索什么是最有效的培训方式。如何快速培养一名合格的TWer?在当前招聘策略下,这是一个严峻的问题。由什么来指导我们做“正确“的培训,培训什么、怎么培训以及能不能达到我们的预期这些都尚未可知。

培训是需要反馈周期的,假设首次教学目的(收集受众能力metrics,得出初步结论),导入教学形式(是讲理论,做实践还是改变习惯)并对当前的产出做出量化评估(学员能力分级)。形成教学目的>教学形式>量化评估>修改当前教学目的/下次教学目的的闭环,逐步提高培训的质量和效率。

以学前班的郑大夜校为例,我们已经实践了很多期。不否认地,很多毕业生从中获益颇丰,入了ThoughtWorks的门坎。但是,我们也不得不正视每期结束后褒贬不一的反馈。我们不妨试着剖析反馈背后的问题,从一个个点切入,顺藤摸瓜,反观教学的形式、目的和整个郑大夜校编排的合理性。

首先我们回顾一下本期郑大夜校的阶段安排

阶段一(毕业到年前):
静态语言阶段

阶段二(年后到入职前):
动态语言阶段

阶段三(入职后一个月内):
工程实践阶段

第一阶段主要囊括了Java语言,面向对象编程,设计原则和模式等内容,采取了讲师讲解,学生提问交流和练习的方式;

第二阶段预计采用Javascript语言,应用动态语言的特性重新实现第一阶段的练习。但是事实上,由于培训人员变动等原因,该阶段没有遵循预期计划,而是直接进入了阶段三。

第三阶段,毕业生应用我们平时工作的流程和使用的工具,参与实现一个公司内部的固定资产管理系统,用XR的话来说就是将思想上的东西应用落地。

经过两期的培训,通过毕业生自己的在retro上的反馈以及各位mentor的观察,大体上可以看出毕业生对于第一阶段的培训内容和形式还是很认可的。比较有趣的是培训中就已经有人明确告诉我他自己越来越不会写代码了,这并不是说他能力不行,反而恰恰说明他正在提升,也证实我们培训的目的达到了——对于整个软件行业以及自身能力有清晰的认识。但是必须承认这个例子不是普遍现象,思想上的东西不是一蹴而就的,而且也不能轻松量化以待检验,至少基于当前的教学形式还做不到。

对于第二阶段的培训,学生普通反映引入的工具和框架过多,再加上课程安排松散,每个迭代的目的不甚明确,严重拖延了项目的进度,结果距离完成的目标尚远。而从mentor的角度,理论到实践的落地过程预估得还是过于乐观,不过这个罅隙在和senior dev的结对过程中得到了有效的弥合。

那么问题是我们现在的问题是什么?

在回答这个问题之前,我们先问自己一个问题:

公司想要把毕业生培养成什么人?

如果答案是一群高级技工。那么我觉得郑大夜校可以不存在,直接找一群senior dev和这些新人pair,手把手教授过程和工具,模仿着码代码。因为基于我们的培训结果,这样的效果是最快最直接的。

若答案是对技术有兴趣、有想法和自主学习的geek。我们就要真的从毕业生角度出发,明确他们缺少什么能力以及如何弥补这些短板。

那么我就以答案二为假设,结合本期培训,展开问题的论述:

第一阶段


验证理解是否到位最好的方式是做练习。问题是一些学生没能完成课上的配套练习,课后也未必补上。这点与课程设置缺少检验环节必然相关,但归根结底是学生缺乏主动性。那什么叫做有主动性?同样是布置POS机的作业,有学生在每一次培训后都会持续改进自己的程序,将新的知识点实现一遍。没人强制这么做,但他做了就是主动。

问题一来了,学生缺乏主动性。该如何解决这一问题?

第一,我不认为学生缺少学习的动力,那么可能的原因是没有明确的期望和目标。期望和目标应当来自于未来的雇主。这里有详细的论述,我就不再赘述。

第二,组内职责轮换。基于本期分组教学的模式,我们已经尝试分派一些职责,效果良好。职责轮换可以让学生因感受到压力而成长。

第三,建立惩罚制度。对于mentor而言,我们并没有有力的手段给予学生压力,我们至多会告诫说若表现不好,会影响其过试用期,但是,这样的压力距离太远。所以建议提前建立因技能等评估维度(后面会详细讨论)严重不足而拒绝offer的制度。

以上解决问题的建议是基于现有的教学形式提出的。经过和DW的讨论,这一问题或许反映了原有的教学形式本身就存在漏洞。

我们依旧遵循大学的填鸭式教学模式。要知道没有哪个行业像IT行业这样特殊:没有什么东西不能够(应该)在互联网上学到的。我们何妨换个思路,将以前的课上讲授,课后练习,下次课上检验的方式改变成提前提供课题和所用教学资源,课前学生自己学习,课上解惑的模式。这样可以最大化调动学生自主学习能力,而且学生带着问题上课的效率也远高于直接教学。

第二阶段


问题二:我们第二阶段的初衷其实是将第一阶段的思想方面的东西落地。但是大量的工具和框架,这些干扰因素严重分散了学生的注意力,原本检验学生设计和创造能力的时间被极大地缩短了。

问题三:课程安排松散,迭代的目的不清则更为严重。这反映了郑大夜校本身就是不规范的,集中体现在讲师变动频繁、能力良莠不齐,授课内容临时起意,内部的目标不清不楚等。

针对问题二,我们首先再次明确公司需要的是对技术有兴趣、有想法和自主学习的geek。所以建议不强制学生学习使用工具和框架,尽量选用简单的技术栈,重点检验设计能力。

问题三,这样的现象一直存在,而且涉及的问题有点多,我们慢慢剖析。

第一,讲师变动频繁、能力良莠本身不应该是个问题,这点可以参考TWU的办学模式。关键还是如何标准化。

第二,授课内容不固定,典型反映在临时找的session上,涉及Agile演化、scrum workshop、业务、QA和Dev技能等各个方面。考虑时间限制和学生的接受能力,对于学前班的郑大夜校而言,这样的scope未免过于庞大。或许需要重新定义郑大夜校的scope,例如:将培养卓越的软件能力提到首位,而将P1和P3的侧重弱化,延迟到TWU和On Board Training上。

第三,相较于TWU,郑大夜校缺少了一个P2P feedback的环节,我们也不知道经过培训什么样的人就满足公司的期望。这就引出了量化指标,可想而知,量化指标不是绝对量,它应该是相对于以前能力模型的增长量。举个例子:从70分上升到80分和从10分上升到60分体现得是后者成长空间更大。量化指标需要根据夜校的scope,划分出能力象限,因为我们能考察的不应该越界。

孔子曰:温故而知新,可以为师矣。我们做个recap。

教学目的


  • 拓宽毕业生的软件开发的视野,明确自身能力
  • 帮助公司尽早筛选可用之才

形式


  • 颠覆填鸭式教学,自学为主,解惑为辅
  • 分组,轮换职责
  • 入职前惩罚制度
  • 制定标准的教学流程
  • 限定夜校教学范围
  • 制定量化指标

量化评估


  • 有待确定能力象限。

什么是事务


事务是一组不可分割的SQL query语句,或者说是一个最小的工作单元
### 事务与锁的关系

为什么提出这个问题

在阅读《Java虚拟机并发编程》(Programming Concurrency on the JVM - Materning Synchronization, STM and Actors)中STM(Software Transaction Memory)时,我看到transaction特征在concurrency中的神奇应用场景:

  • 原子性:涉及一组操作,这组操作具有原子特征,比如存款和取款的组合操作。这组操作内部的所有更改要么全部成功,要么全部失败。
  • 一致性:所有并行的事务所造成的变更,从外部来看,都是一个接一个发生的。比如:存款,取款这两个独立事务。如果存款的过程中间,取款操作接入,那么取款读取的数据是旧的。待存款恢复并执行完毕,取款想要写回的数据必然无效。取款事务需要重做!所以外观来看这是存款到取款的序列,反之亦然。其实本质上,就是可见性的问题。
  • 不需要显式地运用锁,不论是读锁还是写锁。这样就为程序员提供了比较好的抽象屏障(abstract barrier)。
  • 隔离性:事务在未提交之前,所做的任何更改都不能被其他事务看到。

我看到了很多STM的好处,但是看到处理写偏斜异常(Handling Write Skew Anomaly)(可以简单理解为两个事务修改的变量不是同一个,但是两个变量之间又有约束关系)一章时,作者使用ensure函数给约束变量加了读锁。加读锁的意义在于本事务之外,其他事务无法获得该变量的写锁,自然无法修改它的值。但是这里显式地使用了锁,所以可以明确事务不是锁无关的,而且这让我联想到了数据库事务隔离级别中的可重复读(REPEATABLE_READ)。可重复读也是使用在特定记录行上使用读锁,来防止外部事务修改了该条记录行。

微妙的关系

有趣的事情来了,事务原子性能确保数据的完整性,而事务的一致性和隔离性则侧重于数据的可见性。可见性的保证在并发当中绝对和锁相关。我刚说了,事务给锁提供了抽象屏障,而且事务的隔离级别依旧仰仗锁的粒度,所以不要将事务看做银弹,以为有了事务,锁就不值一提。

数据库事务的隔离级别


为什么提出这个问题

一直被《高性能MySQL》里的解释弄得稀里糊涂,纠结于脏读、不可重复读和幻读之间的关系。而且某些解释看似合理,但完全没有指导价值。比如:阐述隔离级别,却没法从中得出我们如何结合应用场景选择合适的隔离级别。

隔离级别

  • Uncommited Read
    一个事务未提交,另一个事务却能读到该事务所做的更改。因为有可能读到未提交到数据库里的脏数据,这一级别会导致脏读。适用于只读场景下。

  • Commited Read
    未提交之前,事务之间是不可见的,所以可以阻止脏读。
    但是会导致不可重复读问题,也就是在本事务内,读取一条记录,另一个事务修改了此条记录并提交,本事务再读取同一条记录时,发现得到记录和前一条不一致的场景。这一级别也被称为不可重复读级别。
    但上述场景没有半点指导意义!
    如果你的应用场景是这样的——你想查询的变量是通过本事务里只读但是对于外部事务可写的变量作为条件查询出来的,那么这个只读变量很可能不可重复读,导致这个事务会失效。举个例子:如果你的查询语句是这样的SELECT USER.age into age FROM USERS name='YOU';
    这时候外部事务修改了名字UPDATE SET name='ME' WHERE name='YOU';并提交。那么这时候,age是无效的状态,你再拿来用就有问题了。这一级别适合于读多写少且写偏斜不存在的场景。

  • Repeatable Read (MySQL的默认隔离级别)
    可重复读,可以理解给只读的记录行加了读锁。这样,外部事务无法获得写锁,本事务内部这条记录始终有效,待事务结束即可解锁。反之,外部事务先得写锁,那么本事务无法获取记录行的读锁,导致重试发生。显然,如果你的应用场景里,读多写少且读写操作同一条记录的可能性很大的时候适合。

  • 幻读
    可重复读级别无法防止幻读。幻读是这样一种场景,本事务读取一个范围内,范围内,范围内(重要的事情写三遍)的数据集,但是另一事务又向这个范围内插入一条记录,导致数据集发生变化了,像是出现了幻觉,所以称为幻读(我很痛恨一些奇葩的科学家起的不合理的名字,这就是其一。按着这种逻辑,不可重复读不也可以说是出现幻觉吗?)。那么为什么会出现这种情况,原因是新插入的记录以前不存在于数据库中,所以你没法为它加锁。而且可重复读只是为每行记录加锁,没有用到Range Lock,这一幻影插入操作总能成功。
    不过MySQL中InnoDB存储引擎提供了MVCC(多版本并发控制)技术,为每条记录设置一个递增的事务编号,大于本次事务编号的记录,不准插入记录。
    可重复读+MVCC即可解决并发中的大部分问题。

  • Serializable Read
    顾名思义,串行读,事务之间是串行的,同步的。换言之,并发性剧减。

STM的隔离级别


  • 隔离级别处于提交读。

事务的级别


描述的是事务本身的属性

  • ReadOnly:所有的操作都是读取操作,不涉及任何产生副作用的操作。

什么是竞态条件?


tips: The situation where two threads compete for the same resource, where the sequence in which the resource is accessed is significant, is called race conditions. A code section that leads to race conditions is called a critical section. In the below example the method add() is a critical section, leading to race conditions. Race conditions can be avoided by proper thread synchronization in critical sections.
两个线程竞争同一个资源,而该资源的访问顺序十分重要的情况就被称为竞态条件。导致竞态条件的代码区就被称为临界区。下面的例子中的add()方法就是一个导致竞态条件的临界区。临界区上合适的线程同步能避免竞态条件。
Race conditions arise in software when an application depends on the sequence or timing of processes or threads for it to operate properly.
race conditions often happen when the processes or threads depend on some shared state.
软件里的竞态条件发生在一个应用程序依赖进程或者线程的执行顺序和时间以确保该程序执行正确的时候。竞态条件总是发生在进程或线程依赖某些共享资源的时候。

一个临界区的例子:

1
2
3
4
5
6
7
public class Counter {
protected long count = 0;

public void add(long value){
this.count = this.count + value;
}
}

什么是线程安全?


tips: Code that is safe to call by multiple threads simultanously is called thread safe. If a piece of code is thread safe, then it contains no race conditions.
能被多个线程同时安全地调用的代码就是线程安全。如果一段代码是安全的,那么它就不存在竞态条件。

  • Thread safe: Implementation is guaranteed to be free of race conditions when accessed by multiple threads simultaneously.
  • Conditionally safe: Different threads can access different objects simultaneously, and access to shared data is protected from race conditions.
  • Not thread safe: Code should not be accessed simultaneously by different threads.
  • 线程安全:实现以保证多个线程同时访问不存在竞态条件
  • 条件安全:不同的线程可以同时访问不同的对象,且在访问共享资源时保护以免于竞态条件
  • 非线程安全:代码不能被多个线程同时访问

可见性和竞态条件的关系?


如果存在竞态条件,那么就必须保证变量的可见性

可见性和线程安全的关系?


严格意义上,线程安全其实是相对于非线程安全的,即包含了线程安全和条件安全两部分。

Below we discuss two approaches for avoiding race conditions to achieve thread safety.

The first class of approaches focuses on avoiding shared state, and includes:
也就是纯粹的线程安全。

  • Re-entrancy
    Writing code in such a way that it can be partially executed by a thread, reexecuted by the same thread or simultaneously executed by another thread and still correctly complete the original execution. This requires the saving of state information in variables local to each execution, usually on a stack, instead of in static or global variables or other non-local state. All non-local state must be accessed through atomic operations and the data-structures must also be reentrant.
  • Thread-local storage
    Variables are localized so that each thread has its own private copy. These variables retain their values across subroutine and other code boundaries, and are thread-safe since they are local to each thread, even though the code which accesses them might be executed simultaneously by another thread.
    The second class of approaches are synchronization-related, and are used in situations where shared state cannot be avoided:
    也就是条件安全部分。
  • Mutual exclusion
    Access to shared data is serialized using mechanisms that ensure only one thread reads or writes to the shared data at any time. Incorporation of mutual exclusion needs to be well thought out, since improper usage can lead to side-effects like deadlocks, livelocks and resource starvation.
  • Atomic operations
    Shared data are accessed by using atomic operations which cannot be interrupted by other threads. This usually requires using special machine language instructions, which might be available in a runtime library. Since the operations are atomic, the shared data are always kept in a valid state, no matter how other threads access it. Atomic operations form the basis of many thread locking mechanisms, and are used to implement mutual exclusion primitives.
  • Immutable objects
    The state of an object cannot be changed after construction. This implies both that only read-only data is shared and that inherent thread safety is attained. Mutable (non-const) operations can then be implemented in such a way that they create new objects instead of modifying existing ones. This approach is used by the string implementations in Java, C# and Python.

    所以可见性只是针对于条件安全部分而言的。

竞态条件和线程安全的关系?


避免竞态条件以获得线程安全。换句话说,避免竞态条件是线程安全的充要条件。

为什么说跨越内存栅栏(可见性)和避免竞态条件是同步相关的两大主要问题?


跨越内存栅栏(可见性)是应对竞态条件的一种方式,出现竞态条件必须确保可见性。举个例子:一个写线程写完之后,要保证所做更改对其它线程可见,否则会让其它线程读到脏数据。
避免竞态条件的方式有很多种,包括上述的线程安全的各种手段。

相关术语


  1. livelock(活锁):请求一个锁的时候不断失败。
  2. starvation(饿死):无法定期访问共享资源来执行,发生在某个线程长期霸占共享资源的时候。
  • Starvation
    Starvation describes a situation where a thread is unable to gain regular access to shared resources and is unable to make progress. This happens when shared resources are made unavailable for long periods by “greedy” threads. For example, suppose an object provides a synchronized method that often takes a long time to return. If one thread invokes this method frequently, other threads that also need frequent synchronized access to the same object will often be blocked.
  • Livelock
    A thread often acts in response to the action of another thread. If the other thread’s action is also a response to the action of another thread, then livelock may result. As with deadlock, livelocked threads are unable to make further progress. However, the threads are not blocked — they are simply too busy responding to each other to resume work. This is comparable to two people attempting to pass each other in a corridor: Alphonse moves to his left to let Gaston pass, while Gaston moves to his right to let Alphonse pass. Seeing that they are still blocking each other, Alphone moves to his right, while Gaston moves to his left. They’re still blocking each other, so…

你说,昨天。

有一片天地,

在氤氲的叹息里,

幻做纸张,

将我和你写入了泛黄。

你说,今天。

我只是一支尾音,

很小心地,

从你的心底淌出。

然后,

躲在雨水里——变轻。

String.valueOf


example.java
1
2
3
4
String.valueOf(null);

->
java.lang.NullPointerException

why does it cause excepation? Java call the wrong method because type match!

String.java
1
2
3
4
5
6
7
public static String valueOf(char data[]) {
return new String(data);
}
...
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length); //=> cause exception.
}

how to correct?

1
2
3
String.valueOf((Object)null)
->
null

Varargs bug


Changes in Most Specific Varargs Method Selection
Description: The overload resolution algorithm in the javac compiler has been fixed in how it selects the most specific varargs method when more than one method is applicable to a given call-site (see the JLS, Java SE 7 Edition, section 15.12.2.5). Because of a bug, both JDK 5.0 and JDK 6 compilers reject the following legal code:

1
2
3
4
5
6
7
8
class Test {
void foo(int... i) {}
void foo(double... d) {}

void test() {
foo(1,2,3);
}
}

In the above example, both methods are applicable (because you can pass an int where a double is expected). Since both methods are applicable, the compiler must select the so-called most-specific method, that is, the best candidate among the two. This is done by looking at the signatures of both methods; in this case, since one method (foo(double…)) is accepting an argument that is more general than the other (foo(int…)), the answer is straightforward: the most specific method is foo(int…).
While the javac compiler accepts more code than it did prior to JDK 7, this fix also results in a slight source incompatibility in the following case:

1
2
3
4
5
6
7
8
class Test {
void foo(int... i) {}
void foo(Object... o) {}

void test() {
foo(1,2,3);
}
}

This code compiles in JDK 6 (the most specific method is foo(int…)). This code does not compile under JDK 7. As per 15.12.2.5, it is not possible to choose between foo(int…) and foo(Object…) as neither int is a subtype of Object, nor Object is a subtype of int. This program should be disallowed (in fact, it should never have been allowed in the first place).

0%