λ

(conj clojurians me)

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


继承具有强耦合(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).

Promise对象最早被C++工程师使用,接着以Deferred对象出现在Python中。随着NodeJS的兴起,Promise在javascript中的应用越来越广。

大凡技术成熟之后都会逐渐形成一个标准,Promise的标准定义在CommonJS中,全称是Promise/A,不过新近出现了改良版的Promise/A+,只不过它仅仅对原来的标准部分澄清,同时依据实践稍作扩展。

The core Promises/A+ specification does not deal with how to create, fulfill, or reject promises, choosing instead to focus on providing an interoperable(协作的) then method. Future work in companion specifications may touch on these subjects.

但是使用Promise还是有些地方值得留意的。

1. Then的Resovler Function不返回任何值


举个例子,打印deferred数组的首个元素

promise_sample.js
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
function printFirstAndLast(itemsDeferred){
findFirst(itemsDeferred).then(util.puts); //=> 0

last(itemsDeferred).then(function(last){
var deferred = defer();
deferred.resolve(last);
findFirst(deferred).then(util.puts) //=> 1
}).then(function(result){
console.log('result:', result); //=> 2
}, function(reason){
console.log('reason:', reason);
});
}

function last(itemsDeferred) {
return itemsDeferred.then(function(items){
return items.slice(1, items.length);
});
}

function findFirst(itemsDeferred){
return itemsDeferred.then(function(items){
return items[0];
});
}
---
var deferred = defer();
deferred.resolve([1, 2, 3, 4, 5]);
printFirstAndLast(deferred);

->
1
result: undefined
2

从输出看,由于我故意没有返回1处的Promise对象,最后一个then 的2处代码先被执行,打印出undefined,然后才会执行完1处的代码,打印出数字2。

这样的现象有两处值得注意的地方

  • 执行顺序混乱
  • 最后then的结果是undefined

顺序混乱是由于标识为1处的resolve的时间较长,回调的时间较久,所以后执行。

2处为undefined,是因为1处没有返回值,我们知道javascript的function始终会返回值且这两种方式:return;(返回void)和不显式写return语句都会返回undefined。所以将值为undefined的promise对象传给下一个then方法。

2. Then的Rejector Function不返回Reject Promise


reject.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function printFirstAndLast(itemsDeferred){
return itemDeferred.then(function(){}, function(reason){
util.puts('1:'+reason);
return 'error-1';
}).then(function(result){
util.puts('resolve 2:'+result);
}, function(reason){
util.puts('reject 2:'+reason);
});
}

var deferred = defer();
deferred.reject('error-0');
printFirstAndLast(deferred);

->
1: error-0
resolve 2:error-1

从输出看,如果reject function不返回reject promise,而只是error-1这样的值,那么调用then方法,执行的还是resolve方法。

从Promise/A+参考描述可证明上述

promise2 = promise1.then(onFulfilled, onRejected);

If either onFulfilled or onRejected returns a value x, run the Promise Resolution Procedure [[Resolve]](promise2, x).

If x is a thenable, it attempts to make promise adopt the state of x, under the assumption that x behaves at least somewhat like a promise. Otherwise, it fulfills promise with the value x.

3. Then的Resolver Function返回的是对象,而且包含then方法


obj_then.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

var obj = {then: function(resolver, reject){
return resolver('i am an obj.');
}};

function printFirstAndLast(itemsDeferred){
return itemDeferred.then(function(){}, function(reason){
util.puts('1:'+reason);
return obj; //=> 1
}).then(function(result){
util.puts('resolve 2:'+result);
}, function(reason){
util.puts('reject 2:'+reason);
});
}

var deferred = defer();
deferred.reject('error-0');
printFirstAndLast(deferred);

->
1: error-0
resolve 2:i am an obj

从结果看,obj这个含有then方法的对象,在1处被调用了then,同时将resolver('i am an obj.')作为Promise返回。

从Promise/A+可知

If then is a function, call it with x as this, first argument resolvePromise, and second argument rejectPromise, where:

If/when resolvePromise is called with a value y, run [[Resolve]](promise, y)

If/when rejectPromise is called with a reason r, reject promise with r

那么如果是以下这些对象

  • {then: ‘something’}
  • {then: function(){}}
  • {A: ‘A’}
  • {then: function(resolver, rejector){return rejector(“error”);}}

结果会是

  • resolve 2:[Object Object]
  • 忽略
  • resolve 2:[Object Object]
  • reject 2: error

singletons suffer state pollution between tests

singletons.js
1
2
3
4
5
6
module.exports = {
counter: 0,
inc: function() {
return ++this.counter;
}
};
test.js
1
2
3
4
5
6
7
8
9
10
11
var singleton = require('singletons');
module.exports = testCase({
"should equal one after calling inc": function (test) {
test.equal(1, singleton.inc());
test.done();
},
"should get one after calling inc": function(test) {
test.equal(1, singleton.inc());
test.done();
}
});

结果第二个测试会失败,但是从测试本身是看不出为什么第一个可以通过,而第二个则相反。当然,我们可以通过setUptearDown方法来重置对象的状态,但是这无疑增加了维护测试的成本,你得记着重置对象这件事本身就是负担。

下面就是解决方案

