想象你是某国总统,国内形势动荡不安,民众对你的支持率已经降到冰点。突然接到消息,全副武装的游击队冲入了某大使馆,数百政要沦为人质,其中甚至还有你的母亲和兄弟。游击队开出了一系列苛刻的条件,你如何应对?是老老实实地合作?还是义正严辞地竞争?

这一场景并非来自某部虚构的电影,而是1990年秘鲁真实发生的事件。秘鲁前总统藤森的策略是——既合作又竞争。故事的结局我们在系列文章的最后讲,先问一个问题,为什么要既合作又竞争?

首先,资源是稀缺的,我们不得不通过合作或者竞争去生产、获取、保护甚至掠夺资源。其次,人类是社交动物,拉帮结派培养感情需要合作竞争。最后,这个世界是动态的,没有永远的朋友,也没有永远的敌人。所以,合作或者是竞争只是达到目的的手段,而不是目的本身。

哥伦比亚大学社会心理学家亚当·加林斯基的这本《朋友与敌人:卓有成效的合作与竞争》,旁征博引了大量最新的研究成果,来更新我们对合作与竞争的世界观和方法论——让我们成为可靠的朋友的同时,也可以成为更可怕的敌人。

不患贫而患不均

为什么英国政客米利班德两兄弟会反目成仇?为什么威廉姆斯姐妹能够制霸网坛多年?为什么铜牌得主总是会比银牌得主更开心?为什么经济萧条时期找到工作得人尽管工资低但是更有幸福感? 为什么半场稍微落后会激励队伍取得最终的胜利?

因为比较!社会比较能够促进合作或者激励竞争。它无可避免地有两个方向:向上比让人自我感觉糟糕,但是会让我们努力奋斗;向下比较让人自我感觉良好,但是会让我们安于现状。极端情况下,向上比较过了头,会导致疯狂的竞争,甚至不惜作弊搞破坏。

不患贫而患不均,对不公平的愤懑已经写到了动物的本能里。埃默里大学的研究者教会了猴子们使用石子作为货币,在猴子们的世界里,显然甜美多汁的葡萄要比黄瓜更值钱。当他们发现,付出同样数量的石子,自己只能得到黄瓜,而他们的邻居却得到了葡萄,他们抓狂了——把黄瓜扔了回去,甚至冲到隔壁把葡萄抢了过来。

那我们个人如何去利用这种比较呢?有四项基本原则:

  1. 调整期望。如果我们期望别人赢,别人赢了我们的滋味不会特别不好受。

  2. 在新的领域竞争,这一次的失败就会转化成下一次的动力。

  3. 我们得意识到我们的成功会让别人不舒服,即便他们没有告诉你。想要消除这种不悦,可以分享负面的信息。

  4. 向上比较激励自己,向下比较让自己开心。同样的规则也可以应用到谈判上:谈判前向上比较激励自己找到最好的deal,谈判后向下比较让自己满意。

既然一切都是比较,人们最常比较的是什么呢?答案是权力。

权力越大,“脑洞”越大

惠普的前CEO马克·赫德是出身草莽的典范,他花了二十年在NCR公司从一个小销售员成长为COO,后来又被挖到了惠普做CEO。惠普业绩飙升的同时赫德的生活也极尽奢华——直到他遇到茱蒂·费雪。赫德花公司的钱追求费雪,因此丢了工作。奇了怪了,你说他既然这么聪明这么有钱,又为什么要冒这个险呢?

哲学家罗素曾经说过,社会科学的根本概念是权力,正如物理学的基本概念是能量。权力的本质是藉由对稀缺资源的掌握,或供给或剥夺,来控制他人的行为。而现实世界的资源是会发生变化的——权力就像圣经故事中参孙的头发,参孙头发被剪,力量就会消失;人没有了资源,权力就会消失。

而我们常常会忽视这一点,如果我们感到很有权力,就会认为和自我表现得很有权力。这也是我们可以利用到的地方——想要表现得更自信更有力吗?好,只要让自己感受到有权力就可以了。你可以想象你拥有某项重要资源、对人颐指气使,回忆你人生中最孔武有力的那一刻,甚至仅仅只是叉腰站着,或者枕着头往后躺,你就会能感受到权力!是的,人的行为可以影响人的感受,这种扩张性的姿势也被称作权力姿势。西北大学最新的研究表明,甚至音乐也可以让人产生权力感。

权力感滋味是如此的美妙,让人自以为无可战胜,以至于人的大脑也会随之改变。UCLA的研究者发现,权力感让人的大脑难以激活前额叶皮质和扣带皮层,这也就意味着人会难以换位思考,更容易忽视其他人的存在。当权者忽视其他人的存在,就会严于律他,宽以待己,这就是所谓的“虚伪”——而虚伪的当权者,不会当权太久。当权,却地位低下,则是一种更具毒性的组合,会产生比如美军在伊拉克的虐囚门事件。所以下一次你看到类似于像赫德这样的大老板落马的时候,你可以说,权力腐蚀他的”脑洞”如此之大,以至于他们就像是高速上的飙车党,看不到路上还有别人,以为整个路都是自己的。

权力感太强让人看不到他人,权力感太弱让人没有自信。对于自己,我们需要找到一个平衡——比如面试的时候,我们因该表现得既自信又恭敬。当着老板的面摆出权力姿势会损害老板的权威,所以要记得这套姿势得提前做。对于权力感爆棚的领导,我们要学会把他的注意力引导到团队目标上。当他们做决定的时候,多让他们讲讲为什么要这么做。当然了,最好的办法,是选一位与人为善的领导——你看看他们怎么对待小老百姓,就知道啦。

谈完了权力与个人,我们再来谈谈组织。

等级制度管用吗?

如何协调人与人的合作?如何协调个人利益与组织利益?答案很简单——通过等级制度。因为有合作就有分工,在分工中,观其大略的我们称之为领导者,只见树木不见森林的我们称之为追随者。等级简化了组织里面的人际关系,组织里的人的目标会更清晰。时局越是混乱,越是不利,它带来的确定性就越宝贵。正是因为它能帮助人们这么多难题,难怪这种组织形态无所不在,它能很快地被确立,一旦确立了,又很难被打破。谷歌的拉里·佩奇和谢尔盖·布林曾经一度撤掉了整个公司的经理层,但是不久之后整个组织陷入了混乱与困惑。你看,连谷歌也需要等级制度。

那员工是不是真的就那么傻,愿意眼巴巴的把更多的蛋糕分给老板呢?答案是“并非如此”——如果跟着领导一起能够很好的树立“确定性”,有更多的产出,因此把蛋糕做大,那多分一点给老板又有什么不可以呢?所以说,按照分蛋糕多少的标准来衡量,单干、做老板、合伙、打工从来都不是问题,问题是怎样才能把自己到手的蛋糕最大化。

当等级失去了确定性,组织就会陷入灾难。正所谓一山不容二虎,有时候,艄公多了打翻船。在完成任务需要高水平协作竞技的领域,团队成员互相依赖对方达成目标,比如行军作战、足球、篮球、商务,这种确定性尤为宝贵,因此在这种情况下,团队中能人太多反而不好。而如果任务相互独立,比如做科研、打棒球,能人就是越多越好。

古板的等级制度的另一个弱点在于,会导致人微言轻——扼杀思想,甚至人的生命。而在认知复杂度高的领域,事物的发展迅速而多变,人就更容易犯错,犯错的结果也会更严重。单个人的思想就弥足珍贵了——毕竟做事多一双眼睛盯着,看问题多一个的角度,也就多一份安全。2014年韩国世越号沉没事故就应证了等级制度的严重弊端,在船就要沉没的顷刻之间,自上而下的决策过程实在太慢了!这不是个例,哥伦比亚大学的研究者分析了喜马拉雅山登山者的数据,发现来自等级鲜明的国家的登山者更容易死亡。很可能是因为,在复杂多变的环境中,即便这些登山者看到了各种各样的问题,但是他们也不会轻易地说出口,这样做虽然保全了秩序,但是却付出了宝贵的生命!

那我们如何能够鼓励他们多说呢?一个简单的法则是保证说话者的心理安全,让他们感觉到分享见解是受到鼓励的,而这些见解正是生产高质量和创新性想法的土壤。

总的来说,我们需要等级制度在混乱中建立秩序。面对认知复杂的、多变的环境,或者互相之间相对独立的任务的时候,单个人起到的作用越大,等级就不太管用,甚至会起反作用了。

讲完了组织内部的权力分布——等级制度,我们再来说组织与社会。

女领导的双刃剑和妈妈熊之盾

经过多年的艰苦努力,安·霍普金斯终于在普华永道赢来了晋升合伙人的拐点。她的业绩打败了其他的87位参选者,大家都纷纷称赞她为“卓越”,“项目管理能力高超”——然而不幸的是,只有不到一半的合伙人赞成她的晋升。“果断决绝、咄咄逼人”对于男性来说是成功的加速器,而对于女性来说是双刃剑,因为这有悖于社会传统对女性的刻板期望——温暖如春、善于合作。

这种对女性的刻板印象来自于社会中男女的权力差异。比如,对于“女孩子的数学没有男孩子好”这一观点,研究者遍历了40多个国家的比萨测试(PISA, Programme for International Student Assessment)成绩,发现,越是男女平等的国家,男生女生的数学成绩的差异越小。

女性感觉没有权力,就不会去为自己争取更大的利益。拿到一份新工作不去争取工资,工资就会更低。而女性如果去争取,便又陷入了两难的困局:如果争取了,说不定对方就会一怒之下收回这份工作机会……

这种社会加给性别的偏见不仅仅来自男性,也同样来自女性。其中的典型便是“蜂后综合症”,这些“蜂后”周围一般被众多男性环绕,少有或没有女性簇拥,她们在高社会地位的群体中突破发展瓶颈、获得要职,却处处针对那些对他们“王座”有威胁的优秀的女性。看到这儿,权利的游戏中瑟曦一脸不爽地看小玫瑰的镜头就显得极为经典了。这种情况确实令人沮丧,解决问题的关键在于保证“蜂后”的安全感,不过令人欣慰的是,在这个时代,随着更多的女性成为领导者,这种现象也在逐渐消失。

在组织和社会层面,要想真正改变性别不平等的问题,关键在于人们要真真正正地意识到,平等能够让蛋糕变大。哈佛大学的研究者发现,单纯的多样性培训对增加管理层女性的效果为零,实际上,反而减少了管理层中黑人女性的数量。真正能够起作用的是来自组织顶层的支持和许诺,并且直接参与到帮助女性和少数群体的社交与指导中来。同时,招聘和晋升机制要做到公平公正。

在女性的自我层面,我们首先要在自身上消除这种区别,用实实在在的数据说话,在评判者带上有色眼镜之前就设定标准,一碗水端平。如果别人的有色眼镜已经戴上了怎么办?多问问自己——假设我自己是男性,我会对眼前的情况作出同样的反应吗?害羞的亚裔男性要假设自己是白人男性,我会对眼前的情况作出同样的反应吗?

应对女领导双刃剑的最佳盾牌是,做个“妈妈熊”——想要自己争取利益?没问题!关键在于为“我”争取利益的同时,也要为“我们”争取利益。这也解释了为什么女性做律师要比做其他职业更容易成功,因为人们就是预期她们表现得自信决绝,替人争取利益。

小结

