为什么不要把zookeeper 安装用于服务发现

技术人员在大公司要面对的问题
个人成长,方法大致是两种,第一是主动学,现在互联网这么开放,IT行业中的知识,只要你想学,几乎没有找不到的资料。基本上,稍微靠谱点的技术人才,都具备主动学习的素质,然而这种学习方式,无论是看书、读博客、上在线课程…… 都有个非常明显的缺点,就是缺乏对问题的直观体验,几年前我看《Java Concurrency In Practice》,囫囵吞枣,表面上懂了,实际上压根没理解。近期当我面对一个比较典型的并发问题的时候,再翻出那本书,忍不住一口气读了几十页,因为实在是太对胃口了!所以第二种学习方法往往更为重要,那就是:面对问题,解决问题,这是一种基于体验的成长,比基于纯理性的记忆理解,深刻得多。
所以,有哪些问题,大公司需要面对?小公司不需要面对?我总结下基本是三个问题:
大公司服务的用户数量级相比小公司不在一个层次。
大公司需要考虑如何保持数百数千程序员高效工作。
大公司处理的业务往往非常之复杂。
当然上述几点并不总是正确的,比如现在会有一些小公司服务千万级别的用户,也需要面临类似的技术问题。但大体上就是这些问题,大公司必要面对,小公司在早期是不需要面对的。
上述三个问题实际上是 Scalability 的问题,具体我推荐大家看的详细阐述。解决上述三个问题,需要技术人员具备怎样的能力?
三个问题要求你学什么?
第一个问题,如果面对百万级、千万级的用户,是被大家讨论最多的,具体的技术会涉及到:无状态应用、负载均衡、分布式缓存、分布式队列、高性能Web服务器、数据分库分表…… 在大公司,也许根本轮不到你去开发分布式缓存,但只要你留点心,就能很快理解在什么情况下该用分布式缓存,它能带来多少性能提升,命中率还有多少提升空间,等等。这一块,是大家面试的时候比较喜欢问题的,没做过的,都觉得很酷很牛逼的样子,经历过了,感觉其实也就那样,关键是你遇到问题了,并且用这些技术解决了。
第二个问题,如何管理数百人的研发部门,更多的是被很多纯管理职位的经理在讨论。普通程序员,面对由于跨团队跨部门沟通所带来的消耗,10个程序员估计会有11个骂娘;然而问题始终是客观存在的,我现在的日常工作一直遇到类似的问题,我也问过 Facebook 的工程师,人家也坦然承认,较之与 Facebook 早期,他们现在研发的效率也的确慢了很多。
保持自己工作高效,并不是个特别困难的问题;保持2-3人小团队工作高效,也不难,只要大家志趣相投、目标一致基本就可以了;10人左右的团队,就需要聪明的管理者花许多时间去理解大家的想法并协调。当然,愚蠢的管理者只会开一大堆无意义的会,刷点存在感,传达点上面给的压力。团队规模再扩大,带领上百人团队的中层管理者,他就需要去帮助一线管理者了,这个超出我的经验了。我想说的是,管理也是技术,不比编程更难,但也不见得比编程简单。
除了管理能力外,保持大规模团队的研发效率,还需要规范和工具支撑。使用一致的基础设施(如版本控制、测试环境、发布流程、沟通协议),规范化大家的代码组织结构,抽取共有的技术服务,防止重复造轮子…… 这些都是非常具体、非常现实的技术问题。小公司几万行的代码做整体技术升级,找个牛逼的程序员就能搞定了,大公司几十万、几百万的代码做技术升级,没有任何一个英雄主义程序员能搞定,解决这类问题需要有前瞻性的架构,需要有善于沟通的架构师。虽然过程中难免会需要和人扯皮开会,但做好了也是极富有成就感的事情。
第三个问题,在大公司要面对更大的业务复杂度。也许是大公司产品经理太多了,大家都想折腾点东西出来,所以各种功能特性不停加不停变。这时候技术人员就不得不去理解各种业务的含义,我们都知道,如果实现和业务意义不吻合,最终的代码就会变成一个无人可维护的怪胎,因此优秀的技术人员就能很好识别业务边界,把小怪兽关在各自的笼子里,防止他们聚在一起搞得天翻地覆。再优秀的技术人员,就能说服产品经理,“这么干是不对的”。这方面我推荐。
大公司的普遍弊端
我并不是说任何一个大公司的程序员都会去面对上面三个问题,事实上刚入职的新人程序员一般只会处理很小业务范围内的没有太大挑战的任务。不过只要你有兴趣并持续提高自己能力,还是有很大机会去面对并处理这些问题的,因为公司的管理者终究是期望有人站出来帮他们排忧解难的。不过大公司或多或少都有一些普遍的问题。
首先是人浮于事、文山会海。有很多人不停开会、不停写邮件,就是不干活,搞的你也没好心情干活。吐槽归吐槽,我还是会仔细想想为什么这样。首先,沟通是必要的,两三个人干活打个招呼就行了,十多人干活就需要开会,事情多了,会也自然多,再加上很多人其实没有基本的主持会议技能,那很容易搞成垃圾会议。其次,公司大了自然会有一些兵油子,每句话说出来都大方得体,但就没见他把事情落地,更别提自己挽起袖子干了,比较麻烦的是这些人普遍层级还相对高点。我能做的,就是离他们远点。
其次是目光狭隘。如果一个程序员刚毕业就来到大公司,而且恰好这几年这家大公司业务和技术突飞猛进,那他的眼光就容易受限,觉得自己的公司全国甚至全世界最牛逼,再加上我们中国人普遍存在的报喜不报忧文化,他的这种盲目就更容易被环境所固化了。于是一不小心,一些人的技术视野就变得很窄,我在内部推广Git的时候,持疑惑最大的也就是一些工作年限较长的资深工程师。
还有,我个人比较头大的是,目标不一致。跨团队、跨部门沟通的时候,你很容易发现自己在鸡同鸭讲。可能团队A关心技术架构,团队B关心业务指标,然后你上升一层,在部门层面做个决策先做什么后做什么。一会你又发现,部门A关心业务指标、部门B关心基础建设,你又得上升一层,可那一层离你好远…… 所以你会发现很多大公司很多人做事很大程度上是在靠个人影响力,而不是正规的流程。如果我曾经做过些成功的事情,我平时对大家也比较热心,那我做一些事情的时候,一些人会把自己的目标暂时放一边,来帮你一把,这就是俗称的“刷脸”。
总结之前,有一点我要额外提一下,大公司毕竟是藏龙卧虎的地方,各个领域都有比较资深的人存在,如果公司文化鼓励分享,那你就很容易找机会请人家喝杯咖啡,聊聊。
做任何选择,如果你不考虑失去什么,只考虑得到什么,那就是典型的幼稚。因此选择大公司还是小公司,你不仅得明白你期望收获什么,还得坦然面对要失去的东西。我说这么多,基本就是告诉你,有些东西你肯定会失去,还有一些东西,如果你努力,你可能会得到。
原创文章,转载请注明出处, 本文地址:
Categories:
ZooKeeper 是分布式环境下非常重要的一个中间件,可以完成动态配置推送、分布式 Leader 选举、分布式锁等功能。在运维 AliExpress ZooKeeper 服务的一年多来,积累如下经验:
1. 集群数量
3台起,如果是虚拟机,必须分散在不同的宿主机上,以实现容灾的目的。如果长远来看(如2-3年)需求会持续增长,可以直接部署5台。ZooKeeper集群扩容是比较麻烦的事情,因此宁可前期稍微浪费一点。
2. 客户端配置域名而不是 IP
如果有一天你的 ZooKeeper 集群需要做机房迁移,或者其中几个节点机器挂了,需要更换。让你的用户更新 ZooKeeper 服务器配置不是件轻松的事情,因此一开始就配置好域名,到时候更新 DNS 即可。
3. 开启 autopurge.snapRetainCount
ZooKeeper 默认不会自动清理 tx log,所以总会有一天你会收到磁盘报警(如果你有磁盘监控的话)。开启自动清理机制后,就不用担心了,我的配置如下:
autopurge.snapRetainCount=500
autopurge.purgeInterval=24
autopurge.snapRetainCount=500autopurge.purgeInterval=24&
如果你可以接受停止服务半个小时,那基本随意玩了,但在比较严肃的环境下,还是不能停服务的。我的做法是这样的:
0. 有节点 A, B, C 处于服务状态
server.3=192.168.12.1:
server.4=192.168.12.2:
server.5=192.168.12.3:
server.3=192.168.12.1:2888:3888server.4=192.168.12.2:2888:3888server.5=192.168.12.3:2888:3888&
1. 加入节点 D,配置如下:
server.3=192.168.12.1:
server.4=192.168.12.2:
server.5=192.168.12.3:
server.6=192.168.12.4:
server.7=192.168.12.5:
server.3=192.168.12.1:2888:3888server.4=192.168.12.2:2888:3888server.5=192.168.12.3:2888:3888server.6=192.168.12.4:2888:3888server.7=192.168.12.5:2888:3888&
用 4 字命令检查,保证该节点同步完毕集群数据,处于 Follower 状态:
echo srvr | nc 192.168.12.4 2181
Zookeeper version: 3.4.5-1392090, built on 09/30/ GMT
Latency min/avg/max: 0/0/13432
Received: ***
Connections: ***
Outstanding: 0
Zxid: 0x***
Mode: follower
Node count: ***
1234567891011
?&&~&&echo srvr | nc 192.168.12.4 2181Zookeeper version: 3.4.5-1392090, built on 09/30/2012 17:52 GMTLatency min/avg/max: 0/0/13432Received: ***Sent: ***Connections: ***Outstanding: 0Zxid: 0x***Mode: followerNode count: ***&
需要注意的是,这一步加入的节点的 id,必须大于集群中原有的节点的 id,例如 6 & 3,4,5,我也不知道为什么需要这样。
2. 同上一步一样,加入节点 E
3. 更新 A B C 的配置如 D 和 E,并依此重启
5. 机房迁移
例如要把服务从 X 机房的 A B C 迁移到 Y 机房的 A’ B’ C’。
做法是首先把集群扩容成包含6个节点的集群;然后修改域名指向让用户的连接都转到 A’ B’ C’;最后更新集群配置,把 A B C 从集群摘除。
6. 跨机房容灾
由于 ZooKeeper 天生不喜欢偶数(怕脑裂),因此有条件的就三机房部署,但机房之间的网络条件得是类似局域网的条件,否则性能就堪忧了。
双机房做自动容灾基本不可能,加入手动步骤是可以的,和 DB 一样,短时间不可用,立刻启用另外一个机房,平时保证数据同步。
三机房部署,如果一个机房离的比较远,网络延迟较高怎么办?可以 3 + 3 + 1 部署,1 就放在那个网络延迟较高的地方,确保 leader 在 3 + 3 这两个机房中间,那么平时的性能就能保证了。怎么保证 leader 不到 1 呢?目前能想到的办法就是如果发现就重启它。
原创文章,转载请注明出处, 本文地址:
Categories:
今天读到一篇博客,标题为 ,作者 MARTY ABBOTT 的观点大致为,DevOps 把开发、QA、运维等环节打通,解决或缓解了传统 IT 所遇到的很多问题,然而这还不够,我们需要认清最根本的两个问题。第一个问题是:传统企业的 IT 基础设施和架构是为了支持各种各样的第三方软件,其目的是节省成本而不是创新;第二个问题是:传统的软件开发和维护是分离的,开发软件更多的关注是把它卖了,而不太关心软件最终如何运行。
然而,今天行业发生了巨大的变化:
现在,客户购买服务而不是软件。要在这个全新的世界获得成功,就需要改变观点,即组成这些服务的原材料不仅包括软件,也包括硬件(基础设施)。如果你的团队没有合理地组合这些原材料,那么最终服务也不会如预期的那么好。如果继续“生产”软件,然后“托管”在基础设施上,那么最好的结果也仅仅是次优的解决方案,最差的结果则可能是一场灾难。
MARTY 又接着说,我们应该:
把团队组织成跨职能的、实用且多样的、自治的团队,各个团队拥有整个产品中独立的一些服务。各个团队和服务应该由集中面向业务的 KPI 驱动。这些团队应该能够独立自主地开发、部署、支持自己的服务,不需要依赖其他团队。
我们都知道,如果一件工作需要跨团队合作,工作效率会急剧下降,各种推责扯皮的事情就会冒出来,管理成本直线上升,需要富有经验的项目经理协调,需要指定共同的目标,需要开很多很多的会…… 在这种背景下,快速地应对市场变化、开发创新的产品就难免变得困难重重。为什么现在很多很小的互联网公司能那么快速地发展起来,我想一个很大的原因就是因为他们够快, 够“敏捷”,现在很多基础设施如代码托管、服务托管、应用程序监控都有了比较成熟的市场及服务(至少在国外),它们中有我们耳熟能详的 Github、Amazon EC2、New Relic 等等,在这些基础设施足够灵活、足够稳定的情况下,小型互联网公司能够专注自己产品和服务,技术人才也比较容易“跨职能”,设想在传统的大的 IT 公司内,一名开发人员要了解运维体系几乎是不太可能的事情,但现在如果是用 Amazon EC2,则只要学习一些简单的 API 即可,而且相关工具也更加简单。
市场上成熟的 IAAS/PAAS 帮开发人员简化了很多问题,也使得程序员关注 OPS 的门槛降低了。除此之外,敏捷运动在帮助开发团队跨职能方面也功不可没,尤其是,敏捷强调开发和测试角色的融合,事实上,现在优秀的开发人员都具备比较强的测试意识和测试技能,而像 Google 那样的公司,专职测试的研发能力较普通开发来说有过之而无不及(见)。也正因为如此,如果一个团队的成员都比较优秀,再加上有比较出色的产品经理,那么拥有一支靠谱的跨职能团队也不是那么遥不可及的事情,他们能够每天直面用户,以小时为单位做出响应,因此在市场中有巨大的竞争优势。
对于一名普普通通的程序员(像我这样),应该如何面对这样的行业趋势?如何才能保持自己的竞争优势,不至于在40岁的时候被裁员、被淘汰?
首先是观念的转变,“程序员”或者“软件工程师”这样的头衔也许已经过时了,实际上我们应该是做服务的,这一点和开餐厅、开发廊没有本质的区别。唯有做到吸引更多的客户、让客户满意,才能保证组织的生存和壮大,也进而是我们自己的发展和壮大。怎样做才能吸引客户?才能让客户满意呢?那你得理解他,例如通过运维数据去理解;你得保证服务的质量,要保证可用性,保证功能正确;你得有创新,让客户眼前一亮。因此,你不仅要和机器打交道,你还要学会通过各种方法和客户打交道。
其二,在观念转变的基础上,有针对地拓展自己的技能。为什么要学习自动化测试?因为它能帮助我们提升软件质量,让客户满意;为什么要学习 DevOps 方法论和相关工具?因为它能让我们更快发布软件响应客户的需要;为什么要关注性能?因为客户是人,人的耐性是有限度的;为什么要关注可用性?因为客户是人,一般人如果遇到 Starbucks人满为患,它会去对面的 Costa 买拿铁的。
我无法想像20年后程序员后会用什么语言怎样开发软件,正如20年前也很少有人能想到今天移动互联网的如火如荼,也许届时程序员这个职业也会渐渐消失,但不论如何,我相信通过技术服务人的人性需求,这样的一个基本事实是不大可能发生变化的。
原创文章,转载请注明出处, 本文地址:
Categories:
随着的流行,相比较以前一个大型应用程序搞定所有需求,我们现在更倾向于把大型应用程序切分成多个微服务,服务之间通过 RPC 调用。微服务架构的好处非常多,例如稳定的服务变化较少,不会被非稳定服务所影响;不同的服务更方便交给不同的人管理;发布、扩容等操作也更加有针对性。不过这也不是没有代价的,额外的成本最主要的可能就是运维成本。
我们维护的一个产品,由 7 个微服务构成,它们各司其职,承担上行、下行、同步等各类职责,我非常喜欢这种架构,但也面临一个小小的烦恼。每次我们发布其中一个或者多个服务,就需要去验证服务的健康度,极限情况下,7 个服务 x (国内环境 + 国外环境)x (预发布环境 + 生产环境),总共需要验证 28 次!我希望有简单、标准、自动的方式去验证这些服务是否健康。当然,验证健康也不是跑一个完整的回归测试,那是在测试环境就需要完成的事情,健康检查基本只是关注环境是否 OK,最核心的一两个用例是否 OK。由于部署到预发布或者线上的代码,和线下测试的代码是一致的,因此就不需要重复验证各种功能了,关注点应该在环境上,这一点线上和线下是有明显区别的。至于环境区别,通常就是磁盘、数据库、其他分布式服务等等。
此外,我还希望所有服务的健康检查接口是完全一致的,没有人希望检查服务 A 的时候用 url /ok,检查服务 B 的时候用 url /good。
我曾尝试定义一个健康检查协议,让所有服务都暴露一个HTTP接口 ,返回的内容就包含这个这个服务的基本状态。
这几天看 Spring Boot ,发现它已经很好地集成了我想要的功能,而且看起来更简单,因此我就直接扔掉了自己定义的协议,改而使用 Spring Boot 的方式,Spring Boot 有一个称之为 endpoint 的概念,每个 endpoint 是一个非常简单的 HTTP 接口,用户可以通过 endpoint 监控 Spring Boot 应用,甚至与之交互。这其中,最简单的 endpoint 就是 health,只要加入必要的 Spring Boot 依赖,用户就能通过 health 查看 Spring Boot 应用的基本状态。
$ curl http://localhost:8080/health
"status":"UP"
$ curl http://localhost:8080/health{&&&&"status":"UP"}&
这里我们看到服务的状态是 UP,不过也许这个检查太简单了,例如我的服务依赖其他外部服务,其中一个 Tair,一个是 TFS,这两个都是强依赖,如果它们有问题,我的服务就应该是 DOWN 的状态,在 Spring Boot 中,可以这么扩展:
@Component
public class MyHealth implements HealthIndicator {
public Health health() {
return new Health.Builder()
.withDetail("tair", "timeout") // some logic check tair
.withDetail("tfs", "ok") // some logic check tfs
.status("500")
1234567891011121314
@Componentpublic class MyHealth implements HealthIndicator {&&&&&@Override&&&&public Health health() {&&&&&&&&return new Health.Builder()&&&&&&&&&&&&&&&&.withDetail("tair", "timeout") // some logic check tair&&&&&&&&&&&&&&&&.withDetail("tfs", "ok") // some logic check tfs&&&&&&&&&&&&&&&&.status("500")&&&&&&&&&&&&&&&&.down()&&&&&&&&&&&&&&&&.build();&&&&}}&
只要加入一个 bean 实现 HealthIndicator 就能实现更加全面的检查,现在访问 health endpoint 是这样的:
$ curl http://localhost:8080/health
"status": "DOWN",
"tair": "timeout",
"tfs": "ok"
$ curl http://localhost:8080/health{&&&&"status": "DOWN",&&&&"tair": "timeout",&&&&"tfs": "ok"}&
只要在每个服务稍微实现一些基本的环境检查,那我就可以用几行脚本快速地完成 7 个服务 x (国内环境 + 国外环境)x (预发布环境 + 生产环境)的健康检查,如果有哪个服务出问题了,定位环境问题也是非常方便的。
这种监控是实时的,这一点非常重要。在实际工作中我们其实有非常完善的系统监控平台,平台能提供 CPU、内存、磁盘、网络IO、JVM 等等各种各样非常全面的信息,这种平台的优势有历史趋势记录,有汇总,有比较,劣势就是不够实时,通常只能看到 5 分钟前的数据。因此,在发布服务,扩容的时候,等待这样的系统监控平台反馈就不够了。
除了 health endpoint 之外,Spring Boot 还提供了,它们都是针对运维设计的,例如可以用 shutdown endpoint 来关闭服务、用 beans endpoint 来查看所有的 Spring Bean,下面我想详细讲一下 metrics 这个 endpoint。
默认访问 metrics 我们能得到很多信息,包括 JVM 的线程数、内存、GC 数据等等……,这些都是系统级别的数据,但其实我们可以通过 metrics 收集实时的业务数据,例如每分钟用户登陆数量、每分钟文件同步数量、实时的缓存命中率……等等。
实现是这样的:
@Component
public class MyMetric {
private final CounterService counterS
private final GaugeService gaugeS
@Autowired
public MyMetric(CounterService counterService, GaugeService gaugeService) {
this.counterService = counterS
this.gaugeService = gaugeS
public void exampleCounterMethod() {
this.counterService.increment("login.count");
// reset each minute
public void exampleGaugeMethod() {
this.gaugeService.submit("cache.hit", 80.0);
12345678910111213141516171819202122
@Componentpublic class MyMetric {&&&&&private final CounterService counterService;&&&&private final GaugeService gaugeService;&&&&&@Autowired&&&&public MyMetric(CounterService counterService, GaugeService gaugeService) {&&&&&&&&this.counterService = counterService;&&&&&&&&this.gaugeService = gaugeService;&&&&}&&&&&public void exampleCounterMethod() {&&&&&&&&this.counterService.increment("login.count");&&&&&&&&// reset each minute&&&&}&&&&&public void exampleGaugeMethod() {&&&&&&&&this.gaugeService.submit("cache.hit", 80.0);&&&&}}&
Spring Boot 内置了两个 Service,CounterService 可以用来做简单的累加累减,GaugeService 可以用来存放简单的 double 值,数据都存放在内存中。
现在访问 metrics endpoint 的效果是这样的:
$ curl http://localhost:8080/metrics
"counter.login.count": 42,
"counter.status.200.beans": 1,
"counter.status.200.metrics": 9,
"counter.status.200.root": 4,
"gauge.cache.hit": 80.0,
"gauge.response.beans": 55,
"gauge.response.health": 12,
"gauge.response.metrics": 4,
12345678910111213
$ curl http://localhost:8080/metrics{&&&&"counter.login.count": 42,&&&&"counter.status.200.beans": 1,&&&&"counter.status.200.metrics": 9,&&&&"counter.status.200.root": 4,&&&&"gauge.cache.hit": 80.0,&&&&"gauge.response.beans": 55,&&&&"gauge.response.health": 12,&&&&"gauge.response.metrics": 4,&&&&...}&
Spring Boot 的 metrics endpoint 带了很多的信息,这里我们只关注自定义的数据。
如果所有服务的核心业务数据都通过 metrics 暴露,我们接下来要做的无非就是通过一些数据可视化的 JavaScript 组件访问这些数据,做成一个 Dashboard,那我们就能通过这样一个 Dashboard 查看系统的实时状态。
Spring Boot 的 Endpoints 带着强烈的 DevOps 色彩,,开发不仅要关心如何实现功能,还需要关心服务在线上运行的状态,如果缺乏实时监控,维护线上服务必然是一场噩梦。如果基于 Spring Boot 开发服务,那只需要稍作扩展,实时监控就足够用了,就算不使用 Spring Boot,类似的思路自己实现也并不复杂。
原创文章,转载请注明出处, 本文地址:
Categories:
上周参加了一个 Michael Feathers 的 workshop,整个 workshop 内容非常多,其中大部分是围绕他那本著名的所阐述,不过除此之外 Michael 还以 Beyond Error Handling, Using Design to Prevent Errors 为主题,讲了不少如何优雅地处理错误的做法和思路。其中的一个例子涉及并融合了面向对象和函数式的相关知识,引发我的一些思考,本文就此做一些总结。
例子的代码很典型也很简单,简化后,主要涉及三个类,分别是 Inventory, Item, 和 Sale,代码如下:
public class Inventory {
private HashMap&String,Item& items = new HashMap&String,Item&();
public Inventory() {
items.put("1", new Item("Preserved Duck Eggs", 150000));
items.put("2", new Item("Milk", 7000);
items.put("3", new Item("Tomato", 5500));
public Item itemForBarcode(String barcode) {
return items.get(barcode);
1234567891011121314
public class Inventory {&&&&private HashMap&String,Item& items = new HashMap&String,Item&();&&&&&public Inventory() {&&&&&&&&&&&&&&&&items.put("1", new Item("Preserved Duck Eggs", 150000));&&&&&&&&items.put("2", new Item("Milk", 7000);&&&&&&&&items.put("3", new Item("Tomato", 5500));&&&&&& &&&&}&&&&&public Item itemForBarcode(String barcode) {&&&&&&&&return items.get(barcode);&&&&}}&
public class Item {
public Item(String name, int price) {
this.name =
this.price =
public String getName() {
public int getPrice() {
123456789101112131415161718
public class Item {&&&&private String name;&&&&private int price;&&&&&public Item(String name, int price) {&&&&&&&&this.name = name;&&&&&&&&this.price = price;&&&&}&&&&&public String getName() {&&&&&&&&return name;&&&&}&&&&&public int getPrice() {&&&&&&&&return price;&&&&}}&& &
public class Sale {
private SaleEventL
private Inventory inventory = new Inventory();
private ArrayList&Item& items = new ArrayList&Item&();
public Sale(SaleEventListener listener) {
this.listener =
public void addBarcode(String barcode) {
Item item = inventory.itemForBarcode(barcode);
items.add(item);
listener.itemAdded(item);
public void subtotal() {
Money sum = new Money();
for(Item item : items) {
sum = sum.add(item.getPrice(items));
listener.subtotaled(sum);
123456789101112131415161718192021222324
public class Sale {&&&&private SaleEventListener listener;&&&&private Inventory inventory = new Inventory();&&&&private ArrayList&Item& items = new ArrayList&Item&();&&&&&public Sale(SaleEventListener listener) {&&&&&&&&this.listener = listener;&&&&}&&&&&public void addBarcode(String barcode) {&&&&&&&&Item item = inventory.itemForBarcode(barcode);&&&&&&&&items.add(item);&&&&&&&&&&&&&&&&listener.itemAdded(item);&&&&}&&&&&public void subtotal() {&&&&&&&&Money sum = new Money();&&&&&&&&for(Item item : items) {&&&&&&&&&&&&sum = sum.add(item.getPrice(items));&&&&&&&&}&&&&&&&&listener.subtotaled(sum);&&&&}&& }&& &
想象我们去超市购物,然后到收银台结账,当我们把一车东西交给收银员的时候,她会对每件东西(Item)逐一扫描条形码(barcode),扫描条形码的时候系统会去库存(Inventory)中查询这件东西的名称和价格,扫描完成后,收银员会计算总价格(subtotal)。上述代码所表述的就是这个场景。
读者不妨花5分钟理解一下上述代码,接着考虑一个问题,如果扫描的条形码在库存中不存在怎么办?具体来说 inventory.itemForBarcode(barcode); 返回 null 怎么办?下面的所有讨论都围绕这个问题展开。
如果我们随意把 null 传来传去,什么都不干,那必然早晚会出现 NullPointerException ,那么最简单的做法是加上 null 判断以保证错误的 null 不会再被传出去:
public void addBarcode(String barcode) {
Item item = inventory.itemForBarcode(barcode);
if (item == null) {
items.add(item);
listener.itemAdded(item);
&&&&public void addBarcode(String barcode) {&&&&&&&&Item item = inventory.itemForBarcode(barcode);&&&&&&&&if (item == null) {&&&&&&&&&&&&return;&&&&&&&&}&&&&&&&&items.add(item);&&&&&&&&&&&&&&&&listener.itemAdded(item);&&&&}&
这可能是我们能见到的最常见的做法,代码中到处充满了 null 判断,在某种程度上把代码主要想表达的主干逻辑给污染了,过多 null 判断影响了代码的可读性。如果所有使用变量的地方都要去检查 null,那代码必然会恶心的不行,实际中我们不会这么干,因为变量的值什么来的,很多时候也是由我们自身控制的。但如果我们在使用第三方的代码,那为了保护自己,检查 null 则是比较明智的选择了。因此,在自己的代码库内部,尽量避免传 null 并去掉 null 判断;而在集成第三方代码的时候,则多防御性地使用 null 判断。
如果我们设计一个 API,那显式地告诉别人是否可以传入 null,是否会返回 null,则是很好的习惯,例如:
public Item itemForBarcode(@Notnull String barcode)
@Nullablepublic Item itemForBarcode(@Notnull String barcode) &
上述代码表示传入的 barcode 不能为 null(如果是 null,不保证会发生什么后果),而返回的 Item 则可能是 null,这样实现的时候我就不需要去判断 barcode 是否为 null,另外用这个接口的人也明确知道自己要检查返回是否为 null。@Nullable 和 @Notnull 都已经在
中明确定义。
避免 null 判断的一种最直接做法就是抛出异常,例如:
public void itemForBarcode(String barcode) throws ItemNotFoundException {
if (items.containsKey(barcode)) {
return items.get(barcode);
throw new ItemNotFoundException();
&&&&public void itemForBarcode(String barcode) throws ItemNotFoundException {&&&&&&&&if (items.containsKey(barcode)) {&&&&&&&&&&&&return items.get(barcode);&&&&&&&&} else {&&&&&&&&&&&&throw new ItemNotFoundException();&&&&&&&&}&&&&}&
遇到 ItemNotFoundException 怎么办?继续往上抛?处理掉?记个日志然后吃掉?在这个上下文中,我无法给出明确的答案,具体选择哪种方式还得看具体场景,通常来说,**先想想当你遇个异常的时候,你的用户希望得到怎样的结果?然后基于这点来指导你的异常处理方式。**
异常在很多情况下是很适用的,例如磁盘坏了,那抛出 IOException 让系统 Fail Fast 往往是一种很好的处理方式,然后异常也有不少问题,首先 Java 中的 Checked Exception 很容易让 API 变得丑陋无比;其次异常的跳跃式的抛来抛去,也让有 goto 的感觉,很难 follow 。
有没有更好的办法保持 API 的简洁,同时也能避免 null 判断呢?
Null Object 模式
在面向对象设计中,我们常常可以使用
来去掉 null 检查逻辑或者异常捕捉,该例中,我们可以创建一个 NotFoundItem ,它继承自 Item,不过 name 和 price 比较特殊:
public class NotFoundItem extends Item {
public NotFoundItem() {
super("Item not found", 0);
public class NotFoundItem extends Item {&&&&&public NotFoundItem() {&&&&&&&&super("Item not found", 0);&&&&}}&
然后再 Invetory 中适当地返回 NotFoundItem:
public void itemForBarcode(String barcode) {
if (items.containsKey(barcode)) {
return items.get(barcode);
return new NotFoundItem();
&&&&public void itemForBarcode(String barcode) {&&&&&&&&if (items.containsKey(barcode)) {&&&&&&&&&&&&return items.get(barcode);&&&&&&&&} else {&&&&&&&&&&&&return new NotFoundItem();&&&&&&&&}&&&&}&
这样,所有使用 Inventory.itemForBarcode() 的地方都不需要特殊的错误处理了,例如在 Sale 类中,addBarcode() 和 subtotal() 都能正常工作。想象一下,如果有五个、十个、或者更多的类使用 Inventory.itemForBarcode(),这样可以简化多少代码!因此,如果有可能,尽量在下层完成错误处理,因为越往上层,需要的错误处理代码往往越多。这实际上是契合
的,这条原则是这么说的:
Code that sends commands or data to other machines (or to other programs on the same machine) should conform completely to the specifications, but code that receives input should accept non-conformant input as long as the meaning is clear.
Inventory.itemForBarcode() 能够接受不合法的 barcode ,但是它返回的 Item 是符合接口定义的,没有特殊性。
到目前为止一切似乎看起来很美好,但实际上 Null Object 模式不是完美的,想象一下,如果我们要在 Sale 类中加入这样一个逻辑:如果购买物品的数量达到了10,则有5%的折扣。显然 NotFoundItem 会破坏这样的逻辑,扫描1个合法 barcode 加9个不合法的 barcode 也会造成 5% 折扣。
我们花了大量的精力对付 null,事实上 null 这个概念的发明人
I call it my billion-dollar mistake.
是否有其他更好的方案来解决这个问题呢?让我们来到函数式的世界,看看 Scala 是怎么对付 null 的。Scala 内置了一个特殊类型,叫做 Option,顾名思义,可以认为 Option 可能包含一个值,也可能不包含,例如我们可以修改 Inventory.itemForBarcode() 如下:
def itemForBarcode(barcode: String): Option[Item] = {
if (items.contains(barcode)) Some(items(barcode))
&&def itemForBarcode(barcode: String): Option[Item] = {&&&&if (items.contains(barcode)) Some(items(barcode))&&&&else None&&}&
这一段 Scala 代码也比较好理解,*itemForBarcode* 接收一个 String 参数,返回 Option[Item],而*Option[Item]* 有两种子类型,一种是 Some[Item],表示有实际的 Item,另外一种是 None,表示没有。
现在 Sale.addBarcode() 是这么处理 Option[Item] 的:
def addBarcode(barcode: String) {
inventory.itemForBarcode(barcode).foreach(item =& {
items :+ item
listener.itemAdded(item)
&&def addBarcode(barcode: String) {&&&&inventory.itemForBarcode(barcode).foreach(item =& {&&&&&&items :+ item&&&&&&listener.itemAdded(item)&&&&})&&}&
代码中对 Option[Item] 进行了迭代访问,与迭代一个集合的做法完全一样,这么做的优雅之处在于,如果 Option[Item]是 None,迭代中的逻辑不会被执行,也不会有任何副作用,与我们迭代一个空的集合一样。当然,如果 Option[Item]是 Some[Item],则 item 会被取出来并执行相应的逻辑。因此我们可以简化地把 Option 认为是一个包含1个或者0个元素的集合类。
事实上 Scala 的 Library 中大量使用了 Option,例如,由于 Scala 中的 Map 实际上有方法返回 Option,因此 Inventory.itemForBarcode() 可以简化成:
def itemForBarcode(barcode: String): Option[Item] = {
items.get(barcode)
&&def itemForBarcode(barcode: String): Option[Item] = {&&&&items.get(barcode)&&}&
现在,*Inventory.itemForBarcode()* 接口的协议是:我会返回一个 Option[Item],您自己去迭代里面的内容。有没有可能把接口简化下?让用户(这里的 Sale )不必关心迭代呢?让接口的协议变成:我会去找 Item,找到的话帮你执行逻辑 blabla ……
高阶函数( Lambda 表达式)
要回答上面的问题,我们得看一下另一个 Scala 的函数式特性,那就是高阶函数,如果是 Java 8 ,那就是 Lambda 表达式,我们可以这样定义 Inventory.itemForBarcode(),它接收一个 barcode 和一个接收 Item 的函数 f :
def itemForBarcode(barcode: String)(f: Item =& Unit) {
items.get(barcode).foreach(f)
&&def itemForBarcode(barcode: String)(f: Item =& Unit) {&&&&items.get(barcode).foreach(f)&&}&
如果能找到 item ,f 就会执行,现在 Sale.addBarcode() 变成了:
def addBarcode(barcode: String) {
inventory.itemForBarcode(barcode) { item =&
items :+ item
listener.itemAdded(item)
&&def addBarcode(barcode: String) {&&&&inventory.itemForBarcode(barcode) { item =&&&&&&&items :+ item&&&&&&listener.itemAdded(item)&&&&}&&}&
面向对象设计中有一条著名的原则:,《Smalltalk by Example》一书的作者这样描述该法则:
Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.
从 Inventory 获取数据( item 也好,null 也好,Option[Item] 也好),然后根据其内容是否存在再做操作,更多是过程式思想;相对的,扔给 Inventory 一个 barcode 和一段函数则是所谓的 Tell :去查一查,查到就干这个!
上述的代码用 Java 8 Lambda 表达式也能轻松实现,但如果是 Java 6/7 ,就比较麻烦了,你得搞一个 interface,然后传一个匿名内部类,非常麻烦。
整个过程我接触了 null, 异常, Null Object Pattern, Option, 高阶函数等概念,这里有过程式编程范式、面向对象编程范式、函数式编程范式,这是最让我惊异的,各种编程范式对于错误处理有着不同的方式,你会发现 Java 作为一门面向对象语言其实糅合了不少过程式的处理方式,而 Scala 或者 Java 8 则有函数式的处理方式,而且总体上来说函数式的处理方式更加优雅。
原创文章,转载请注明出处, 本文地址:
Categories:
有一点 Ruby 经验的朋友都知道 Ruby 的元编程威力十分强大,Rails 中的 ActiveRecord 就是一个极佳的例子, 对 Ruby 对元编程原理有着非常深入的阐述。此外, 中也有一段介绍 Ruby 元编程的实例代码,主要是用到了 method_missing:
class Roman
def self.method_missing name, *args
roman = name.to_s
roman.gsub!("IV", "IIII")
roman.gsub!("IX", "VIIII")
roman.gsub!("XL", "XXXX")
roman.gsub!("XC", "LXXXX")
(roman.count("I") +
?roman.count("V") * 5 +
roman.count("X") * 10 +
roman.count("L") * 50 +
roman.count("C") * 100
puts Roman.X
puts Roman.XC
puts Roman.XII
123456789101112131415161718
class Roman&&def self.method_missing name, *args&&&&roman = name.to_s &&&&roman.gsub!("IV", "IIII") &&&&roman.gsub!("IX", "VIIII") &&&&roman.gsub!("XL", "XXXX") &&&&roman.gsub!("XC", "LXXXX")&&&&(roman.count("I") +&&&& ?roman.count("V") * 5 +&&&& roman.count("X") * 10 +&&&& roman.count("L") * 50 +&&&& roman.count("C") * 100&&end end&puts Roman.Xputs Roman.XCputs Roman.XII
这段代码简单实现了一道非常著名的
练习题,要实现的逻辑是把 I, II, IX 之类的罗马数字转换成阿拉伯数字,规则可参考
解释,不过这不是本文的重点,这段代码中最有意思的是这么一行:
def self.method_missing name, *args
def self.method_missing name, *args
Ruby 支持定义一个 method_missing 方法,它带有一个名字和多个可选参数,当 RVM 执行 Ruby 代码发现类 Roman 不存在 XC 方法的时候,就会去看是否有 method_missing 实现,这该例中实现了,就继续执行下去,相当于 Roman.XC 是动态生成的一个方法,可以近似地说,我们写的代码会在运行时生成代码并执行,这就是元编程。就像生成罗马数字的阿拉伯数字一样,传统的编程方式,要实现的方法数量几乎是无限的,而元编程能让我们动态的生成方法,适当使用,非常优雅。
Ruby 是动态类型的语言,因此能做到上述功能一点不奇怪,而 Scala 是静态类型的,要让他动态地生成方法似乎不太容易,不过从 Scala 2.10 引入的 Dynamic 毫无困难地做到了与 method_missing 几乎一致的功能,下面是 Roman 的 Scala 版:
import scala.language.dynamics
object Roman extends Dynamic {
def selectDynamic(name: String): Int = {
val s = name.replaceAll("IV", "IIII")
.replaceAll("IX", "VIIII")
.replaceAll("XL", "XXXX")
.replaceAll("XC", "LXXXX")
s.count(_ == 'I') +
s.count(_ == 'V') * 5 +
s.count(_ == 'X') * 10 +
s.count(_ == 'L') * 50 +
s.count(_ == 'C') * 100
def main(args: Array[String]) {
println(Roman.VI)
println(Roman.X)
println(Roman.XII)
println(Roman.XV)
123456789101112131415161718192021
import scala.language.dynamics&object Roman extends Dynamic {&&def selectDynamic(name: String): Int = {&&&&val s = name.replaceAll("IV", "IIII")&&&&&&.replaceAll("IX", "VIIII")&&&&&&.replaceAll("XL", "XXXX")&&&&&&.replaceAll("XC", "LXXXX")&&&&s.count(_ == 'I') +&&&&&&s.count(_ == 'V') * 5 +&&&&&&s.count(_ == 'X') * 10 +&&&&&&s.count(_ == 'L') * 50 +&&&&&&s.count(_ == 'C') * 100&&}&&def main(args: Array[String]) {&&&&println(Roman.VI)&& //&&6&&&&println(Roman.X)&&&&// 10&&&&println(Roman.XII)&&// 12&&&&println(Roman.XV)&& // 90&&}}
上述代码通过 import scala.language.dynamics 开启 Dynamic 功能,然后让需要实现动态方法的 Roman 继承 Dynamic trait 以获得 Dynamic 特性,最关键的是这一行:
def selectDynamic(name: String): Int
def selectDynamic(name: String): Int
它接受一个 String 参数并返回 Int,这正是调用的时候代码 Roman.VI 所期望的,对比 Ruby 一个 method_missing 方法,Scala 共提供了四个方法:
- applyDynamic
- applyDynamicNamed
- selectDynamic
- updateDynamic
它们各自对应的用法可以参考 。
Scala Dynamic 和 Ruby method_missing 看起来差不多,不同的是 Ruby 少了一点点代码,而 Scala 多了些编译器检查。对于喜爱 Scala 或者在工作中需要编写 Scala 的码农来说,多了一个本来只有在 Ruby 那里才听到的特性,那是再好的消息不过了。
原创文章,转载请注明出处, 本文地址:
Categories:

我要回帖

更多关于 为什么要用zookeeper 的文章

 

随机推荐