singletons_impr.js
1
2
3
4
5
6
7
8
module.exports = function() {
return {
counter: 0,
inc: function() {
return ++this.counter;
}
};
};

再看我们的测试

test.js
1
2
3
4
5
6
7
8
9
10
11
var singleton = require('singletons_impr');
module.exports = testCase({
"should equal one after calling inc": function (test) {
test.equal(1, singleton().inc());
test.done();
},
"should get one after calling inc": function(test) {
test.equal(1, singleton().inc());
test.done();
}
});

这样两次返回的对象都是全新的,测试通过。

不过,似乎这样让测试变得更加麻烦了,好处不明显。但是结合这篇文章[Javascript Modularize 2nd],就会巧妙地解决多个模块依赖某一个模块,某个模块修改被传染至其他模块的问题。因为每个被引入的模块都将被重新执行生成一遍,成为独立的对象。

结论

  • module.exports最好使用function方式导出。

参考链接
[1] Writing Testable JavaScript

The extent of a scope refers to the lifetime of a variable (i.e., how long a variable holds a certain value)

Global Scope

Javascript中任意不使用var创建的变量具有全局作用域。

global
1
2
3
4
5
6
7
8
9
10
globalVariable = "global";
(function() {
console.log(globalVariable);
})()

-> global

delete globalVariable;

-> true

当然,任意定义在文件最顶层的变量,事实上,由于Javascript Hositing作用,所有定义在最外层的变量都会被提升至作用域首部,都具有全局作用域。

值得注意的是使用var定义的变量是不能delete的,它不是全局变量的属性:

del_variable
1
2
3
4
var globalVariable = 'global';
delete globalVariable;

-> false

除此之外,任意被暴露到最外层的变量,一旦被全局作用域捕获,其本身都会存在被随意修改的风险。

Lexical Scope

Lexical scope refers to the visibility of a variable and its value analogous to its textual representation.

Javascript会从内向外寻找变量的绑定,所以变量的定义距离使用处最近,则该变量会被使用。

lexical_scope
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function lexicalScope() {
var lexicalVariable = "outer";
if(true) {
var lexicalVariable = "inner";
console.log(lexicalVariable);
(function(){
var lexicalVariable = "innerMost";
console.log(lexicalVariable);
})();
console.log(lexicalVariable);
}
}

lexicalScope();
-> inner
innerMost
inner

Dynamic Scope

the value of any given binding cannot be known until the caller of any given function is known— which may be too late.

dynamic_scope
1
2
3
4
5
6
7
function globalThis() { return this; }
globalThis();
-> some global object, probably Window
globalThis.call('barnabas');
-> 'barnabas'
globalThis.apply('orsulak', [])
-> 'orsulak'

在Javascript中,this所处的作用域就是动态作用域。也就是说,globalThis()返回值完全由调用方决定,this变量的对照表是不断改变的。更多参考这里

Function Scope

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function strangerIdentity(n) {
for(this['i'] = 0; this['i'] < n; this['i']++);
return this['i'];
}

--- case 1
strangerIdentity(100);
-> 100
i
-> 100

--- case 2
var id = new strangerIdentity(100);

id
-> strangerIdentity {i: 100}
id.i
-> 100
i
-> Uncaught ReferenceError: i is not defined

在Javascript中,只有一个Function才会产生新的作用域。直接调用一个funcation,其内部的指针指向global对象。但当使用new后,其作用域绑定到一个Function Object上。

Javascript的赋值操作就是引用修改,举个例子:

reference.jsgrunt-properties
1
2
3
4
5
6
7
8
9
10
11
12
13
function splitKeys(obj, splitter) {
var keys, value, parent, result = {};
for (var key in obj) {
keys = key.split(splitter);
value = obj[key].replace(/"/g, '\\"');
parent = result;
for (var j = 0; j < keys.length-1; j++) {
parent = parent[keys[j]] = parent[keys[j]] || {};
}
parent[keys[keys.length-1]] = value;
}
return JSON.stringify(result, null, 2);
}

这段代码将Java程序中的常见的Properties文件中的键值映射成了嵌套的JSON对象:

test.js
1
2
3
4
5
6
7
8
9
10
var obj = {"country.province.city": "Chengdu"};
splitKeys(obj, '.');

-> "{
"country": {
"province": {
"city": "Chengdu"
}
}
}"

我们从实现代码的这行开始看起parent = result;,很简单地把parent指向result这个空对象。

再看接下来的代码,也是最巧妙的地方:

parts
1
2
3
for (var j = 0; j < keys.length-1; j++) {
parent = parent[keys[j]] = parent[keys[j]] || {};
}

第一次循环:
parent开始是空对象,parent = parent['country'] = parent['country'] || {};,将会生成一个parent的country属性,该属性的值还是一个空对象。于此同时,parent会指向新生成的、也即它的country属性。

第二次循环:
parent指向了country,parent = parent['province'] = parent['province'] || {};也即parent = country['province'] = country['province'] || {};,此时,将会生成一个country的province属性,该属性的值还是一个空对象,而parent则指向了province对象。

所以 parent[keys[keys.length-1]] = value;等价为province['city'] = "Chengdu";

最后,return JSON.stringify(result, null, 2);,这最后的result就是具有嵌套效果的JSON对象了。

0%