最后我们总结一下这一部分的内容

  • 不患贫而患不均——向上比可以激励自己,向下比让自己开心。比较无处不在,常见的比较本质上是权力的比较。
  • 权力是合作与竞争的催化剂。权力的来源是对资源的控制,而即刻影响人的行为的却是权力产生的感觉。
  • 权力感爆棚的领导人就像是高速上的飙车党,看不到路上还有别人,以为整个路都是自己的。同样的道理,我们可以摆出“权力姿势”让自己感觉更有力,以便更自信地去竞争,但是别忘了表现出换位思考的恭敬来合作。
  • 权力放到组织里就产生了等级制度,等级在混乱中建立秩序,但是在“人”越重要的地方,越不管用。
  • 权力放到社会中就产生了偏见甚至歧视,要解决这个问题,就得让掌权者真真正正地意识到,平等能够把蛋糕做大。女性面对“双刃剑”的策略是做个“妈妈熊”,就是在表现得自信决绝的同时,别忘了为“我们”而不仅仅是“我”争取利益。

Building applications is not a hard thing, but having the vision of the overall architecture really makes a difference. We have to crack the system interview sooner or later in our career as a software engineer or engineering manager.

Interviewers are looking for future teammates that they like to work with. The future teammates are expected to be, at least, capable of solving problems independently. There are so many solutions to any given problem, but not all of them are suited given the context. So the interviewee has to specify different choices and their tradeoffs. To summarize, system design interview is a happy show-off of our knowledge on technologies and their tradeoffs. Ideally, keep talking what the interviewer expect throughout the interview, before they even have to ask.

Keep talking for 45 mins could be easy, as long as we are armed with the following four steps and three common topics. Take “design Pinterest” for example.

1. Four steps to Crack the System Design Interview

Breaking down a complex task into small chunks helps us handle the problem at a better pace and in a more actionable way.

1.1 Step 1. Clarify Requirements and Specs

First things first, the ultimate goals should always be clear.

Pinterest is a highly scalable photo-sharing service:

  • features: user profile, upload photos, news feed, etc.
  • scaling out: horizontal scalability and micro services.

1.2 Step 2. Sketch Out High Level Design

Do not dive into details before outlining the big picture. Otherwise, going off too far towards a wrong direction would make it harder to even provide a roughly correct solution. We will regret wasting time on irrelevant details when we do not have time to finish the task.

OK, let us sketch out the following diagram without concerning too much about the implementation detail of these components.

pinterest architecture

So far so good! To some extent, congrats, we have solved the problem!

1.3 Step 3. Discuss individual components and how they interact in detail

When we truly understand a system, we should be able to identify what each component is and explain how they interact with one another. Take these components in the above diagram and specify each one by one. This could lead to more general discussions, such as the three common topics in Section 2, and to more specific domains, like how to design the photo storage data layout

1.3.1 Load Balancer

Generally speaking, load balancers fall into three categories:

  • DNS Round Robin (rarely used): clients get a randomly-ordered list of IP addresses.
    • pros: easy to implement and free
    • cons: hard to control and not responsive, since DNS cache needs time to expire
  • L3/L4 Load Balancer: traffic is routed by IP address and port. L3 is network layer (IP). L4 is session layer (TCP).
    • pros: better granularity, simple, responsive
  • L7 Load Balancer: traffic is routed by what is inside the HTTP protocol. L7 is application layer (HTTP).

It is good enough to talk in this level of detail on this topic, but in case the interviewer wants more, we can suggest exact algorithms like round robin, weighted round robin, least loaded, least loaded with slow start, utilization limit, latency, cascade, etc.

1.3.2 Reverse Proxy

Reverse proxy, like varnish, centralizes internal services and provides unified interfaces to the public. For example, www.example.com/index and www.example.com/sports appear to come from the same domain, but in fact they are from different micro services behind the reverse proxy. Reverse proxy could also help with caching and load balancing.

1.3.3 (Frontend) Web Tier

This is where web pages are served, and usually combined with the service / backend tier in the very early stage of a web service.

Stateless

There are two major bottlenecks of the whole system – requests per second (rps) and bandwidth. We could improve the situation by using more efficient tech stack, like frameworks with async and non-blocking reactor pattern, and enhancing the hardware, like scaling up (aka vertical scaling) or scaling out (aka horizontal scaling).

Internet companies prefer scaling out, since it is more cost-efficient with a huge number of commodity machines. This is also good for recruiting, because the target skillsets are equipped by. After all, people rarely play with super computers or mainframes at home.

Frontend web tier and service tier must be stateless in order to add or remove hosts conveniently, thus achieving horizontal scalability. As for feature switch or configs, there could be a database table / standalone service to keep those states.

Web Application and API

MVC(MVT) or MVVC pattern is the dominant pattern for this layer. Traditionally, view or template is rendered to HTML by the server at runtime. In the age of mobile computing, view can be as simple as serving the minimal package of data transporting to the mobile devices, which is called web API. People believe that the API can be shared by clients and browsers. And that is why single page web applications are becoming more and more popular, especially with the assistance of frontend frameworks like react.js, angular.js, backbone.js, etc.

1.3.4 App Service Tier

The single responsibility principle advocates small and autonomous services that work together, so that each service can do one thing well and not block others. Small teams with small services can plan much more aggressively for the sake of hyper-growth.

Service Discovery

How do those services find each other? Zookeeper is a popular and centralized choice. Instances with name, address, port, etc. are registered into the path in ZooKeeper for each service. If one service does not know where to find another service, it can query Zookeeper for the location and memorize it until that location is unavailable.

Zookeeper is a CP system in terms of CAP theorem (See Section 2.3 for more discussion), which means it stays consistent in the case of failures, but the leader of the centralized consensus will be unavailable for registering new services.

In contrast to Zookeeper, Uber is doing interesting work in a decentralized way, named hyperbahn, based on Ringpop consisten hash ring. Read Amazon’s Dynamo to understand AP and eventual consistency.

Micro Services

For the Pinterest case, these micro services could be user profile, follower, feed, search, spam, etc. Any of those topics could lead to an in-depth discussion. Useful links are listed in Section 3: Future Studies, to help us deal with them.

However, for a general interview question like “design Pinterest”, it is good enough to leave those services as black boxes.. If we want to show more passion, elaborate with some sample endpoints / APIs for those services would be great.

1.3.5 Data Tier

Although a relational database can do almost all the storage work, please remember do not save a blob, like a photo, into a relational database, and choose the right database for the right service. For example, read performance is important for follower service, therefore it makes sense to use a key-value cache. Feeds are generated as time passes by, so HBase / Cassandra’s timestamp index is a great fit for this use case. Users have relationships with other users or objects, so a relational database is our choice by default in an user profile service.

Data and storage is a rather wide topic, and we will discuss it later in Section 2.2 Storage.

1.4 (Optional) Step 4. Back-of-the-envelope Calculation

The final step, estimating how many machines are required, is optional, because time is probably up after all the discussions above and three common topics below. In case we run into this topic, we’d better prepare for it as well. It is a little tricky… we need come up with some variables and a function first, and then make some guesses for the values of those variables, and finally calculate the result.

The cost is a function of CPU, RAM, storage, bandwidth, number and size of the images uploaded each day.

  • N users 1010
  • i images / (user * day) 10
  • s size in bytes / image 106
  • viewed v times / image 100
  • d days
  • h requests / sec 104 (bottleneck)
  • b bandwidth (bottleneck)
  • Server cost: $1000 / server
  • Storage cost: $0.1 / GB
  • Storage = Nisd

Remember the two bottlenecks we mentioned in section 1.3.3 Web Tier? – requests per second (rps) and bandwidth. So the final expression would be

Number of required servers = max(Niv/h, Nivs/b)

2 Three Common Topics

There are three common topics that could be applied to almost every system design question. They are extracted and summarized in this section.

2.1 Communication

How do different components interact with each other? – communication protocols.

Here is a simple comparison of those protocols.

  • UDP and TCP are both transport layer protocols. TCP is reliable and connection-based. UDP is connectionless and unreliable.
  • HTTP is in the application layer and normally TCP based, since HTTP assumes a reliable transport.
  • RPC, an application layer protocol, is an inter-process communication that allows a computer program to cause a subroutine or procedure to execute in another address space (commonly on another computer on a shared network), without the programmer explicitly coding the details for this remote interaction. That is, the programmer writes essentially the same code whether the subroutine is local to the executing program, or remote. In an Object-Oriented Programming context, RPC is also called remote invocation or remote method invocation (RMI).
Further discussions

Since RPC is super useful, some interviewers may ask how RPC works. The following picture is a brief answer.

RPC

*Stub procedure: a local procedure that marshals the procedure identifier and the arguments into a request message, and then to send via its communication module to the server. When the reply message arrives, it unmarshals the results.

We do not have to implement our own RPC protocols. There are off-the-shelf frameworks.

  • Google Protobuf: an open source RPC with only APIs but no RPC implementations. Smaller serialized data and slightly faster. Better documentations and cleaner APIs.
  • Facebook Thrift: supports more languages, richer data structures: list, set, map, etc. that Protobuf does not support) Incomplete documentation and hard to find good examples.
    • User case: Hbase/Cassandra/Hypertable/Scrib/..
  • Apache Avro: Avro is heavily used in the hadoop ecosystem and based on dynamic schemas in Json. It features dynamic typing, untagged data, and no manually-assigned field IDs.

Generally speaking, RPC is internally used by many tech companies for performance issues, but it is rather hard to debug and not flexible. So for public APIs, we tend to use HTTP APIs, and are usually following the RESTful style.

  • REST (Representational state transfer of resources)
    • Best practice of HTTP API to interact with resources.
    • URL only decides the location. Headers (Accept and Content-Type, etc.) decide the representation. HTTP methods(GET/POST/PUT/DELETE) decide the state transfer.
    • minimize the coupling between client and server (a huge number of HTTP infras on various clients, data-marshalling).
    • stateless and scaling out.
    • service partitioning feasible.
    • used for public API.

2.2 Storage

2.2.1 Relational Database

Relational database is the default choice for most use cases, by reason of ACID (atomicity, consistency, isolation, and durability). One tricky thing is consistency – it means that any transaction will bring database from one valid state to another, (different from the consistency in CAP, which will be discussed in Section 2.3).

Schema Design and 3rd Normal Form (3NF)

To reduce redundancy and improve consistency, people follow 3NF when designing database schemas:

  • 1NF: tabular, each row-column intersection contains only one value
  • 2NF: only the primary key determines all the attributes
  • 3NF: only the candidate keys determine all the attributes (and non-prime attributes do not depend on each other)
Db Proxy

What if we want to eliminate single point of failure? What if the dataset is too large for one single machine to hold? For MySQL, the answer is to use a DB proxy to distribute data, either by clustering or by sharding.

Clustering is a decentralized solution. Everything is automatic. Data is distributed, moved, rebalanced automatically. Nodes gossip with each other, (though it may cause group isolation).

Sharding is a centralized solution. If we get rid of properties of clustering that we don’t like, sharding is what we get. Data is distributed manually and does not move. Nodes are not aware of each other.

2.2.2 NoSQL

In a regular Internet service, the read write ratio is about 100:1 to 1000:1. However, when reading from a hard disk, a database join operation is time consuming, and 99% of the time is spent on disk seek. Not to mention a distributed join operation across networks.

To optimize the read performance, denormalization is introduced by adding redundant data or by grouping data. These four categories of NoSQL are here to help.

Key-value Store

The abstraction of a KV store is a giant hashtable/hashmap/dictionary.

The main reason we want to use a key-value cache is to reduce latency for accessing active data. Achieve an O(1) read/write performance on a fast and expensive media (like memory or SSD), instead of a traditional O(logn) read/write on a slow and cheap media (typically hard drive).

There are three major factors to consider when we design the cache.

  1. Pattern: How to cache? is it read-through/write-through/write-around/write-back/cache-aside?
  2. Placement: Where to place the cache? client side/distinct layer/server side?
  3. Replacement: When to expire/replace the data? LRU/LFU/ARC?

Out-of-box choices: Redis/Memcache? Redis supports data persistence while memcache does not. Riak, Berkeley DB, HamsterDB, Amazon Dynamo, Project Voldemort, etc.

Document Store

The abstraction of a document store is like a KV store, but documents, like XML, JSON, BSON, and so on, are stored in the value part of the pair.

The main reason we want to use a document store is for flexibility and performance. Flexibility is obtained by schemaless document, and performance is improved by breaking 3NF. Startup’s business requirements are changing from time to time. Flexible schema empowers them to move fast.

Out-of-box choices: MongoDB, CouchDB, Terrastore, OrientDB, RavenDB, etc.

Column-oriented Store

The abstraction of a column-oriented store is like a giant nested map: ColumnFamily<RowKey, Columns<Name, Value, Timestamp>>.

The main reason we want to use a column-oriented store is that it is distributed, highly-available, and optimized for write.

Out-of-box choices: Cassandra, HBase, Hypertable, Amazon SimpleDB, etc.

Graph Database

As the name indicates, this database’s abstraction is a graph. It allows us to store entities and the relationships between them.

If we use a relational database to store the graph, adding/removing relationships may involve schema changes and data movement, which is not the case when using a graph database. On the other hand, when we create tables in a relational database for the graph, we model based on the traversal we want; if the traversal changes, the data will have to change.

Out-of-box choices: Neo4J, Infinitegraph, OrientDB, FlockDB, etc.

2.3 CAP Theorem

When we design a distributed system, trading off among CAP (consistency, availability, and partition tolerance) is almost the first thing we want to consider.

  • Consistency: all nodes see the same data at the same time
  • Availability: a guarantee that every request receives a response about whether it succeeded or failed
  • Partition tolerance: system continues to operate despite arbitrary message loss or failure of part of the system

In a distributed context, the choice is between CP and AP. Unfortunately, CA is just a joke, because single point of failure is a red flag in the real distributed systems world.

To ensure consistency, there are some popular protocols to consider: 2PC, eventual consistency (vector clock + RWN), Paxos, In-Sync Replica, etc.

To ensure availability, we can add replicas for the data. As to components of the whole system, people usually do cold standby, warm standby, hot standby, and active-active to handle the failover.

3 Future Studies

Just as everything else, preparation and practice makes perfect. This article just provides limited resources for your reference. Please keep learning, and enjoy ;)

EOF

五年前在读谷歌黑板报《浪潮之巅》的时候,就被吴军博士所讲述的“大学的理念”深深地震撼到了,纽曼所倡导的通识教育与洪堡所倡导的专业教育在现在看来已经深入我心。所以这一次读他的新书《大学之路》,我并没有过多的期待,只是想知道在他眼里,这些大学都是怎样教育学生的。

总结起来,就是一个灵魂,两个维度

一个人的灵魂决定了他的趣味,我一直想做一个有趣的人,所以我一直在反问自己,什么是人的灵魂。看到吴军博士说到,“一所没有独特理念的大学一定是办不好的,就如同一个没有灵魂的人走不远一样。”我会心一笑,发现原来有灵魂就意味着有着独特的信念与价值观。反观好的互联网产品也是这样,我们可以从 UI 上就可以看出 Uber 的高贵冷艳,Lyft 的可爱亲民。

在我过去求知的道路上,有灵魂的学校给我留下了深深的烙印。耶鲁教育了我不单单要探索真理,更是要动用智慧带领人类走向光明(Lux et veritas);清华教会了我要向天学习刚强劲健,有恒心有毅力地自我激励与成长,但是又不可心生傲慢,要向地学习敦厚和善,包容他人帮助他人(天行健,君子以自强不息,地势坤,君子以厚德载物);国关则教育了我要忠于党、国家、和人民,用勤奋的工作报答他们的恩情,要实事求是地看待这个世界,并想方设法用创新改善这个世界(忠诚勤奋,求实创新)。

两个维度,是指教育的深度和广度。人的精力有限,不可能没有重点地挥霍。人才的知识体系往往是 T 字型的,有些学校更注重于深度,有些学校更注重于广度:扎实的专业知识有助于学生找工作,立竿见影,广博的知识让学生看到更大的世界,能够在未来的道路上有更多的可能性,有助于学生更长远的发展。

既然不同学校不同专业侧重的维度不一样,所以我们认为,合适的才是最好的。Jeff Atwood 就说,当初读书的时候,幸好并没有主修计算机,否则将会浪费大量的时间在没用的数学课上,而作为其他专业的学生,可以只选择那些对编程有用的课程。

这种适合不仅仅是灵魂上的合适,维度上的合适,还有时机上的合适。人生的各个时期有不同的侧重点;从学生来讲,大一大二融入并探索大学生活,大三大四朝着某个方向发力,为未来做准备。从创业者来讲, Peter Thiel 谈做生意要回答的七个问题,排名第二的就是时机问题。

个人的发展,从小的来说,需要掌握火候,从大的来讲,需要适应天下大势。

在逆境中,用发展的眼光看世界,你会发现,读哪一所大学这个问题本身,也并没有那么那么重要了。世界发展到今天这个时代,对个人的容错率是很高的,一个人不需要再死守着二十四节气,春耕夏耘秋收冬藏,不能错过每一天的时令。个人的失败,不再像过去,武士相遇,生死悬于一线,没有再来一次的机会。只要有心,在正确的领域,刻意练习一万个小时,总有出头的机会。

而那些辉煌的顺利的,也终究有衰败的那一天。比如 Reid Hoffman 就提醒我们看看底特律的昨天和硅谷的今天,要居安思危。童话故事总是喜欢以王子和公主从此过上了幸福的生活为结束,那后来呢?可惜没有一成不变的关系,没有事事顺心的境遇,凡事怎么可能就那么简单?

EOF

Nathan Marz 是 Twitter 的分布式流处理框架 Storm 的主要作者,他的新书 Big Data: Principles and best practices of scalable realtime data systems 今天正式发售了,我半个月前就下了订单,满心期盼。先前在浏览他的博客时看到一篇有趣的文章,发表于2012年,现中文翻译于此,和大家分享。


面向痛苦编程

作者:Nathan Marz

译者:潘天 (puncsky)

前些天,有人问了我个有趣的问题:“在创业公司工作的时候,还能够有勇气去写 Storm,你是怎样做到的?” 我知道,在外界看来,创业公司做如此大规模的项目,是相当冒险的行为。而在我自己看来,其实,写 Storm 一点儿风险都没有:它很有挑战性,但并没有风险。

我遵从了一种能够极大地规避风险的开发方式,来应对像 Storm 这种大型项目,并将其命名为 “面向痛苦编程” 。简单来说,面向痛苦编程是这样的:不去做那些不重要的项目,除非,缺少它真的让你感到痛苦难耐。这一准则放之四海而皆准,大到架构上的决策,小到每天写的代码。面向痛苦编程保证你总是在做重要的事情,在尝试大的投入前先从小处精通问题的本质,因此而能够极大地规避风险。

面向痛苦编程有三句箴言:“先做成,再做美,后做快。” (First make it possible. Then make it beautiful. Then make it fast.)

先做成

当你进入某个你不熟悉的问题领域 (problem domain) 时,不要立马写一个 “通用的” 或者 “可拓展” 的解。你都不太了解,如何预见未来会有哪些需求?你会把不必通用的变通用,浪费了时间,增加了复杂性。

更好的做法是仅仅 “三下五除二干出来再说” (hack things out) ,直面解决手头的问题。这样,你就能够做成你需要做成的事儿,避免浪费时间。

Storm 的 “先做成” 阶段是一年的快速开发,用队列和工作节点做一个流试处理处理系统。我们学到了如何用 “ack” 协议保证数据处理,我们学到了如何用集群的队列和工作节点规模化实时计算,我们学到了有时候要用不同的方式分割 (partition) 消息流:有时候随机地、有时候 hash/mod 地,确保同样的东西总是流到同样的工作节点上。

当时我们还甚至不知道我们处在 “先做成” 的阶段,只是专注于做产品。尽管队列和工作节点们形成的系统在后来很快让我们痛得不行,但是当时,规模化这个系统很乏味,我们也并不需要容错机制。很明显,队列和工作节点形成的范式并不是一层正确的抽象,因为我们大多数的代码与路由 (routing messages) 和序列化有关,并非我们真正关心的业务逻辑。

同时,开发产品让我们发现了 “实时计算” 的新用例。我们写了一个功能,计算 Twitter 上某一个 URL 的 受众量 (Reach),所谓受众量,就是能够接触到 Twitter 上的某个 URL 的去除重复后的人数。这一计算很困难,一次计算就需要上百次的数据库访问、和上千万的去重操作。要处理这些绝对路径的 URL,我们最初的单机实现要跑一分多钟,显然,我们需要某种分布式系统来并行处理,加速计算。

启发 Storm 的一个关键就在于,我们意识到了 “受众量问题” 和 “流处理问题” 可以被归化成简单的抽象。

再做美

当你三下五除二干出来再说 (hacking things out) 的时候,这一问题领域的 “地图” 也逐渐明晰起来。随着时间的推移,你逐渐习得这一领域越来越多的用例,深入了解这些系统的微妙与复杂之处。这些深入的了解能够启发 “美丽” 的技术,来替代现有的系统,减轻你的痛苦,让过去的不可能做到的新系统、新功能变成可能。

做“美”的关键在于,搞清楚能够解决已知具体问题的最简抽象集。不要预测那些实际上你并未遇到的具体用例,否则结果就会做得太过了 (overengineering)。经验法则是,要想投入更多,对问题领域的理解需要更深刻,用例需要更多样。否则,就有发生第二系统效应的风险。

在“做美”阶段,你动用设计与抽象的技能,把问题区间提炼成简单的、可组合在一起的抽象们。提炼美好的抽象就似统计学所谓的回归一般:图上一堆点 (用例) ,找条最简单的曲线 (抽象) 契合这些点。

用例越多,找契合点的曲线也就越方便。如果点不够,得到的曲线要么做得太过,要么做得不够好,最终过度工程或者浪费时间。

做美有很大一块是去了解问题区间的性能与资源特征 (understanding the performance and resource characteristics of the problem space),设计优美的解要利用那些属于前一阶段“做成”时学到的微妙与复杂。

就 Storm 而言,我把实时处理提炼成了一堆小的抽象:流 (streams),出水口 (spouts),水栓 (bolts),和拓扑结构 (topologies)。我发明了新的算法,消除中间消息的代理,同时又能够保证数据得到了处理,整个系统中最复杂最令人痛苦的部分因此而被除去。流处理和受众量,两个看上去迥异的问题,如此优雅地归化到了 Storm,这强烈地预示着,我做的东西,了不得。

我继而进一步地为 Storm 寻找更多的用例,验证我的设计。我和其他工程师深入讨论,学习他们处理实时问题的独到之处。我并没有只去问我认识的人,我还发了 Twitter 说我正在做一款新的实时系统,想要知道其他人的用例,随后有很多有趣的讨论,让我对问题领域有了更深的认识,并验证了我的设计思路。

后做快

一旦得到了优美的设计,就能安全地花时间做性能分析 (profiling) 和优化了。过早优化浪费时间,因为你仍然有可能重新做设计。

“做快”并不是指系统高层级的性能特征。这些特征本应该在“做成”阶段被理解到,在“做美”阶段被设计好。“做快”是指微观上的优化,让代码更紧凑,更有效率地利用资源。所以说,在“做美”阶段,你会关心诸如渐进复杂度的问题;在“做快”阶段,你会关心诸如复杂度中那些常量的问题。

反复雕琢

面向痛苦编程是持续的过程。构建美丽的系统赋予你新的能力,让你在问题区间里更新更深的领域有能力“做成”。学到的新的知识又反馈到原有的技术中,用以微调或者补充原有的抽象,这样就能够搞定更多的用例。

Storm 就像这样迭代了好多次。一开始使用 Storm 的时候,我们发现了,单一的组件需要有能力放出多个独立的流。我们发现了,Storm 批处理元组 (process batches of tuples) 作为具体的单元,需要增加一种特殊的“直接流 (direct stream) ”。最近,我又开发了“事务性拓扑 (transactional topologies)” ,在 Storm 至少处理一次的基础上,对几乎任意情况的实时处理需求,达成恰好通知一次的语义。

当然了,在自己不了解的领域三下五除二干出来再说,然后持续性地迭代,会导致一些渣代码。而面向痛苦编程最重要的特性便是大无畏地专注于重构。这正是规避“偶发复杂度 (accidental complexity)” (译者注:将概念上的构思施行于电脑上所遭遇到的困难。) 破坏项目的核心所在。

结论

用例在面向痛苦编程中至关重要,用黄金来形容它们的价值也不为过。而习得用例的唯一方法,就是写代码积累经验。

绝大多数的程序员都会经历这样的进化过程。一开始挣扎地让东西跑起来,代码乱得毫无章法,渣代码和拷贝粘贴的代码一塌糊涂。后来,你会发现结构化编程的好处,尽可能多地共享代码逻辑。然后,你学到了如何抽象,用封装的思想看系统。再后来,你着迷于通用化你的代码,让一切的一切可以拓展到未来的程序中。

面向痛苦编程拒绝相信人们能够有效地预测未知的需求,它承认,在不了解问题领域的情况下通用化代码,终将导致复杂与浪费。设计一定要,也总是要,驱动于活生生的用例。

EOF

作者:Miguel de Icaza

译者:潘天 (puncsky)

本周,我一直在准备一份讲稿,主题为“让 C# 在 iOS 和 Android 上欢唱”,我突然意识到,基于 callback 的编程,从某种程度上说,竟然成为了大家可以接受的编程方式。

成为了大家可以接受的编程方式,就好像用 IF / GOTO 写代码在上世纪六十年代可以被大家接受一般。他们之所以可以被接受,是因为那时候没有更好的东西来替代他们。

时至今日,C# 和 F# 都支持这种基于 callback 的编程方式。然而这种编程方式却让我们这一代人重蹈覆辙,如同六十年代结构化编程、高级语言、控制流语句对开发者所干的那些事情一般。

可悲的是,很多开发者一听到 “C# async” 就立马想到了 “我的编程语言有 callback”。类似的回答还有 “Promise 更好”,“Future 才是正道”,“Objective-C 有 block 了”,“操作系统有那个功能”。所有的这些说法,都出自于那些还没学习过 C# 的人之口,或者他们还不知道它有多么地美妙。

这次,我要解释,为什么对开发者来说,C# async 模型是一次巨大的飞跃。

Callback 是创可贴

Callback 这些年来进展显著。在纯 C 语言的年代,如果你要用 callback ,代码会是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void cback (void *key, void *value, void *user_state)
{
    // 我们的数据存在 user_state 中, 把它拿出来
    // 在这里,它就是个 int 整数
    int *sum = (int *) user_state;

    *sum = *sum + *(int *)value;
}

int sum_values (Hashtable *hash)
{
    int sum = 0;

    hash_table_foreach (hash, cback, &sum);
    return sum;
}

开发者不得不把这些指向状态的指针传来传去,手动管理,如此地笨拙。

而当今这些支持 lambda 表达式的语言,能让你的代码自动保留状态。所以上面的东东就变成了这样:

1
2
3
4
5
6
int sum_values (Hashtable hash)
{
    int sum = 0;
    hash.foreach ((key, value) => { sum += value; });
    return sum;
}

Lambda 让写代码更加轻松简单,如今我们能看到很多应用程序 UI 会用到 events / lambdas 与用户的输入交互,JavaScript 则在浏览器和客户端拿 callback 完成工作。

Node.js 最初的想法就是,去掉那些阻塞的操作 (blocking operations),取而代之以纯 callback 驱动的异步模型。就桌面应用而言,通常需要链接一系列的操作,“当响应点击的时候,下载文件,解压缩,保存到用户指定的位置”,此时,UI 操作和背景操作就交杂在一起。

于是就产生了一些 callbacks 嵌套在另外的 callbacks 中,他们每个的缩进的层级对应着在未来的某个时间点执行。有人说,这就是 Callback Hell.

本周,Marco Arment 恰好 tweet 了一条消息,大意是一堆 block callback 们深深地混杂在一起,产生了滑稽的效果。

这其实很常见,在我们的网站上,做异步操作时,我们发布了这样的范例:

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
private void SnapAndPost ()
{
    Busy = true;
    UpdateUIStatus ("Taking a picture");
    var picker = new Xamarin.Media.MediaPicker ();
    var picTask = picker.TakePhotoAsync (new Xamarin.Media.StoreCameraMediaOptions ());
    picTask.ContinueWith ((picRetTask) => {
        InvokeOnMainThread (() => {
            if (picRetTask.IsCanceled) {
                Busy = false;
                UpdateUIStatus ("Canceled");
            } else {
                var tagsCtrl = new GetTagsUIViewController (picRetTask.Result.GetStream ());
                PresentViewController (tagsCtrl, true, () => {
                    UpdateUIStatus ("Submitting picture to server");
                    var uploadTask = new Task (() => {
                        return PostPicToService (picRetTask.Result.GetStream (), tagsCtrl.Tags);
                    });
                    uploadTask.ContinueWith ((uploadRetTask) => {
                        InvokeOnMainThread (() => {
                            Busy = false;
                            UpdateUIStatus (uploadRetTask.Result.Failed ? "Canceled" : "Success");
                        });
                    });
                    uploadTask.Start ();
                });
            }
        });
    });
}

这般 callback 们嵌套所导致的问题是,你会很快发现,你根本就不想碰这堆乱糟糟的代码。它确实做了一些基本的错误处理 (basic error handling),但是却没有丝毫做好错误恢复 (error recover)的意图。

我停下来慢慢思考,如何扩展上述功能,或许还有更好地做法,能够让我避免这种潦草的实现?

如果我想要更好的错误恢复,代码逻辑更加流畅,我会发现,需要类似记流水账一般,在每一处代码的出口,(以及我可能会拓展处的出口),手动核对每个 Busy 值的正确性,真烦躁。

这种丑陋的实现会让你走神:”或许可以刷一刷 hacker news 啦”,”又有新的喵星人帖到 catoverflow.com 上了吗?”

注意,如上代码中每一个 lambda 都会产生一次上下文切换:从背景线程 (background threads) 切换到前景线程 (foreground threads) 。可以想象,真实世界中这种代码会更加地庞杂,加入更多的功能,也就会有更多的 bug 积累在犄角旮旯,难以被注意得到。

这让我想到了 Dijkstra 的 《Go To 语句有害论》,他在六十年代是这么说的:

多年来,我一直注意到,程序员的水平和他们goto语句的使用频率成递减关系。直到前段时间,我发现了为什么使用goto语句会有如此恶劣的影响。我相信所有的“高级”编程语言都不应该再支持goto语句。那时候我还没有意识到这一发现的重要性,但在直到最近的一次讨论中,有人敦促我发表这些看法,我才提交这篇文章。

我的第一个看法是,虽然程序员的工作在他们写出了正确的程序 (program) 后就结束了,但在此程序控制下的进程 (process) 才是真正重要的,因为正是进程来达成预期的效果,正是进程的动态行为要满足程序员预期的规范。一旦程序写完后,相应的进程就转交给了机器来做了。

我的第二个看法是,我们的大脑善于处理静态关系,却不善于想象随时间推进的进程。所以我们应该(聪明的程序员明白自己的局限性)尽力缩小静态程序和动态进程之间的概念差距,让文本里的程序 (program) 和时间轴上的进程 (process) 的对应关系尽量简单。

而这,恰恰是我们现在正在面对的问题——混杂嵌套的 callback 所导致的问题。

就像是那 GOTO 的年代,或者是手动内存管理的年代,我们这一代人也正在成为光荣的会计师,勤勤恳恳地在每条代码路径下核查那些状态,该重置的重置,该修改的修改,该抛弃的抛弃,该交付的交付。

作为一个可以进化的物种,我们理所应当地能够做得更好。

就在这个节骨眼上,C# async (和 F#)到来了。每当你在程序中放入await 关键字,编译器就知道你程序中的这一点需要被挂起,切换做某背景操作。一旦 task 执行完毕,程序执行就恢复到这 await 一点的位置。

如上丑陋的代码,会变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private async Task SnapAndPostAsync ()
{
    try {
        Busy = true;
        UpdateUIStatus ("Taking a picture");
        var picker = new Xamarin.Media.MediaPicker ();
        var mFile = await picker.TakePhotoAsync (new Xamarin.Media.StoreCameraMediaOptions ());
        var tagsCtrl = new GetTagsUIViewController (mFile.GetStream ());
        // Call new iOS await API
        await PresentViewControllerAsync (tagsCtrl, true);
        UpdateUIStatus ("Submitting picture to server");
        await PostPicToServiceAsync (mFile.GetStream (), tagsCtrl.Tags);
        UpdateUIStatus ("Success");
    } catch (OperationCanceledException) {
        UpdateUIStatus ("Canceled");
    } finally {
        Busy = false;
    }
}

编译器处理如上代码后,编译的产物和人写的代码间就没有(按照同样的顺序)一行行直接对应的关系了。类似的事情会发生在 C# iterator 甚至 lambda 身上。

这种代码看上去很线性,这样一来,我就能信心满满地修改程序流。或是使用条件语句开不同的背景进程,或是使用循环语句把图片存到不同的位置,又或是一次性使用多个过滤器。Jeremie 恰巧写了篇不错的文章做这些事情

注意,因为 finally 语句,Busy 状态的处理变得集中而简洁。我现在能够保证,变量总是合理地得到了维护,无论在这段代码长怎么样,发生了什么。

记流水账这种事情,就交给了编译器。

async 让我能够动用流程图上的那些基本元素,来思考我的软件。而不是按照那些紧紧耦合在一起的混乱而粗糙的进程们来做同样的事情。

思维的解放

C# 编译器处理 async 的基础是 Task 原生类型。Task 类正是其他编程语言所谓的那些 future, promise 和 async,难免会导致混淆。

此刻,我把所有的这些框架(包括 .NET 里的这只)都比作很底层的基础水暖管道。

Task 类封装一系列的操作,由一些 properties 来反应它们的执行状态、结果(如果完成的话)、抛出的异常或者错误。有很丰富的 API 来有趣地组合这些 task :等待所有、等待一些、聚合很多个成一个,等等不一而足。

那些(callback 的做法)尽管很重要,比起你自己惯常的做法已经是很大的进步了,但它们却并不能帮你解放思维。C# async 却可以。

关于 async 关键字的常见问答

借这次机会,我要直接回答下列问题:

问:每次使用 async 的时候会创建新的线程么?

答:当你使用 async method 的时候,你的操作被封装在 Task (或者 Task<T>) 对象中。有时候,这些操作需要另一个线程去跑;有时候,这些操作会进入事件队列,供给事件循环去处理(runloop);有时候,这些操作会调用带有通知机制的内核异步API. 你不会确定地知道幕后到底发生了什么,这取决于 async method 自己的实现。(译者注:调用 async method 不会产生新的线程,但是 async method 内部,开发者自己定义的实现里面可以开新的线程。)

问:在使用 async 的时候,你似乎不再需要 InvokeOnMainThread了,为啥呢?

答:当 task 结束的时候,执行流程会默认恢复到当下同步的上下文,这是因为 ThreadLocal 的 property 指向了SynchronizationContext 的某个具体实现。

在 iOS 和 Android 上,我们在 UI 线程设定一个SynchronizationContext,确保代码每次都恢复到主线程上。微软在它们的平台上也在做同样的事情。

而且,在 iOS 上,我们还有一个 DispatchQueue 同步上下文,因此默认情况下,用Grand Central Dispatch (GCD)调用 await ,完成后会回到那个 queue.

当然你可以调整这一行为,用 SynchronizationContextConfigureAwait

最后,PFX team 有篇不错的 FAQ for Async and Await.

async 的参考资源

这儿有些不错的 async 学习资料:

What is the best practice of a server architecture for an I/O bound web application on commodity machines? Why is the answer event-driven reactor pattern with async non-blocking I/O? How to implement an echo web server with reactor pattern in Java? Why does reactor pattern come with JavaScript and Node.js?

To answer these questions, let us first look at how an HTTP request is handled in general. After accepting the incoming request, the server establishes a TCP connection. It reads and parses the content in the request from the socket (CPU bound). Then the request is dispatched to the application level for domain-specific logics, which would probably visit the file system for data. Or even more, since we are investigating a scalable website for high raw data throughput (I/O bound), and all complex components are decoupled, the server will probably execute a network-based task, e.g. fetching data from remote caches and databases. Once finished, the server writes the response to the client, and waits for the next request, or closes the connection.

1. Why async non-blocking I/O?

Since we are assuming it is an I/O bound web application (which is often the case), I/O operations can be extremely slow compared to the processing of data. Think about switching electric current vs. a physical hard drive seeking a track.

Traditionally, we write an application to execute I/O operations in a synchronous and blocking way, that is to say, if the CPU has to wait for the I/O device to load all the data slowly, it has to wait and do nothing else. What? Idle CPU resources? Bad news for us! We should exhaust them!

1
2
3
4
// Sync blocking  I/O example in JavaScript.
var fs = require('fs');
var data = fs.readFileSync('filename'); // wait for returning data, and waste valuable CPU time.
console.log(data);

Why not let the control flow and I/O operations return immediately just a status, and free the CPU from waiting and do other meaningful operations? After all, we can still revisit the status or results later. Here comes the notion of aync non-blocking I/O.

1
2
3
4
5
6
// Async non-blocking I/O example in JavaScript.
var fs = require('fs');
fs.readFile('filename', function(err, data) {
    console.log(data);
}); // returns immediately, function will do the work when the data is ready.
// do other meaningful operations.

It looks quite straightforward in JavaScript as shown above, but how is it implemented under the hood? Intuition told me it was manually done by the application developers with threads, but I was wrong. Actually, there are various ways to do this – different programming languages have their own libraries (e.g. NIO for Java, libuv for JavaScript) on different operating systems. And the operating systems themselves also provide system calls in the kernel level – e.g. select, poll, epoll, and kqueue.

2. Why event-driven?

To handle web requests, there are two competitive web architectures – thread-based one and event-driven one.

2.1 Thread-based Architecture

The most intuitive way to implement a multi-threaded server is to follow the process/thread-per-connection approach.

In reality, the first HTTP server, CERN httpd, was created with a process-per-connection model. Nowadays Apache-MPM prefork still retains the feature for the following reasons.

It is appropriate for sites that need to avoid threading for compatibility with non-thread-safe libraries. It is also the best MPM for isolating each request, so that a problem with a single request will not affect any other.

However, the isolation and thread-safety come at a price. Processes are too heavyweight with slower context-switching and memory-consuming. Therefore, the thread-per-connection approach comes into being for better scalability, though programming with threads is error-prone and hard-to-debug.

In order to tune the number of threads for the best overall performance and avoid thread-creating/destroying overhead, it is a common practice to put a single dispatcher thread (acceptor thread) in front of a bounded blocking queue and a threadpool (worker threads). The dispatcher blocks on the socket for new connections and offers them to the bounded blocking queue. Connections exceeding the limitation of the queue will be dropped, but latencies for accepted connections become predictable. A pool of threads poll the queue for incoming requests, and then process and respond.

Apache-MPM worker takes advantages of both processes and threads (threadpool).

By using threads to serve requests, it is able to serve a large number of requests with fewer system resources than a process-based server. However, it retains much of the stability of a process-based server by keeping multiple processes available, each with many threads.

Here is a simple implementation with a threadpool for connections:

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
45
46
47
48
49
50
51
52
53
54
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.*;

public class EchoServer {
    private static final int PORT = 4000;
    private static final int BUFFER_SZ = 1024;

    public static void main(String[] args) {
        try {
            ServerSocket server = new ServerSocket(PORT);
            ExecutorService executor = Executors.newCachedThreadPool();
            while (true) {
                Socket s = server.accept();
                executor.submit(new Handler(s));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static class Handler implements Runnable {
        Socket _s;

        public Handler(Socket s) {
            _s = s;
        }

        @Override
        public void run() {
            try {
                InputStream in = _s.getInputStream();
                OutputStream out = _s.getOutputStream();
                int read = 0;
                byte[] buf = new byte[BUFFER_SZ];
                while ((read = in.read(buf)) != -1) {
                    out.write(buf, 0, read);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    _s.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Unfortunately, there is always a one-to-one relationship between connections and threads. Long-living connections like Keep-Alive connections give rise to a large number of worker threads waiting in the idle state for whatever it is slow, e.g. file system access, network, etc. In addition, hundreds or even thousands of concurrent threads can waste a great deal of stack space in the memory.

2.2 Event-driven Architecture

Event-driven approach can separate threads from connections, which only uses threads for events on specific callbacks/handlers.

3. Reactor Pattern

The reactor pattern is one implementation technique of the event-driven architecture. In simple words, it uses a single threaded event loop blocking on resources emitting events and dispatches them to corresponding handlers/callbacks. There is no need to block on I/O, as long as handlers/callbacks for events are registered to take care of them. Events are like incoming a new connection, ready for read, ready for write, etc. Those handlers/callbacks may utilize a threadpool in multi-core environments.

This pattern decouples modular application-level code from reusable reactor implementation.

3.1 Reactor Pattern Explained with Echo Web Server in Java

Wait! Talk is cheap and show me the code :) Yeah, now let’s build an echo web server that can be tested with telnet localhost 9090. You can also try to build with Netty, a NIO client server framework.

In the following code, a single boss thread is in an event loop blocking on a selector, which is registered with several channels and handlers. Associated handlers will be executed by the boss thread for specific events (accept, read, write operations) coming from those channels. In terms of processing the request, a threadpool is still used.

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package reactor;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.nio.channels.ServerSocketChannel; // listening for incoming TCP connections
import java.nio.channels.SocketChannel;    // TCP connection
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class ReactiveEchoServer implements Runnable {
    private final Selector _selector;
    private final ServerSocketChannel _serverSocketChannel;
    private static final int WORKER_POOL_SIZE = 10;
    private static ExecutorService _workerPool;

    ReactiveEchoServer(int port) throws IOException {
        _selector = Selector.open();
        _serverSocketChannel = ServerSocketChannel.open();
        _serverSocketChannel.socket().bind(new InetSocketAddress(port));
        _serverSocketChannel.configureBlocking(false);

        // Register _serverSocketChannel with _selector listening on OP_ACCEPT events.
       // Callback: Acceptor, selected when a new connection incomes.
        SelectionKey selectionKey = _serverSocketChannel.register(_selector, SelectionKey.OP_ACCEPT);
        selectionKey.attach(new Acceptor());
    }

    public void run() {
        try {
            // Event Loop
            while (true) {
                _selector.select();
                Iterator it = _selector.selectedKeys().iterator();

                while (it.hasNext()) {
                    SelectionKey sk = (SelectionKey) it.next();
                    it.remove();
                    Runnable r = (Runnable) sk.attachment(); // handler or acceptor callback/runnable
                    if (r != null) {
                        r.run();
                    }
                }
            }
        }
        catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public static ExecutorService getWorkerPool() {
        return _workerPool;
    }

    // Acceptor: if connection is established, assign a handler to it.
    private class Acceptor implements Runnable {
        public void run() {
            try {
                SocketChannel socketChannel = _serverSocketChannel.accept();
                if (socketChannel != null) {
                    new Handler(_selector, socketChannel);
                }
            }
            catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        _workerPool = Executors.newFixedThreadPool(WORKER_POOL_SIZE);

        try {
            new Thread(new ReactiveEchoServer(9090)).start(); // a single thread blocking on selector for events
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
package reactor;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.channels.Selector;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;

class Handler implements Runnable {
   private final SocketChannel _socketChannel;
   private final SelectionKey _selectionKey;

   private static final int READ_BUF_SIZE = 1024;
   private static final int WRiTE_BUF_SIZE = 1024;
   private ByteBuffer _readBuf = ByteBuffer.allocate(READ_BUF_SIZE);
   private ByteBuffer _writeBuf = ByteBuffer.allocate(WRiTE_BUF_SIZE);

   public Handler(Selector selector, SocketChannel socketChannel) throws IOException {
       _socketChannel = socketChannel;
       _socketChannel.configureBlocking(false);

       // Register _socketChannel with _selector listening on OP_READ events.
       // Callback: Handler, selected when the connection is established and ready for READ
       _selectionKey = _socketChannel.register(selector, SelectionKey.OP_READ);
       _selectionKey.attach(this);
       selector.wakeup(); // let blocking select() return
   }

   public void run() {
       try {
           if (_selectionKey.isReadable()) {
               read();
           }
           else if (_selectionKey.isWritable()) {
               write();
           }
       }
       catch (IOException ex) {
           ex.printStackTrace();
       }
   }

   // Process data by echoing input to output
   synchronized void process() {
       _readBuf.flip();
       byte[] bytes = new byte[_readBuf.remaining()];
       _readBuf.get(bytes, 0, bytes.length);
       System.out.print("process(): " + new String(bytes, Charset.forName("ISO-8859-1")));

       _writeBuf = ByteBuffer.wrap(bytes);

       // Set the key's interest to WRITE operation
       _selectionKey.interestOps(SelectionKey.OP_WRITE);
       _selectionKey.selector().wakeup();
   }

   synchronized void read() throws IOException {
       try {
           int numBytes = _socketChannel.read(_readBuf);
           System.out.println("read(): #bytes read into '_readBuf' buffer = " + numBytes);

           if (numBytes == -1) {
               _selectionKey.cancel();
               _socketChannel.close();
               System.out.println("read(): client connection might have been dropped!");
           }
           else {
               ReactiveEchoServer.getWorkerPool().execute(new Runnable() {
                   public void run() {
                       process();
                   }
               });
           }
       }
       catch (IOException ex) {
           ex.printStackTrace();
           return;
       }
   }

   void write() throws IOException {
       int numBytes = 0;

       try {
           numBytes = _socketChannel.write(_writeBuf);
           System.out.println("write(): #bytes read from '_writeBuf' buffer = " + numBytes);

           if (numBytes > 0) {
               _readBuf.clear();
               _writeBuf.clear();

               // Set the key's interest-set back to READ operation
               _selectionKey.interestOps(SelectionKey.OP_READ);
               _selectionKey.selector().wakeup();
           }
       }
       catch (IOException ex) {
           ex.printStackTrace();
       }
   }
}

3.2 Reactor Pattern, JavaScript, and Node.js

Node.js is a phenomenon in the Silicon Valley. Yay, it is server-side JavaScript! Atwood’s law says any application that can be written in JavaScript will eventually be written in JavaScript. My question is why it is JavaScript with powerful Node.js and its reactor pattern, not other programming languages.

The answer may be as simple as a single word – tradition. JavaScript has a tradition of being single threaded (though it has limited web worker API). Its concurrency model is based on an event loop. Words like “async”, “non-blocking”, and “callback”, which sound so fancy and advanced in other programming languages, are so ordinary and can be seen everywhere in the JavaScript world. In this world, if you want your APIs to be popular, you have to make them aync and non-blocking.

As to C# async programing with async and await keywords, that is another story.

References

  • C10k problem, http://www.kegel.com/c10k.html
  • Architecture of a Highly Scalable NIO-Based Server, https://today.java.net/pub/a/today/2007/02/13/architecture-of-highly-scalable-nio-server.html
  • Explain “Event-Driven” Web Servers to Your Grandma, http://daverecycles.tumblr.com/post/3104767110/explain-event-driven-web-servers-to-your-grandma
  • Reactor pattern, http://en.wikipedia.org/wiki/Reactor_pattern
  • JavaScript 运行机制详解:再谈Event Loop, http://www.ruanyifeng.com/blog/2014/10/event-loop.html
  • Concurrency model and Event Loop, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/EventLoop
  • Concurrent Programming for Scalable Web Architectures, http://berb.github.io/diploma-thesis/index.html
  • Scalable Event Multiplexing: epoll vs. kqueue, http://www.eecs.berkeley.edu/~sangjin/2012/12/21/epoll-vs-kqueue.html

III Variables

10 General Issues in Using Variables

Abstract 变量的声明与使用遵循精准确定、单一集中原则

Keywords proximity principle

尽可能显式声明变量类型

在靠近使用的地方初始化变量,而不是一开始的附近。因为 Principle of Proximity(相关的东西要放一起)

  1. 可能在使用的时候早就被其他(很可能后来新加的)代码修改了
  2. 集中性的一次性初始化多个变量,会造成这些变量将在整个代码中用到的错觉
  3. 围绕这小片代码将来可能会加循环之类的结构,导致bug

在靠近使用的地方声明、定义变量

尽量使用final和const

初始化0是一个不错的选择,Intel处理器0xCC, 有些debugger容易识别的有0xDEADBEEF

尽量让变量短命 live time = 最初到最后对变量引用的行数,比如,一开始选择最保守的visibility,需要时再拓展

使用常量不要hard-coding,用类似private static final int COLOR_BLUE = 0xFF; 或者color = ReadTitleBarColor() 越早绑定优化程度越高,越迟绑定灵活性越高(更高的灵活性会导致更高的复杂性)。看如何权衡。

Use each variable for EXACTLY one purpose.

避免hybrid coupling: pageCount=-1表示错误

11 The Power of Variable Names

Abstract 变量的命名极为重要且有章可循

Keywords 8~20, qualifier, naming conventions

命名要充分、准确。当然一串对于该变量的描述是最准确的,但是,很可能会长得离谱。

按what命名,不按how命名。能用problem domain的名称就不要用计算机方面的名称。

研究表明平均变量名长度10~16是debug最容易的,其次是8~20。

Good example: numTeamMembers, teamMemberCount, numSeatsInStadium, seatCount, teamPointsMax, pointsRecord.

太长或太短的命名就完全不能用吗?当然不是,更长的变量适合于少用到的变量或者全局变量,更短的变量适合于局部或者loop里的变量。

为了避免命名冲突,有namespace的编程语言用namespace,没有的在variable名称前加前缀。

如果命名要用qualifier(限定词),比如Total, Sum, Average, Max, Min, Record, String, or Pointer, 把限定词放到命名最后。除了num,放在前面代表总数,放在后面代表index,所以尽量不要用num,而是用Count后缀和Index后缀。(我个人喜欢用i前缀)

用约定俗成的反义词

  • begin/end
  • first/last
  • locked/unlocked
  • min/max
  • next/previous
  • old/new
  • opened/closed
  • visible/invisible
  • source/target
  • source/destination
  • up/down

循环: 如果index仅限于循环内部,用i,j,k,如果1)循环外部也要用 2)循环过长 3)nested 则考虑有意义的名称

状态: 用比flag更有意义的状态变量名称

临时变量: 对临时变量持怀疑态度,能不用就不用

bool: 用done, error, found, success or ok. 用is前缀是一个不错的做法。用正向含义的名词,不要用类似于notFound这种。

不用的语言和项目有不同的naming conventions,P277有例子

如果两个变量的命名可以互换而不会影响程序,两者的命名是有问题的。

用数组file[]不要用file1, file2.

12 Fundamental Data Types

Abstract 数据类型们有着各自的使用规则。本章单独列出了许多C语言的注意事项,足以说明C语言在数据类型方面的使用很容易出错。

Keywords int, float, char, string, bool, enum, named constants, array, user-defined types

Avoid “magic numbers”(literal numbers such as 100 or 47524 出现在程序中而不加解释)唯一能够出现的数字literals应该是0和1,作为初始量或者增量。

Chars and Strings

  • All strings in Java are Unicode. 但是C/C++处理Unicode要用专门的库
  • 在软件开发早期就考虑国际化的问题

Strings in C

  • 区分string pointers和char arrays
    • C几乎所有的字符串操作都是通过strcmp(), strcpy(), strlen()等完成的,StringPtr = "text";此处“text”是指向literal text的指针,=并没有拷贝内容的作用
  • char string[NAME_LENGTH + 1] = { 0 }; 尽量用array of chars
  • strncpy(), strncmp() 不用strcpy(), strcmp(),因为至少有一个parameter保证函数一定会终止

bool: 当判断条件太多,用bool variable表示可读性更高

Enum

  • 与switch合用的时候,default处理错误的case
  • 为enum定义first last 元素用于enumeration (First(0), A(0), B(1), C(2), Last(2)) 这样一个loop就能搞定了

Array

  • 在C里面可以用宏#define ARRAY_LENGTH(x) (sizeof(x)/sizeof(x[o]))表示数组的长度

用户自定义类型的时候尽量以现实世界的实体命名,尽量用class而不是typedef

13 Unusual Data Types

Abstract 什么时候使用struct? pointer有哪些注意事项?使用全局数据导致的问题如何解决?

Keywords struct, pointer, memory corruption, global data

struct

group related data

C/C++ struct vs. class: In C++, structs and classes are pretty much the same; the only difference is that where access modifiers (edit: for member variables, methods, and for base classes) in classes default to private, access modifiers in structs default to public. However, in C, a struct is just an aggregate collection of (public) data, and has no other class-like features: no methods, no constructor, no base classes, etc. Although C++ inherited the keyword, it extended the semantics. (This, however, is why things default to public in structs—a struct written like a C struct behaves like one.)

pointer

= position + how to interpret contents from that position

最常见的错误是指针指向了不该指向的地方,(这时候向该内存写数据)会导致memory corruption.

解决策略很简单,尽早甚至一开始就规避这种错误

  • 把指针操作限制在函数或者类的内部
  • 定义声明和初始化在一起(proximity principle)
  • 对指针持怀疑态度,无论是指针本身,还是指针指向的变量,使用之前都要检查,怀疑他们超出范围了
  • 用额外的dog-tag空间(和原空间在一起)以检查memory corruption. e.g. isOK tag for free
  • 不要链条似地使用指针
  • 画图让问题更直观
  • 预留空间以在空间耗尽时备用
  • free/allocate pointers 放在同一scoping level
  • delete前要检查(并填充专门的垃圾数据),delete完要归NULL
  • 能不用指针还是不用指针的好

C++ Pointer Pointers

pointers vs. references?

  • The most significant differences are that a reference must always refer to an object, whereas a pointer can point to NULL; and what a reference refers to can’t be changed after the reference is initialized.

Use pointers for “pass by reference”. Use const references for “pass by value”.

用smart pointer: unique_ptr vs. shared_ptr (there can only be one unique_ptr to any resource, any attempt to make a copy of a unique_ptr will cause a compile-time error.)

C-pointer Pointers

使用显示类型的指针

尽量避免type-casting

使用 sizeof() 内存分配时决定变量大小

Global Data

全局变量因为scope过大,导致该变量会对其他模块产生不可预料的影响,不到万不得已,不用全局变量。

不要直接访问类成员变量就好像他们是全局变量似的,即便编程语言允许你这样做。

  • 用access routines (like getter, setter in Java) 而非直接访问成员有什么好处呢?好处极多(P352),简单来讲
    • 模块化、灵活性:允许变量和内部的实现与外部的交互分离,而外部的访问方式保持不变,代码改变的时候(变量变量,变化的情况还蛮多的)就不会影响到太多其他文件。getter,setter两者的访问级别可以不同。

同样因为模块化,在调用数据成员时,能用access routines尽量用access routines,即便在类的内部也不例外

Don’t pretend you’re not using global data by putting all your data into a monster object and passing it everywhere 这是不必要的开销,要用就光明正大地用

31 Layout and Style

Abstract Layout as Religion 布局是一种信仰,好的布局使得逻辑准确,代码一致、易读、可维护。

Keywords

Objectives of Good Layout

  • 准确表示代码的逻辑结构
  • 始终如一的表现代码的逻辑结构
  • 改善可读性
  • 经得起修改:修改某行时不必连带修改其他行的代码

Layout Styles

  • Pure blocks
  • Emulating Pure Blocks: 用{``}模仿纯块风格,{放在第一行末尾
  • Begin-End pairs (braces) as boundaries: {``}单独占一行,但是都有缩进
  • Endline Layout: 让switch看起来对更齐,但是可维护性差

Laying Out Control Structures

if 必须加大括号,多个条件放到不同的行上

goto使得代码的布局很不好办

段落之间空行

Laying Out Individual Statements

多用空格

Formatting Continuation Lines 断行

  • 让续行更明显,比如,把运算符、逗号等放到行的末尾。如果把运算符放在前面可以凸显逻辑结构。
  • 把紧密关联的元素放在一起
  • 将子程序调用的后续航按照标准量缩进,尽管对齐党觉得应该美观,可是就维护性来讲,标准缩进好些。
  • 让续行的结尾易于发现
1
2
3
4
5
6
CallMethod(
    LongNameA,
    LongNameB,
    C,
    D
);
  • 不要讲赋值语句按等号对齐,要按照标准量缩进
  • 每行只写一条语句,只能有一个操作
1
2
3
4
5
6
7
8
9
10
11
12
strcpy(char *t, char *s) {
    while (*++t = *++s) // 不容易发现错误
    ;
}

strcpy(char *t, char *s) {
    do {
        ++t; // 很容易发现错误
        ++s;
        *t = *s;
    } while (*t != '\0');
}

Laying Out Data Declarations

  • 每行只声明一个数据
  • 声明尽量接近首次使用的位置
  • 合理组织声明的顺序
  • C++指针声明把*靠近变量名

Laying Out Comments

  • 注释缩进与代码一致
  • 每行注释用至少一个空行分开
1
2
3
4
5
// Comment0
Statement0

// Comment1
Statement1

Laying Out Routines

  • 用空行分割子程序的各部分
  • 将子程序按照标准缩进
1
2
3
4
5
6
7
public bool ReadEmployeeData(
    int maxEmployees,
    EmployeeList *employees,
    EmployeeFile *inputFile,
    int *employeeCount,
    bool *isInputError
)

Laying Out Classes

Interfaces

  1. Ctors and destructors
  2. Public routines
  3. Protected routines
  4. Private routines and member data

Class implementations

  1. Class data
  2. Public routines
  3. Protected routines
  4. Private routines

如果文件包含多个类,要(用注释)清楚标出每个类

Laying Out Files and Programs

最好一个文件只有一个类,文件命名与类名一致或者相关

32 Self-Documenting Code

Abstract 好的代码是Self-documenting的,但是因此就不需要注释了吗?非也非也。

Keywords

注释有那些种类?

  • 重复代码(bad)
  • 解释代码:用于解释复杂、巧妙、敏感的代码
  • Maker in the Code: like TODO
  • Summary of the code
  • Description of the Code’s Intent: 指出需要解决的问题why,而非解决的方法how
  • 传达代码无法表述的信息

如何写高质量的注释呢?

  • 不写难以维护的(很麻烦的)注释
  • 用PPP伪代码编程法减少注释时间
  • 将注释集成到你的开发风格中 (写注释也是写代码)

最佳注释频率?

  • 十步一杀

Commenting Techniques

  • Commenting Individual Lines
  • Endline Comments and Their Problems
    • 不好之处
      • 难看难维护
      • 不要对单行代码做尾注释
      • 不要对多行代码做尾注释
    • 那么什么时候可以用呢?
      • 数据声明时解释数据
      • 标记block的尾部, end while, end if, etc.
  • Commenting Paragraphs of Code
    • 注释应该表明代码的意图,为什么这么做而不是具体怎么做
      • e.g. find the command-word terminator ($)
      • 想象将这段代码换成同样功能的sub routine应该如何命名
      • 额外的变量或者函数本身具有说明作用
      • 说明非常规的嘴阀
      • 别用缩略语
      • 错误或者语言环境的独特点要注释
      • 给出违背良好编程风格的理由
      • 投机取巧的代码要重写,而不是加注释
  • Commenting Data Declarations
    • 注释数值单位
    • 对数值允许的范围给出解释
    • 如果没有枚举类型的支持,或者对于bit flags,注释编码含义
    • 注释对输入数据的限制
    • 注释要与变量名关联起来,e.g. cref=""
    • 注释全局数据
  • Commenting Control Structures
    • while上一行,if/case下一行,都是不错的位置
  • Commenting Routines
    • 无视子程序的大小和复杂,在开头放一堆信息:这是荒谬的做法
    • 记得注释接口的assumption
  • Commenting Classes, Files, and Programs
    • 可以考虑说明类的设计方法、局限性等等
    • 如果不看实现就知道接口的用法吗?如果不知道,注释,但是不说实现细节

II Creating High Quality Code

5 Design in Construction

Abstract 软件开发管理的实质是管理复杂性,怎么简单怎么来,设计和抽象有助于降低复杂性。一种方式是层层向下,另一种方式是启发式地从下向上。分层向下时,模块之间应该尽量少交互、尽量不要出现循环依赖。各个模块要把“我该隐藏些什么?”牢记在心。自下向上时,不要固执于单一一种方法,(这会损害创新),反复迭代与重新设计以达到最优方案。在设计的过程中要记录或者画下设计方案。

Keywords KISS, hiding secrets, loose coupling, design patterns, principle of one right place

Software’s primary technical imperative: managing complexity. Dijkstra说,没有人的头脑能大到装得下一个复杂程序的全部细节。

好的设计的特征:

  • 简单(当看到这片代码的时候,不去参考其他代码,也能很好地理解)。
  • minimal connectedness (strong cohesion, loose coupling, encapsulation)
  • 可拓展性(可以在enhance代码的同时不更改已有的代码)
  • 可重用性
  • high fan-in, low-to-medium fan-out 尽量让别人引用自己,自己少引用别人
  • 可移植性
  • 精简,一本书的完成并不止于没有内容可以增加,而是没有内容可以除去
  • 分层(stratification)

自上而下:设计是有层次的,不是一蹴而就的

设计的层次:

  1. software system
  2. division into subsystems/packages
    • 任何需要花费几周才能完成的项目
    • 尽量精简子系统之间的交互关系,这样一个子系统的改变,对其他所有子系统的影响就会小很多
    • 依赖关系应该是无环的
    • 常用的子系统
      • business logic
      • UI
      • DB access
      • OS dependencies
  3. division into classes within packages
  4. division into data and routines within classes
  5. internal routine design

自下而上:因为软件开发是非确定性的,启发式(heuristics)的,所以需要“试错”

启发式设计的原则:你无需卡在单一的问题上,将问题留在未解决的状态,等拥有了更多信息后再去做。

  • 从现实世界寻找对象
  • 抽象
  • 封装
  • Inherit when inheritance simplifies the design. 方便之处在于,
    1. 传递同一类对象
    2. 对同一类对象执行同样的操作 e.g. foreach loop
  • 隐藏信息: 本质是隐藏复杂性
    • 大量使用自我说明性质的简短函数有助于减少复杂性
    • 信息隐藏的秘密可以分为两大类:隐藏复杂度,不需要仔细研究的时候不用细看;隐藏变化源(sources of change),当代码被改动时,其影响就能被限制在局部。
    • 信息隐藏失败的例子
      • 信息过度分散,循环依赖,类太大导致类内变量也像全局变量,性能上可能的损耗导致过早优化(明晰的设计比过早优化要好很多)
    • 养成问“我该隐藏什么?”的习惯
  • 找出容易改变的区域,隔离开来,设计好固定的接口,把变化都限制在区域内部。那么,哪些区域容易发生变化呢?
    • 业务规则、硬件依赖、输入和输出、非标准的语言特性、困难部分(因为困难所以很可能被重新改写)、状态变量、data-size constraints
  • 松散耦合
    • 小规模:模块之间的连接数要小,少的调用参数,少的public method
    • 高可见性:不要偷偷摸摸地通过全局变量传数据
    • 高灵活性:LookupVacationBenefit(Employee.Date, Employee.Classification)取代LookupVacationBenefit(Employee)因为有的地方需要临时拼凑Employee的其他字段,但是只有DateClassification用到了
  • 耦合的种类
    • 简单数据参数耦合(simple-data-parameter coupling) primitive data type parameters coupling 正常可接受
    • 简单对象耦合 a module instantiates an object 也很不错
    • 对象参数耦合(object-parameter coupling) 这种耦合比第一中更紧密
    • 语义上的耦合(semantic coupling) 使用某个的模块的时候需要知道其内部的细节,如果想当然就很容易出错,这种耦合很糟糕,因为松散耦合的关键就在于抽象的简单性——意思是,一旦你写好了,使用者就应当可以想当然地用它,不用知道额外信息。
  • Look for common design patterns
  • 其他不大常用的方法
    • 分层、接口是模块与其他部分的契约、为了测试而设计、the principle of one right place 对于每一段有用的代码应该只有一个地方看到他,并且只在一个正确的位置去做维护修改、拿不准时就用蛮力、画图。。。

6 Working classes

Abstact 本章介绍如何使用类,作为数据和操作的集合,是管理复杂度的首选工具。数据成员最好不要超过七个,函数成员也是越少越好。函数的抽象层级要保持一致。类与类之间的联系应该尽可能少,尽量让被人用自己,而自己少用别人。用别人的时候,一大串的调用链条是很危险的。inheritance会增加复杂性,如果这个子类不是真的有其特殊性,那就不要用,即便用,层次超过3层就已经很麻烦了。完成类的最后一步,是检查是不是已经消除了所有不必要的信息。

Keywords 接口抽象一致性, 七正负二, Liskov Substitution Principle, high fan-in and low fan-out, Law of Demeter

为什么需要类?

  • encapsulation: 集中一系列的数据与操作。隐藏自己,减少自己对别人、别人对自己的影响,减少外界感知自己的复杂度。
  • inheritance: 重用代码
  • polymorphism: 对一系列的obj做同样的操作

Good Class Interfaces

  • 接口抽象一致性: 应该展现一致的抽象层次,是对Employees class 的逻辑就不要用计算机逻辑NextListItem()而是NextEmployee()
  • 提供成对的服务,有开就有关
  • 不要对使用者做出任何假设,使用者不应该知道这些个接口的实现细节,比如尽量不要让接口之间相互依赖:method1()必须在method2()之前调用,可以用assert但是不要仅仅靠注释去解释
  • 采取保守的accessibility
  • 不要暴露data member
  • C++中不要把private data member 暴露给到头文件类的接口中
  • 避免使用friend class,因为它tight coupling

Design and Implementation Issues

  • Containment (“has a” Relationship)
    • 警惕有超过7个数据成员的类,人们在做事时能记住的离散项目个数是5~9个,平均7个
  • public inheritance 是”is a” relationship
    • 继承会给程序增加复杂度(尽管带来了重用的便利性),使用时要进行详细说明,要么就不要用它
    • Liskov Substitution Principle: 除非派生类真的是一个更特殊的基类,否则不应该从基类继承
    • 只有一个实例的类(singleton除外)、只有一个派生类的基类都值得怀疑
    • 派生后覆盖了某个子程序,但是其中没有做任何操作,也值得怀疑
    • 尽量使用多态,避免大量的类型检查 频繁重复出现的case意味着或许应该用继承了
  • 多重继承
    • 多重继承的主要用途是定义mixins,很复杂,会极大增加复杂性

什么时候用inheritance 什么时候用containment?

  • 重用数据用包含,一旦重用操作则用继承(继承的方便之处在于对同一类对象执行同样的操作)。由基类控制接口用继承,自己控制接口用包含。

Member Functions and Data

  • keep the number of routines in a class as small as possible
  • 禁止隐式自动生成的,但是自己并不需要的methods or operator
  • high fan-in, low fan-out: minimize direct routine calls to other classes
  • Law of Demeter: 间接调用也要尽量避免, e.g. A.CreateB().CreateC()
  • 尽量减少类和类之间相互合作的范围

Constructors

  • initialize all member data in all constructors, if possible, in the order in which they are declared.
  • Enforce singleton property by using a private constructor. 这时候当然要用static member了
  • 优先使用deep copy,论证可行后才用shallow copy

Classes to Avoid

避免god class,消除无关紧要的类,避免用动词命名类

7 High Quality routines

Abstract 如何写高质量的routine( = function + procedure)? 高内聚(只干一件事),清楚命名(动宾结构、与返回值有关的是function,与返回值无关的是procedure),长度尽量短,参数有序讲抽象层次。除非特定的理由,不要用macro and inline routines.

Keywords cohesion

除了常见的好处,还有,隐藏顺序,自我注解

Design at the Routine Level

Cohesion

  • For routines, cohesion refers to how closely the operations in a routine are related.
  • Good cohesion
    • 最强的内聚, Functional cohesion : 一个函数只做一件事情
    • sequential cohesion: 操作们顺序确定、共享数据 e.g.算年龄,再根据年龄算退休时间。
      • 解决方法:两个函数分别算年龄和算退休时间
    • communicational cohesion: 操作们仅仅共享数据
    • temporal cohesion: e.g. Startup(), Configuration() 这种内聚应该作为一系列事件的组织者,具体操作应该由里面的其他子函数完成。
  • Bad cohesion
    • procedural cohesion, 操作们顺序确定,不共享数据
    • logical cohesion, 一大堆if ... else .../case 混杂着不同层次的逻辑
      • 解决方案:Event handler 唯一的功能是发布各种命令,本身不做任何具体的操作。
    • coincidental cohesion: 一堆操作碰巧聚在了一起

Good Routine Names

  • 完整描述所做的所有事情
  • 不能含糊、通用
  • 不要通过数字区别不同的routine
  • 对返回值要有所描述
  • 使用语气强烈的“动宾”结构
  • 使用对仗词
    • add/remove, increment/decrement, insert/delete, show/hide, create/destroy, start/stop, get/put, old/new
  • 为常用操作确定coding style

How Long Can a Routine Be

越短越好,绝对不要超过200行

How to Use Routine Parameters

  • 按照输入-修改-输出的顺序排列参数,考虑自建inout关键字,甚至把参数加上前缀i_, m_, o_.
  • 类似的函数参数保持一致
  • 使用所有的参数
  • 把状态或者出错变量放在最后
  • 不要把子程序的参数用作工作变量,应该自己新建局部变量
  • 刚开始写函数的时候就document interface assumptions about parameters
  • 参数数量约7个以内
  • pass variables vs. pass objects
    • 答案是都可以,主要看函数的抽象层次。比如,Generally code that “sets up” for a call to a routine or “takes down” after a call to a routine is an indication that the routine is not well designed.

C is weakly typed. C++ and Java are strongly typed. TODO: see Strong and Weak Typing

Special Considerations

Function vs. Procedure?

Function有返回值,Procedure没有返回值

如果routine的主要用途是返回由其名字命名的返回值,就应该使用函数,否则,如果命名根本没有提到返回值的命名,用过程。

Return values

检查所有可能的路径,预设默认返回值是不错的做法。不要返回指向局部数据的引用或指针。

Macro Routines

#define Cube(a) a*a*a   --> (a)*(a)*(a) --> ((a)*(a)*(a))
                -----       -----------     -------------
        如果a是 x+1 就悲剧了 仍然可能被更高级的拆开    不错

还有就是比如 a 是 x++ 就会很隐晦 TODO see Macro and Security

Macros are useful for supporting conditional compilation (see Section 8.6), but careful programmers generally use a macro as an alternative to a routine only as a last resort. 不到万不得已不要用macro

Inline Routines

C++要求inline子程序写在头文件里面,违反了封装原则,除非profile代码觉得真的能够提高性能,否则不要用。

8 Defensive Programming

Abstract 承认程序总会出问题,严于律己宽以待人,所以我们需要Defensive Programming. 具体来说,有很多不同的技术,常用的有Assertion, Exception,前者面向自己,后者面向别人。有很多debugging aids可以在开发时候采纳,使得错误显现得更加明显;在Production code中要清理他们,使得错误不影响程序的运行。处处检查当然也会产生不必要的开销,因此要有主次之分。

Keywords Error-handling, Assertion, Exception, Barricade, Offensive Programming

总是检查输入

Assertion

assert(bool, msg)

在private里用assert, 在public里throw exception

用error-handling处理预期会发生的情况,用断言处理绝不该发生的情况

把断言看做是可执行的注解,如果检查的目标变量来自系统外部,用error-handling,如果来自可信的系统内部,用assert。

Design by Contract, 用断言来注解并验证precoditions and postconditions

Error-Handling Techniques

可以考虑的做法:换用下一个正确的数据,用最接近的合法值,把警告信息记录到日志,返回错误码,调用集中处理错误的对象或者routine,显示出错消息,局部最优地处理错误,关闭程序

Robustness vs. Correctness

  • 两者的选择很可能是冲突的,要根据具体情况trade-off. (注:开发的时候求Correctness, production求Robustness)

保持异常处理的一致性

Exceptions

什么时候抛出异常?遇到了意料之外的情况,举起双手说,“我不知道怎么处理,谁来告诉我该怎么办!”异常机制的优越性在于,提供了一种无法被忽略的错误通知机制,其他的机制可能会导致错误在不知不觉中扩散。

C++ vs. Java on Exception Handling

  • C++不支持try-finally,但是Java支持。
  • C++可以抛出各种数据类型,Java只能抛出Exception的派生类
  • 如果异常未捕获,C++ call std::unexpected() - std::terminate() - abort(), Java: if it is a “checked exception” terminate the thread. If it is a RuntimeException, 不产生任何影响 TODO checked vs runtime exceptions in Java
  • Java必须在类的接口中定义可能会抛出/捕获的异常,而C++不用

避免在构造函数和析构函数中抛出异常,除非你在同一地方把他们捕获

在恰当的抽象层次抛出异常

Coding Horror
1
2
3
4
5
class Employee {
    //                             EmployeeDataNotAvailable
    public TaxId GetTaxId() throws EOFException {
}
}

避免使用空的catch语句

考虑创建一个集中的异常报告机制,void ReportException(className, Exception)

想清楚为什么要使用异常,除了异常还有其他的error handling techniques, 不能仅仅因为编程语言提供了这种机制而使用他,这是一种典型的”Programming in a Language” 而非我们提倡的 “Programming into a Language”

Barricade Your Program to Contain the Damage Caused by Errors

在外部的不可信的脏数据和内部的可信的类之间搭起“防火墙”,统一负责数据的清理工作。这种清理甚至可以有多层。Barricades的外部用error-handling techniques, 内部用assertion. 如果内部assertion检测到了错误,说明错误来自于程序内部,而非外部数据的错误。

Debugging Aides

Don’t Automatically Apply Production Constraints to the Development Version. 为了开发便利,可以采取开发版本不允许的一些做法。

Introduce Debugging Aids Early

Use Offensive Programming: 在开发阶段让异常显现得更猛烈些吧,在产品运行阶段让它能够自我恢复。

Plan to Remove Debugging Aids

  • Use version control and build tools like make
  • Use a built-in preprocessor
  • Write your own preprocessor
  • Use debugging stubs (写routine检查,完结后去掉routine里面的东西)

Determine How Much Defensive Programming to Leave in Production Code

  • Leave in code that checks for important errors. Remove (with verison control, precompiler switches, etc.) code that checks for trivial errors.
  • Remove code that results in hard crashes. Leave in code that helps the program crash gracefully (显示debug信息)
  • Log errors for your technical support personnel

Being Defensive About Defensive Programming

The Pseudocode Programming Process

Abstract 本章讲述了如何利用伪代码简化routine的编码流程。伪代码让你更明确方向,更容易修改。

Keywords PPP

如何写routine,作者最喜欢 Pseudocode

  • 好的伪代码应该忽略编程语言的细节,在intent层面上编码
  • 伪代码可以作为注释,在写代码的时候逐渐被删除掉

在通过编译后,在debugger中逐行执行代码,以检查错误

一件事情完结的时候,不要忘记清理工作!不清理就是没做完!

Peer review! 绝大部分的问题出在自身

印度给人最初的映像是如此的“脏乱臭”:去孟买,下了飞机机场外面就是最大的贫民窟,公路不行。坐火车,里里外外全是人,被戏称为开挂了的民族。余秋雨在《我拒绝说它美丽》是这样描述恒河晨浴的:

这么多蚂蚁一般等死的人露宿河边,每天有多少排泄物?因此整个河岸臭气冲天……焚烧一直没停,恶臭扑鼻,工人们浇上一勺勺加了香料的油脂,气味更加让人窒息。这一切不仅让所有的人都能看到,而且居然成了恒河岸边最重要的景观。几个烧尸坑周围很大一片陋房,全被长年不断的烟火熏得油黑。 火光烟雾约十米处,浮着半头死牛,腔体在外,野狗正在啃噬。再过去几步,一排男人正刷牙咽水,一口又一口。

但是,普天之下,谁不知晓印度在IT行业的地位?

单就我个人所见,这辈子看的第一本编程书就是印度人写的。在耶鲁大学计算机系见识到了吃素还这么圆圆的印度女同学,以及言辞敏捷的博士生。找工作面试,在硅谷遇到了一大波印度大哥、大叔和大妈。而如今,我在微软工作的组尽管比较国际化,但也有不少印度人。有朋友说,他们组几乎全是印度人……更别说Satya Nadella临危受命微软CEO。当然我们华人在IT业也有Steve Chen, Jerry Yang, Qi Lu,但是高层队伍显然是不如印度人壮大的。

这是什么原因呢?也许正是因为印度基础设施建设比较落后,以至于那里大量的人们群众们不得不天天宅在家里,打坐、拜神、瑜伽、编程。还有就是,人多地少、基础设施差、资源竞争激烈,移民英语国家就好处更多,生活水平能够得到极大的改善,动力也就越足。英语作为第二官方语言,融入西方肯定比我们更加容易,也更加源远流长。

当然,仅仅靠自己意淫是不够的,于是我怀着崇敬的心情翻开《印度理工学院的精英们》这本书,想要一窥印度工程师教育的伟大之处。隔壁的隔壁的办公室有位小哥就毕业于IIT-Bangalore,俊朗冷漠……上到Gill Gates, Jeff Bezos, Mark Chandler,下到清华贵系的普通学生们,都对IIT的校友赞不绝口。

纵观此书,IIT校友之强悍,特色有三:

  1. 最强的学生接受着最好的美式高等教育资源。印度理工学院入学考试JEE年录取率约1.25%,在校学生女生仅约3%。号称地球上最难进的理工学院。这当然意味着他的学子们极富坚忍卓绝的品质:要么刻苦勤奋学习有方,要么聪明绝顶。某些校区甚至最初直接是在MIT, Cornell, Columbia, CalTech 等大学的帮助下建立起来的。既有印度大学也有美国大学的教授。
  2. 仰望星空(天之骄子的责任感),脚踏实地(大量的实习项目)。学生有着很高的社会认同感,不需要通过非学术平台表达呼声,除了培养“能力”,不用操心其他。承载着“中产阶级梦想”,毕业理所当然要成为领导者(思维惯性!)。良好的工学传统能在诸多方面体现出来,比如,印度理工的毕业论文需要准备300个小时,而麻省理工需要120个小时。《三个傻瓜》里面能够看到他们在车间里上课。
  3. 适应高压力的竞争、为达到目的不择手段的务实精神。想不到这种学校也有作弊的传统,在Kharagpur校区,“考试作弊”的艺术被称为拓扑学(topology)。对高强度的工作、紧迫的deadline的高度习惯,平常对待这些压力直到最后一刻,是成功者不可或缺的品质。

有趣的是,此书在强调个人的智力、心理素质之外,似乎从未涉及体育运动方面的话题。而我注意到清华建校之初就有其独特的体育传统,是很多其他学校见不到的,学生尽管如此崇尚学术、思想自由,但却又秩序井然、按部就班崇尚集体,对竞技体育热情似火fight to the finish and never say die。他们成群结队来到田径场、体育馆、篮球场、游泳池参与体育活动。当年在清华时实验室的小伙伴们就有羽毛球、轮滑、单车、游泳(其实是为了洗澡吧……)等诸多喜好。

另外,此书还着重强调了文科教育对领导力的重要性。IIT学子与清华北大比起来,缺乏强有力的政治理想。或许是因为课业过于繁重,而种姓和背景关系问题堪比我国更甚吧。此书有两处地方让我记忆深刻引以为戒:

可悲,他们对于印度经济或者政治状况或与社会有关的任何事都漠不关心,他们是非常乏味的一群人,这真是让人气愤,那些学生们成天只知道埋头苦学计算机编程,除此之外,别无其他。而且,还成天梦想着成为硅谷的百万富翁、或者跨国公司的首席执行官。要想实现这一梦想,你至少需要了解一些除了编程或者答题技巧之外的东西吧。

为了考试放弃了其他一切活动,从而变得孤僻……想象一下,一个19岁的男生5年没看过电影,没读过一本课外书,没看过电视甚至连女孩都没有追过,这是怎样的一个人啊。

当然,高等教育的绝大多数理想是共通的。此书强调,学术严谨,生活自由,严谨意味着高门槛,自由意味着趣味性与多样性。还有一个值得注意的点是,个人利益与国家利益的一致性。爱国不是教育出来的,国家只有把两者调和一致了,才能真正让人感受到,爱国就是爱自己。书中人物一致认为印度不因当限制“人才外流”,更有人把海外人才视为“人才银行”,是一种储备,而非损失。

书中开篇就提到了,印度中产阶级的两大理想,一是通过个人奋斗出类拔萃,即便“上面没人”也能够成为杰出领袖;二是到美国去。此书有不少章节谈到“美国vs回国”的问题。大体来讲,留在美国的好处是:

  1. 舒适的生活;
  2. 追求一流的科学技术的使命;
  3. 强烈的正义感和公平意识。

回到印度的原因:

  1. 家庭;
  2. 机会。

此处不必过多解释,在美帝打工的同志应该会深有共鸣吧。

最后,此书此书总体感觉略显浮夸(确实是印度人的风格),全篇没有意外,若非特别好奇,不看也罢。就交流沟通和工程能力上,中国工程师的确应该好好向印度同仁们看齐。此书翻译有些许错误,比如176页,斯坦福大学显然不在康涅狄格州,那是我大康州的Stamford!

EOF