首先假定一个场景:我需要定期抓取亚马逊某商品的价格,并通知自己。传统的实现方式就是写一个爬虫,然后在服务器上cron一下就好了。虽然能使,但是一旦考虑到灵活性(商品应该可定制,通知手段应该灵活)、高可用(服务器挂了也不应该影响到业务)、可视化(提供易于使用的界面,以看当前和历史的执行情况)、可监控(看当前的执行情况、出错了需要通知开发者)等,那就费时费力了。好在AWS给我们提供了一系列的服务,允许我们像Linux的管道那样把服务简单、灵活地拼接起来,从而实现需求。
首先简单介绍一下在这个工作流中我们会涉及到的AWS服务:
Step Functions:用于可视化管理工作流。是本文的核心。
SNS:通知服务,只要给主题(Topic)发消息,主题的订阅者(Subscription)就会通过订阅的渠道(如邮件、短信等)收到消息。
Lambda:无需服务器即可运行代码的计算服务,免去了管理服务器的烦恼。只有在程序运行的时候才收费。
CloudWatch:监控或触发AWS资源的服务。在本文中我们姑且把它当做一个cron服务。
对于本文的需求来说,最简单明了的工作流即是下图:
非常直观,一眼就能看出来它会先抓取亚马逊的价格,然后通知自己。对于Step Functions来说,这张流程图的代码很简洁:
上面的CrawAmazonPrice
指定了Resource
是一个名为CrawAmazonPrice
的AWS Lambda函数,而NotifyMe
则指定了一个名为PriceDown
的SNS主题。对这个主题感兴趣的用户(比如说,我)可以用期待的方式(邮件、短信等)订阅它。所以这个流程就是:开始->CrawAmazonPrice(Lambda)->NotifyMe(SNS)->结束。
为了简便起见,我们可以直接通过AWS界面实现CrawAmazonPrice
的Lambda函数。直接新建一个Python 3.7的脚本即可:
SNS就更简单了,创建一个名为PriceDown
的新主题,然后为它创建一个协议为Email
的订阅,填入自己的邮箱地址,便会收到AWS给这个邮箱发送的确认订阅邮件。点击邮件中的链接,即可完成订阅。要是日后有其他人对这个主题也感兴趣,增加一个订阅即可。
剩下的事情就是创建一个CloudWatch,来定期触发这个Step Functions。直接通过AWS界面创建一个规则(Rule),固定频率为每天,将目标设置为上面的Step Function工作流,取个名字如DailyLookUpPrice
就可以啦!
每次当CloudWatch被触发时,都会在Step Functions中留下自己的足迹。
由上图可以看到,每个状态的进入和退出都清晰可见,非常方便。
假如我们只想在价格低的时候通知自己,除了修改Lambda中定义的代码以外,还可以增加一个Task,以便增强灵活性。例如,公司内部有一个最低价格的服务,但是无法被公司外部(如AWS)调用到。即便如此,Step Functions也可以支持这种应用场景。在任何可以调用公司服务的地方写一段代码,这段代码作为一个活动(Activity)来轮询Step Functions,当执行到该Task时,该代码就被运行,调用公司内部的服务。AWS的实现也很简单,首先新增一个名为EnsureLowestPrice
的活动,然后在Step Function的JSON中增加一个Task,并修改CrawAmazonPrice
,使其Next
指向EnsureLowestPrice
:
这回的Task就不是Lambda啦,而是自己运行在随意机器上的代码。以Java为例,可以参考AWS官方文档来实现。
“价格低于最低价”的服务,在这里只是一个表示内部服务的示例罢了。当然,如果真要实现一个类似的服务,用AWS的DynamoDB甚至S3可以很方便地实现。
假如需要并行查询多种商品价格,Step Functions也能轻易支持:
并行的代码如下:
由于Branches
及States
的灵活性,再复杂的工作流也不在话下。
如果代码出现死循环之类的问题,可能会导致工作流无法继续流动下去。这时可以通过给该状态设置超时TimeoutSeconds
以使之到时退出。合理地设置这个值需要考虑某个状态可能需要运行的时间、是否有人工步骤等。默认的TimeoutSeconds
为99999999。
如果程序可能运行数个小时,也许你难以知道现在的状态是运行中,还是程序挂掉了。这时可以通过设置心跳HeartbeatSeconds
来使Step Functions知道当前的运行状况,以便及时把挂掉了(没有心跳了)的任务分发给其它节点。当然为了这个功能,程序中需要增加相对应的逻辑(定期发送心跳)。
有时候程序出错,可能只需要重试一下就好了。这时我们可以使用Step Functions提供的重试Retry
机制。如以下程序所示:
Retry
是一个集合,所以可以为不同的错误定义不同的重试机制。
如果重试还是不行,那还有一个招式就是异常处理机制Catch
。它与重试类似,可以为不同的错误定义不同的状态流向:
在上面的代码里,只要出错了(并且重试也没有成功),就跳转到NotifyError
的状态,可以通过SNS通知订阅者了。
在异常处理中由于跳转到了NotifyError
,并且通知成功,反而倒让这个工作流从异常变成正常了。想让工作流失败,只需在NotifyError
的后面接一步简单的FailExecution
状态就可以了。
最求完美,永无止境。例如,这些服务我们现在都是在AWS界面上点来点去的,其实这些人工操作可以通过CloudFormation
来变成自动化的脚本。如此,便可以实现我们的基础设施即代码,做到一键部署了。另外,随着需求的演化,如果要更新Step Functions的工作流,还可能需要考虑到对其进行版本管理。还有,当处理活动的节点数较多时,如果能够把某次执行的机器名输出到工作流中,也有助于错误排查。
软件开发和制造业在一定程度上是有相似性的。只不过制造业的历史更悠久、经历更丰富,它的革命总是更早地发生,这就足够让我们有所参照了。
传统制造业的原始阶段呢,最初都是以个体或家庭为单位,把整个产品都制造出来,比如毛皮,斧子等,没有统一标准,但是凑活都能用。哪一天不保暖或者不锋利了,那就再缝一缝或者磨一磨。软件开发的原始阶段呢,一开始都是单枪匹马,搞出来一个程序,凑活就能用。哪天出了个新情况,那就在原来的基础上改巴改巴,争取无需大改就能对付过去。
过了这个原始阶段,随着需求的演进,我们需要的产品更加地精细了。传统的制造方式难以为继,分工合作成为主流。每个人并不都会制造一个大整体,那也不现实;但是可以制造一个个符合标准的零件,最终将它们拼接成一个复杂的整体。而软件开发呢,也发展到了另一个阶段:一个人或一个小团队已经难以对复杂的单体系统进行开发维护的工作了,那就需要引入模块化或者是微服务化,降低各个组件的复杂度,以便可以更容易地让人掌握。市场上也有许多符合标准的开源及收费的包或服务,可在软件开发过程中使用。从这个角度上说,其实程序员们也都是流水线上的工人,只不过制造的是软件罢了。但是注意,传统的开发方式并没有消亡,因为还是会源源不断地冒出需求,人家就要一个斧子,干嘛组件化那么麻烦?能用就行了。就像现在还有许多的个体软件开发者,各自拥有一些框架,要建站?单枪匹马几百元就能搞定!
到了现在这个阶段,制造业迎来的就是机器人时代。2011年,富士康就提出了十年内的“百万机器人计划”,计划投入百万台机器人到生产线上,以取代部分工人,解决用工荒的问题。而现在的软件业呢,也喜迎人工智能时代,各种AI加入软件,不跟它沾点儿边还真不好意思跟别的程序员打招呼。机器人能把高效地把最容易重复的部分完成,也能在高危环境中大展身手。人工智能呢,现阶段也是把容易重复或是根据经验估计的部分逐步取代。有时候软件开发人员也会问问自己,未来我会不会被AI所取代?
不可否认,未来机器人会取代相当一部分人类。但与此同时,机器人也解放了人类,可以让他们创造新的工作类型。无法胜任的人类,会被残酷地淘汰掉,也许会演变为暴乱、战争……本文就不讨论这些了。而AI也将解放出许多软件工程师,这事是注定的,虽然那一天的到来还比较遥远。而那些专业知识不精、无法持续学习的程序员们,也将被AI的洪流无情碾轧。那未来的程序员们如何与AI共处呢?
现在的机器人时代,一些生产线的机器人并非是“替代”人类生产,而是“协助”。在软件开发中,写个小工具或利用现有的工具来辅助自己开发是最正常不过的事情了,跟AI还扯不上关系。但与此同时,AI辅助程序员编程,也已经开始萌芽——Kite了解一下?
现在的教育平台,就如雨后春笋般地冒出来。孩子就读的小学,各科的老师们已经推荐了不下五款的app用来辅助学习。这些教育平台会降低老师的重要性吗?并不。事实上老师们正在用这些app提高教学效率。这正是一个典型的双赢合作啊。未来的软件工程师也可以轻松地在更加智能的AI的配合下,完成可视化的设计、模板代码、用户手册等。
在制造业中,工厂里的机器人取代了劳动力,但是也产生了相当多的数据,需要许多人来分析和监控这些数据,还需要许多人来维护这些机器人。这是由于机器人存在而被人们创造出来的新岗位啊,尽管新岗位的数量远远小于原先的岗位数量。未来的AI可以使用多种方法来迅速实现各种需求,而软件工程师们可能更多负责评审、验证、监控这些程序的运行,当然还要编写、改进那些真正干活儿的AI,就像维护机器人一样。虽然说当人工智能强大到可以自我进化的时候,也许就不需要人类了,可是离那一天还早着呢。
“合并”在这里的语境看起来有点儿吓人。现在的制造业中,已经出现了一些外骨骼机器人,在军事上也有一些可佩戴单兵装备还在研发中。而未来更可能会出现半机器人时代,也就是说,你身体的任何一个部位,都可以被机器替换,从而获得更快的奔跑速度、更大的力量、增强的感官等等。当你的大脑中的某个部分被AI替换时,也许你会拥有永不遗忘的记忆,极快的计算速度,各种知识信手拈来……这时候的软件工程师,也许舒舒服服地躺在海边,随便想一想,便能迅速获得一堆代码及所需的环境,甚至AI已经帮你自动测试完毕了。我们所剩下的,也许就只有思考了。
想想《黑客帝国》……我是一个乐观派,这么黑暗的话题还是不要展开了。未来也许是:与AI斗,其乐无穷?
]]>Java 10引进了var
关键字来指代任意类型,让它朝C#又迈进了一步。以下是两个例子:
var
并不仅仅能类型推断,它还能完成以前做不到的事——引用匿名类的变量:
在所有可能的地方都用上var
也许并不是一个好实践。一种做法是:如果后面的表达式一眼就能看出来是啥类型,我们就用var
;否则,就还是老老实实地写声明,一眼扫过就能明白的代码更具可读性。
更详细的用法请参考Oracle官方文档。Java 11更是允许对lambda的参数使用var
。
一直以来我们都是老老实实地用以下这些方法来初始化一个已知的不可变集合:
可变集合:
对于不可变集合,Java 9引入了集合的工厂方法of
,这回终于可以用上原装的了:
值得注意的是,List
不允许通过of
传入null
,Map
不允许传入相同的键。
Java 10引入了copyOf
方法,也能方便地从可变集合中创建出不可变集合:
Java 10还在Collectors
类中增加了toUnmodifiableList
/toUnmodifiableMap
/toUnmodifiableSet
方法,可以直接从Stream
创建一个不可变集合:
这个相对简单,就是给@Deprecated
注解增加了两个字段:
前者表示从哪个版本起不建议使用,后者表示未来是否会将其删除。
从Java 8起,允许给接口增加default
方法。从Java 9起,接口又增加了一项能力:可以定义private
方法了。示例如下:
既然如此,它跟抽象类还有什么区别吗?接口的优势就是允许子类实现多个接口,而抽象类因为可以拥有可变字段(接口的字段是final
的)而更为强大。我们在接口定义方法时,也应当让其尽可能简洁。
从Java 9起,Optional
终于可以通过stream()
方法返回一个Stream
了,这样它就可以用上Stream
提供的许多API了。原来只能这么做:
另一个可以耍耍的方法是ifPresentOrElse
:
Java 9还新增了一个方法or
,与原来的orElse
类似,但是返回的是一个Optional
:
Java 11引进了HttpClient,http请求变得简单了:
异步也很简单,返回一个CompletableFuture
:
原生的HttpURLConnection
可以抛弃了。Apache的HttpClient
也许也可以雪藏了。
上一小节中,我们可以得到一个CompletableFuture
。从Java 9起,它可以设置过期时间了。可以把上面的futureResponse.get()
替换如下:
如果没有在设置的时间内获得结果,便会抛出java.util.concurrent.ExecutionException: java.util.concurrent.TimeoutException
。如果想在过期时不抛异常而是设一个默认值,可以这样做:
Process API是元老级的API了,但是功能一直不够完整,无法获取进程的PID、用户、命令等。Java 9引入了ProcessHandle
,可以查询进程,甚至允许在进程退出时执行方法。它提供了获取当前进程的current
方法,以及获取全部进程的allProcesses
方法。用法如下:
如果在Mac中打开了一个TextEdit
,便可以看到类似这样的输出结果:1234 Optional[/Applications/TextEdit.app/Contents/MacOS/TextEdit]。Windows的话可以打开notepad.exe
,也能看到:1234 Optional[C:\Windows\System32\notepad.exe]。可以用以下方法在该进程退出时打印一些信息:
甚至还能用optionalProcessHandle.get().destroy()
来摧毁进程。如此这般,Java外部打开的TextEdit
或notepad.exe
都会被退出。由于其它用户的进程无法使用ProcessHandle.of
来获取,所以只能杀掉自己的进程。对安全方面感兴趣的话可以参考一下官方文档。
官网上放出来的示例当然是最佳了:
能有这样的效果那是相当的不错呀,布局、文字、图像都识别得很不错,但也并非完美:
那我自己画一幅,用网站提供的TAKE A PICTURE功能上传试试:
这是什么鬼?敢情什么都没识别出来?是我的画风太清奇了吗?不死心,同一幅图再拍一次:
这次有限地成功了,看来位置、方向、大小、光影上的一点点细微差别,也会很大地影响最终的结果。后来又试了几次,最完美的识别也就是上面这幅图了,要我是草图设计师,用这玩意儿还是挺跟它较劲的。倒是处理速度还挺快,几秒之内就能搞定。
接下来试一试中文:
果然是不支持中文呀。从下图中我们能看到,尽管中文都能够识别成Lable,但确实对中文的OCR无能为力。
再试试横线本,看来背景的横线是可以被过滤掉的:
最后试着参考GitHub的注册首页画一幅手稿:
上面的html是试过几遍之后最好的解析了。生成的html代码中引用了bootstrap的css,所以网页看上去还比较顺眼。但是格式有点乱,有一些连续空行、缩进等问题。不过这些问题相信都很容易解决。最大的问题是目前的实用程度还不太高。想要让前端变凉,数据量应该还不太够(大家多多上传草图吧),而且还应该有不少工作能做。希望它能早日实用化,让前端们投入到更有意义的工作中去。
最终结论:未来很美好,但是现在的实用性还有待提高。
]]>随着IT的不断发展,软件能解决的问题越来越多也越来越大。在大型软件开发中,人们发现要与一大坨代码共舞变得越来越困难,于是微服务架构兴起并持续火热,许多单体系统也纷纷挤上风口随之演进,不管是新服务单提出去还是老服务逐步拆分,总之他们都渐渐走到分布式系统的大道上了。服务的数量上去了,便带来了一个问题:你的服务跟别人的服务应该如何集成呢?最简单粗暴直截了当的做法就是直接调用。但是这样缺少定制化支持。对方有什么,你可能就只能凑合着用什么。要不,就得等对方把你的需求排上档期。
总这样不行呀,缺胳膊少腿的服务和只能傻傻等待着跟不上节奏的服务没法儿工作呀。为了满足不同客户的多方面需求,服务的平台化就成了一个自然而然的选择。作为平台方,不止提供服务,而且提供客户在平台上的定制化功能,客户成为了平台的租户。一般来说,租户需要在平台那边写点儿为自己的需求而定制的代码或是DSL,以便支持自己的业务场景。
如果你是一个平台的所有者,如何才能让别人在你的平台上顺利安营扎寨,而你无需提供太多的支持工作呢?
首先要问自己一下,你的平台,也就是你拥有的服务,面向的客户是什么人呢?从技术维度上来说,无非是这几类:
出于简单起见,我们先考虑前两种类型。实际情况是,第一类其实就是许多第二类的聚合。你可能有许多微服务,但是作为网关的微服务其实数量不多。这样,我们便把要解决的大部分平台问题归类于对技术人员开放的服务了。第三类的混合型也很类似。其实对于微服务来说,我根本不在乎对方的实现方式、架构方式,能提供稳定的服务就可以了。但是,一旦它是个平台,并且我需要在其上定制代码,那我也就不得不关心一下了。还是先分两个类:
如果一个租户的代码挂掉了,从而影响到其他的租户甚至是你的平台,那这个平台就是很有问题的。这句话看起来理所当然,但确实有不少所谓的平台并不是这样设计的。在这种情况下,租户要往这个平台提交代码,就需要接受平台的代码审查,以免待提交的代码影响到其他人。而平台方就不得不耗费人力审查代码、部署代码,当租户的数量多起来时,平台根本就无法提供足够的可伸缩性,势必需要排优先级。于是各种抱怨就纷至沓来,自己的技术人员也苦不堪言。所以,我们希望租户拥有对自己代码的审查和部署的所有权,不希望当租户代码变化时需要平台人员人力审核。
如果你需要开发一个平台,如何能够做到让租户之间不相互影响呢?最佳的租户隔离方式是向租户提供服务级别的隔离。一个租户服务的倒掉不至于影响到其他租户的服务,只是影响已损坏服务的分支业务自己而已(自己的代码挂掉,当然自己承担喽)。如果由于某些原因无法为所有租户都提供服务级别的隔离,那么也可以尝试进程级别的隔离,例如Docker。一个损坏的进程在许多情况下都不太至于影响到平台为其他租户进程提供的服务。如果很不幸的是不得不与租户在同一个进程里,那么起码还可以用DSL来限制租户的能力,让租户的代码不能有太大的破坏性。要是租户也能用平台的语言,还在同一个进程中,那么搞砸平台的服务只是时间问题罢了。
在DevOps的光茫照耀下,我们当然要提倡自动化,这是平台和租户双方的责任。平台提供的是从代码提交到部署测试环境乃至生产环境的自动化能力,而租户需要实现的是自动化测试。一个典型的应用场景就是:平台上的租户代码提交,触发了该租户的持续交付流水线,于是在测试环境上自动部署平台的租户代码,接下来运行租户自己实现的自动化测试,一旦通过,租户便可以决定是否要在平台的生产环境上自动部署这次提交的代码。
最后,作为平台,提供的是能力。不要强迫租户使用你的能力,而要提供稳定的服务,让租户心甘情愿地使用你的能力。向Unix那样,把一个个稳定的服务组合起来,以提供整体的服务;同时,允许租户自由替换其中的某些服务以实现灵活性。能做到这一点的平台就相当成熟了。
]]>如果你需要实时比较生产环境的处理结果和备份环境的处理结果,或是在新系统中重放生产环境的请求,或者像代码一样对对象进行版本管理,那么JaVers就可以成为你的好朋友。它不仅可以比较对象,也可以将比较结果存储在数据库中以便审计。审计是这样的一种需求:
作为用户,我希望知道谁改变了状态,
是什么时候改变的,以及原先的状态是什么。
本文仅关注于比较部分,对审计部分就不具体展开了。
新建Maven工程,往pom.xml中增加dependency,最后的pom.xml看起来就像这样:
假设我们有一个名为Staff
的POJO如下:
在main
函数中如下编写:
即可得到以下输出结果:
大部分的代码都是我们创建对象所用的,可见JaVers非常易于使用。
根据DDD,JaVers把要比较的对象分为三种类型:实体(Entity)、值对象(Value Object)和值(Value)。每种类型都有不同的比较方法。值最简单,就是看它们是否是Object.equals()
的。实体和值对象都是按属性依次比较。它们俩的区别是实体拥有标识(Entity Id),而值对象并没有。标识不同的实体就会被认为是不同的对象,从而不再继续比较其余的字段。从DDD的角度上严格来说值对象不能单独存在,需要依附于实体。好在JaVers并不教条,值对象也可以用来单独比较。上面的代码其实就是把Staff对象当作值对象来比较。如果我们在Staff
类中,给name
添加一个@Id
的注解(所有的注解都在org.javers.core.metamodel.annotation
包里),那么比较结果就会不同:
只有当name
属性相同的时候,这两个对象才会被当成同一实体,从而依次比较其余属性。如果没有权限修改实体以增加@Id
注解,也可以用这样的方法来注册,效果相同:
如果registerEntity
的属性和@Id
注解都存在,那么以registerEntity
所注册的属性为准。
JaVers完全兼容Groovy,可以参考其文档来了解用例。
从业务上说,有些属性新、旧系统本来就不一样,或者是不关心,这时候我们可以在比较中“忽略”这些属性。如果有权限修改要比较的对象类,可以简单地在属性上面增加一个@DiffIgnores
,比较的时候就会将其忽略。@DiffIgnores
相当于黑名单,@DiffInclude
起到了白名单的效果。如果没有修改类的权限,那么也可以用这样的方法来注册,效果相同:
这里我们注册的是个值对象ValueObject,与上一个例子的实体Entity的区别就是有没有标识属性。
一开始细心的读者们就可能注意到了,Lists.newArrayList("film", "game")
和Lists.newArrayList("game", "music", "travel")
的比较结果居然是:
这当然也是可以配置的:
这样的话,结果就变成了:
值得注意的是,这种算法在列表元素过多的时候可能会影响性能。
我们可以注册自定义的比较器,例如,如果在业务上认为两个Double
类型的1.000000001
和1
相等,这时候我们可以注册一个如下的类:
GlobalId
是当前比较对象的标识,如值对象的org.ggg.javers.Staff/
或是以name为标识的实体的org.ggg.javers.Staff/ggg
。Property
是当前比较的属性。可以通过这两个值来设置比较属性的黑名单或是白名单。然后注册进JaVers就好了:
需要注意的是,Double
与double
是不同的,如果Staff
中的height
是double
类型,那么需要在调用registerCustomComparator
时传入double.class
。自定义的比较在许多场合都比较有用,比如String类型的不同格式的日期等。
关联字段就是说,如果几个字段之间有关联,我们就认为它们一样。比如说我们虚构一个需求:对于一个拥有int x
和int y
的Rectangle
类来说,如果x * y
也就是面积相等,我们就认为它们相等。在这种情况下,JaVers似乎并没有原生提供关联字段比较的办法。有一种办法是新建一个包装类,比如说RectangleWrapper
,里面有一个Rectangle rectangle
和一个int area
字段,分别赋值为要比较的对象和其x * y
。注册Javers
的时候,把Rectangle
类的x
和y
忽略即可。如果有更复杂的需求,例如当面积不同时需要报x
和y
不同而不是area
不同,那也可以通过生成多个Javers
对象,并多次调用来解决。Javers#compare
方法返回的是一个Diff
对象,从中可以很方便地查看某些字段是否存在变化。就是要多写点代码咯。
Working backwards原本的意思是,从最终要解决的事情开始,每次回退一步,直到最初。
在亚马逊,这个概念变得更加具体,成为了如何实现“以客户为中心”的一系列过程。它的产出是三份材料:
这三份材料在产品的生命周期中,随着一次次的迭代而不断地演进。在准备文档一节中,我们会更详细地介绍它们。
为什么要使用逆向工作法?简单来说,因为要“以客户为中心”。逆向工作法是一种行之有效的方法,能够把重点放在客户真正关心的问题上,进而交付用户体验极佳的产品。它是亚马逊成功的关键。
逆向工作法的最终目标就是取悦用户。没错,取悦用户能够带来巨大的价值。对亚马逊来说,它力争成为地球上最以客户为中心的公司,与逆向工作法天然契合。如果你的愿景并非如此,那也不妨看看人家是怎么做的。不过你也需要知道,逆向工作法需要很大的工作量,但是它号称可以节省更多的未来工作量。
一开始其实跟头脑风暴法(Brainstorming)很像,在打破时间、预算、资源(含技术)等限制的情况下,尽可能发散地提出各种想法。比如Kindle的首要设计理念,就是让用户感觉不到它的存在,从而能够专心读书。它连音乐播放器都没有!之后,亚马逊花了三年时间打造无线和电子屏技术,才推出了这一革命性产品。
你知道你的用户究竟是什么人吗?有一个很典型的答案:所有人都是我的潜在用户。这话倒是没错,但是这个答案对深入了解用户并没有太大的帮助。考虑一些特定用户的情况,如上班族、网络不好的用户、残疾人等等。思考其面对的问题,他们想要的是什么?问问自己,时间、地点和用户的现状会怎样影响其需求?让最终用户受益,是让参与方各个团队通力合作的基础。以后的团队合作开发中,如果大家有分歧,这正是一个大家达成一致的关键。
如果不知道用户要的究竟是什么,那你很可能就是在浪费时间。用户想要的可能是需求,也可能是方案,一定要搞清楚。这两者之间的区别可能非常小。如果你不知道这到底是需求还是方案,那就问问自己:为什么用户需要它?这个问题的答案有可能就是需求。汽车大王福特有一句名言:“如果我当年去问顾客他们想要什么,他们肯定会告诉我:一匹更快的马。这就是典型的方案,而不是需求。如果不考虑用户的需求而直接转向方案,那很可能就会痛失一个改善用户体验的机会。
“If I had asked people what they wanted, they would have said faster horses.” ― Henry Ford
需求有时会表现为痛点。例如,用户说他总是不知道中午应该上哪儿吃饭去。那么用户的需求是什么?一个帮他做决定的人?点评网站?外卖app?还是一个骰子?这些都是方案,用户的需求其实只是希望让决定中午上哪儿吃饭这件事变得非常简单自然。
如果你能回答关于用户的这五个问题,那这一步就差不多了:
虽然这里说的新闻稿是内部人员写的内部文档,但是看起来应该跟即将公开发布的新闻稿差不多。市场人员几乎无需修改便可以直接使用。想象用户正在看这篇新闻稿的心理活动,避免使用行业术语,因为用户很可能并不了解这些术语。
写好新闻稿需要花时间,也需要重复地练习。多向别人要反馈吧!今天的一点点改进可能会节省未来花在上面的大量时间。一份好的新闻稿描述了你的想法是什么,以及为什么要这样做。一般一页纸就够了,不要长篇大论。避免模棱两可的词(weasel words),如一些,许多,经常,可能,显然,一般,等等等等。用数据来代替它们吧。
这里有一些关键内容:
请注意,所有的文档都不是固定不变的,它们是一直演进的。把最重要的内容放在第一段。保证其他人哪怕只看了第一段也能对你的想法有所了解。想一想电梯游说(Elevator pitch),怎样在很短的时间内表达出你的想法?写文档像写代码一样,要尽量简单。
新闻稿描写了愿景,而FAQ提供更详细的说明。FAQ包含了两个部分:客户会问的问题以及内部参与方会问的问题。关于前者,你需要站在客户的角度上,思考其究竟会问出什么样的问题。例如,我怎么参与进去呢?有了问题我应该找谁呢?我要花钱吗?要像回答真正的客户一样来回答这些问题。关于后者,那就是你的老大、兄弟团队或是投资人应该会问的问题。例如,为什么能够提高用户体验呢?如何验证项目是否成功呢?为什么现在是推行的最佳时期?执行计划是什么?越是常见的问题,放在越上面。
别害怕尖锐的问题,都写上吧。比如说什么问题你最不希望被人问到?有没有你还没找到答案的问题?为什么要这样做,而不是那样做?别藏着掖着,都亮出来吧。这样别人才能更容易地看到这个想法的亮点和痛点。对于暂时还是无解的问题,那就诚实地写下:“我不知道。我还在通过XXX来寻找答案”。在寻求反馈的时候,其他人说不定就能够解决这个问题。但是如果不写,我们不但失去了解决它的机会,而且增加了未来的风险。
视觉资料包括但不限于:白板、分镜(Storyboard)、用户体验历程图(Customer Journey Map)、线框图(Wireframe)、高保真UI(High-Fidelity UI)、技术架构图(Technical Architecture Diagram)等等。分镜是一系列草图的组合,能够呈现端到端的用户体验。到了后期,还可以引入技术架构图等来描述一个复杂的系统。不用拘泥于形式,任何有助于表达的东西都是我们的工具。
视觉资料可以让我们对用户体验有一个直观的认识。可以的话,让设计师尽早参与吧。一开始手绘即可,不需要那么高保真。不然大家的讨论容易集中在页面风格上,而不是客户体验上。高保真图是随着迭代的更替和讨论的深入而慢慢成型的。选择保真度有一个小窍门:保真度应该跟你的想法成熟度相匹配。越不成型的想法应该采用的视觉资料就应该越低保真。草图是最简单有效的开始。别怕画得难看,它能帮你加深对自己想法的认识。记住,画得好看与否并不是关键,清晰地表达你的想法才是。
许多会议一开始都是先花个两三盏茶左右的时间来阅读新闻稿和FAQ。与更多取决于演讲者个人魅力的PPT不同,阅读可以让所有人都处在同一起跑线上。提供反馈时,最好直来直往,别拐弯抹角。如果你有些地方看不明白,尽管说出来。别因为自己跟对方的关系而束手束脚,我们对事不对人。另外在这个时候,我们更多地关注在想法本身,而较少对语法错误、错别字挑刺,这一点与代码审查略有不同。代码审查啥都挑,但要是低级错误太多是要打回去重写的。
我们先前说过,做好这些事情需要时间练习。如果你一时想不出什么点子,上文提过的中午上哪儿吃饭的问题应该怎么解决?亲爱的读者,你有什么想法吗?
]]>2018.1
社区版,快捷键是Mac OS X
。本文的兄弟篇是挖掘IntelliJ IDEA的实用功能。一般来说调试时,我们都是在代码行上鼠标一点,然后运行测试,遇断点所在的行即停,这就是所谓的行断点。IDEA支持以下几种断点类型:
断点是可以设置条件的,这样便可以只在关心的时候停下来。比如说循环里处理一堆字符串,但是只关心特定的字符串,那条件断点便可以派上用场。按住Shift键设置断点,或是右击断点之后选择More来打开以下界面:
上图就是设置条件断点的界面,直接在Condition里输入条件即可,如"ggg".equals(name)
。需要注意的是,Suspend默认是没有打勾的,必须勾选上才能让程序暂停。另外,辛辛苦苦设置的特定断点,是可以拖拽到别的地方去的,这样就省的到处敲来敲去的了。还有一个小技巧是按住Alt的同时设置断点,可以让断点仅停一次便自动消失。在设置临时断点时有点用。
如果在很长的循环时不知道程序运行到哪里了,可以在调试时点击调试窗口上的Pause Program,这样程序便能在当前执行的地方暂停。另外,运行到光标(Run to cursor)也可以在没有设置断点的时候让程序运行到光标所在行时暂停。
下面介绍一些调试的小技巧。
当调试程序运行到类似这样的句子时,如果你想看的是actor.action
方法,那么进入这个方法就相对麻烦一些。
这时可以使用调试窗口上的智能进入,程序会弹出一个对话框,我们选择需要的调用处即可。算是一个提升调试效率的小技巧。
官方文档传送门:https://www.jetbrains.com/help/idea/debugging-code.html#d181035e286
这应该是大部分人都知道的技巧了,可以通过表达式评估来重新赋值当前的变量,以便让程序运行到其它的分支去。当然也可以在Variables窗口中,右击想要改变的变量,选择Set Value。不过表达式评估里可以轻松增加新变量、动态import新类库等,功能更加强大。
官方文档传送门:https://www.jetbrains.com/help/idea/evaluating-expressions.html
如果运行的实例在其它机器(或者虚拟机、docker)上,只要实例设置了以下参数,就可以通过远程调试连接到8000
端口进行调试。
官方文档传送门:https://www.jetbrains.com/help/idea/debugging-code.html#d181035e408
对于IDEA来说,只需要在Run -> Edit Configuration里,增加一个Remote,设置主机Host和端口Port,然后调试它即可。
Visual Studio好的一点是调试时可以拖拽当前执行的位置,方便反复查看。虽然IDEA没有这样的功能,但是它可以使用弃栈帧来把当前调用栈的第一栈帧丢弃掉,相当于重新开始当前调试的方法。使用方法也算简单,在要丢弃的栈帧上右击,选择Drop Frame即可。或者直接单击调试窗口的Drop Frame按钮。不过需要注意的是,如果对象在子方法运行时发生了变化,是不会再变回去的。
官方文档传送门:https://www.jetbrains.com/help/idea/debugging-code.html#d181035e308
这是IDEA 2018年加入的新功能,可以直接在调试中抛出指定的异常。使用方法跟上面的弃栈帧类似,右击栈帧并选择Throw Exception,然后输入如下代码即可:
官方文档传送门:https://www.jetbrains.com/help/idea/altering-the-program-s-execution-flow.html#throw_exception
这是IDEA2015版时增加的功能,类似上面的手动抛异常,只不过是返回一个指定值罢了。使用方法跟上面也都类似,右击栈帧并选择Force Return,然后输入要返回的值即可。如果是void
的方法那就更简单了,连返回值都不用输。
官方文档传送门:https://www.jetbrains.com/help/idea/altering-the-program-s-execution-flow.html
利用Java虚拟机提供的HotSwap功能,我们可以做到一边调试一边改代码。只要在修改完代码之后,点击Run -> Reload Changed Classes即可。不过HotSwap有一些限制,例如不支持static的字段和方法等。
官方文档传送门:https://www.jetbrains.com/help/idea/altering-the-program-s-execution-flow.html#reload_classes
调试窗口里的Settings -> Show Method Return Values开关可以显示方法的返回值。例如以下方法:
只要在return
上设断点然后Step Over,或者在方法内部的任何地方设断点然后Step Out一下,便可以在调用处的变量窗口看到一个类似于这样的值:Test.random() = 0.28735657504865864
。在这个方法调用没有赋值给变量时(如if (random() < 10)
)还挺有用的。
前面说了Visual Studio的好,但是它调试时不能看lambda的值也真是挺恶心的,据说2015版以后开始支持有限的lambda了。IDEA在这方面就做的非常到位。Java 8带来的Stream里面到底是什么,有时候很难知道。通过IDEA提供的这个功能,我们可以很轻松地看到流在各个步骤之间的变化。如下图:
展平模式(Flat Mode)更是提供了全局的视角:
使用这个功能也非常简单,当程序在lambda表达式的任意处停下时,单击调试窗口的Trace Current Stream Chain按钮即可。
官方文档传送门:https://www.jetbrains.com/help/idea/analyze-java-stream-operations.html
内存泄漏是一个比较头疼的问题,好在IDEA提供了内存分析工具,只要单击调试窗口右上角的Restore ‘Memory’ View就能看到内存窗口,然后点击其中的Click to load the classes list就能看到当前内存的对象分布情况。然后可以据此分析到底是哪个类的对象数量看起来有问题。
官方文档传送门:https://www.jetbrains.com/help/idea/analyze-objects-in-the-jvm-heap.html
如果只是想暂停一下set或get方法,可以使用字段断点,只不过可能会在调试中报错:Source code does not match the bytecode,但它能够工作。
如果想设断点的是toString
、hashCode
等方法,可以在注解上设置断点,也可以在调试时使用:Refactor -> Delombok并选择相对应的注解,然后再使用上文介绍的HotSwap功能,就可以生成代码并按需调试了。最后别忘记把代码恢复回来。
调试异步、线程、死锁、活锁等高级功能,官网上面有详细的教程,可以在用到时参考。
官方文档传送门:https://www.jetbrains.com/help/idea/tutorial-java-debugging-deep-dive.html
功能熟悉了以后,熟练使用快捷键能够大幅提高效率。以下是笔者调试时经常使用的快捷键:
软件涉及到的领域太广,以至于程序员之间、程序之间很多时候难分伯仲。你可能也经常会听见一些牛逼的程序员们互相吵来吵去,很多时候并不是自己拥有一个完美的解决方案,而是觉得对方的解决方案在特定情况下不好。当然没有十全十美的方案。举个简单的例子,我们奉行的DRY(Don’t Repeat Yourself)原则,要求我们不要WET(Write Everything Twice或者We Enjoy Typing)。可即便是这么通用的原则,也可以被质疑:这样就失去了两边各自变化的能力了。听起来似乎有些道理,但是回头想想,两边各自变化的可能性有多大?这样的可能性在每个人的眼中是不一样的。因为大家的背景不一样,也许某人知道更多的未来需求,也许某人预见业务增长将会很快,也许某人曾经在这上面吃过亏……不一而足。
像DRY这样,但是更加令人难以决断的例子还有很多,列了一些常见的如下:
String name = someService.getName();
String result = otherService.getResult(name);
return result;
需要重构为:return otherService.getResult(someService.getName());
因为比较简洁嘛。我原来也倾向于使用这种风格。但是新团队的风格是尽量抽变量,理由是方便调试。比如一行中要是出错了呢?调试时要是想知道返回值呢?当然可以查看otherService.getResult(someService.getName())
,但是这个操作要是不幂等呢?似乎也有几分道理。对于永远稳定不会变化的需求(尽管很少,但这样的需求确实存在)而言,软件开发也许就能够分出高下来。举个例子:一个确定不会被重用的小工具。在这种情况下,可以适用的原则是:越快越好。我们甚至可以适当允许一些bug的存在,因为修复它们所需的时间可能大于手动修复运行结果所需的时间。另外,永远稳定不会变化的需求真的就永远不会变化吗?未必。但是在开发的某个时间点上,它确实是被认为是永远不会再变化的了。唯一不变的是变化本身。
曾经有同事去印度当了几个月的程序员讲师,回来后告诉我,在回答学员们的问题时,讲师们说得最多的就是这句话:“It depends.”。这基本上是一个放之四海而皆准的原则:具体情况具体分析。那是不是所有的问题都直接无脑地“具体情况具体分析”就完了?当然可以,但这是一种思想上的懒,不是我这“懒程序员”的“懒”。因为这句话对解决问题并不能有太多实质上的帮助嘛。关键是,我们还得就着“具体情况”来“分析”。所以,我们可以在其上再构建一些原则,来覆盖特定的情况。
比如说设计模式,它是对特定问题的特定解决方案。不要一股脑儿就往上套,好的经验是在发现坏味道以后,重构到设计模式,甚至是重构了一半,就已经消除掉坏味道了。原则:越简单越好。
比如说final,它并不能带来明显可观的价值,所以应该以大多数人的习惯为先。原则:贴近大多数人的习惯。
比如说lambda,明显它是更加先进的生产力,所以上面的原则就不适用了,应该以先进的生产力为先。原则:采用先进的生产力。
比如说代码覆盖率,高覆盖率自然是好,但是值得吗?比如Java可能就很难做到100%,但JS就能轻松一些。原则:采用性价比高的方案。
从“代码审查”中我们也可以看到,每个人都是不一样的,我在人际风格与有效沟通实战中也曾提到不同风格的人。原则:因人而异。
从“尽量内联”、“异常或返回值”中我们也可以看到,应该保持开放的心态来调整各原则。原则:原则需要与时俱进。
比如说要不要写注释,大多数情况下,组织得当的方法名、变量名已经能够说明问题了,这时的注释就显得多余。但偶尔还是需要介绍一块代码的来龙去脉,这时的注释就是必要的。原则:具体情况具体分析。
我们现在已经有了好几条原则了:
有些原则可能在特定的情况下是冲突的,需要自己思考究竟哪条原则更加符合现实情况。在适当的时候使用适当的原则(就像设计模式一样),而不是拿着锤子看见啥都像钉子(也像设计模式一样)。另外,需求是变化的,我们的原则也不必一成不变。有句话说“规则是用来打破的”。我想说的是,在充分理解规则之后,再来决定是不是打破它,并承担相应的后果。或者,考虑是不是用更高级的规则(如“具体情况具体分析”)来约束它,或是用更低级的规则来覆盖它吧。
]]>2017.2.6
社区版,快捷键是Mac OS X
。本文的兄弟篇是挖掘IntelliJ IDEA的调试功能。Sublime Text有一个非常好用的功能,就是可以选择多个光标,允许一起编辑。IDEA也向其学习,提供了类似的功能。只要按住Alt+Shift时,用鼠标点击其它位置即可。还可以通过Ctrl+G选择下一个相同的字符串,或是Ctrl+Command+G选择所有相同的字符串。如下图:
还有一个功能是纵向选择,可以通过Command+Shift+8来开关。之后的效果如下:
有一个注意事项就是,多重选择的时候不要用IDE自带的重构功能。
官方文档传送门:https://www.jetbrains.com/help/idea/editor-basics.html#editor_lines_code_blocks
比较两个项目中的文件很简单,选中这两个文件,然后Command+D就可以了。JAR文件、文件夹也能够进行比较:
如果只有一个文件在项目中,那就选中它,然后Command+D,再从对话框中打开项目外的文件即可。如果另一个文件在剪贴板,那就打开项目中的文件,然后右击编辑器选择Compare with Clipboard即可。
如果两个文件都不在项目中……那好歹复制一个进去呗。
官方文档传送门:https://www.jetbrains.com/help/idea/comparing-files.html
如果你写了个(或搜了个)炫酷的正则表达式,除了单元测试,IDEA也提供了简便的测试方式。比如对于如下代码:
在正则的字符串中按下Alt+Enter,选择Check RegExp,然后填入想校验的字符串即可:
官方文档传送门:https://www.jetbrains.com/help/idea/regular-expression-syntax-reference.html#tips-tricks
还在写这样的html吗?早就out了……
只要输入table#users>tr.user>td*3
然后按下TAB就行了。能自动生成的,我们就不自己写。不过这个功能只在后缀名为html
或xml
的文件编辑器中生效,所以创建一个html文件然后再试试吧。
这里有一张Emmet的语法表:https://docs.emmet.io/cheat-sheet/
官方文档传送门:https://www.jetbrains.com/help/idea/emmet.html
如果你看到这样的代码:
就会忍不住想把它变成这样的话:
只要在等号后面的语句中按下Alt+Enter,选择Replace ‘+’ with ‘StringBuilder.append()’即可。也可以选择Replace ‘+’ with ‘String.format()’,来把它变成这样:
所以当你准备拼接字符串了,考虑这个功能吧,比自己慢慢写要顺手多了。其实Alt+Enter这个万能快捷键在不同的代码下支持许多不同的功能,没事在代码上随便敲一敲,你会发现惊喜的。
通过VCS菜单中,Local History的Show History,可以打开当前文件的本地修改历史。如果某个版本你并没有提交过,只是在本地曾经改过,但是又改掉了,就可以利用这个功能将其找回。甚至还可以针对字段、方法、文件夹、乃至整个项目来查看。所以保存量是比较大的,IDEA默认就保留五个工作日的本地历史,有一周一般来说也就够了吧。这个值也能通过传给JVM的参数localHistory.daysToKeep
来修改。如果因为磁盘不够等原因不想要,也可以把它设置为0。
官方文档传送门:https://www.jetbrains.com/help/idea/local-history.html
通过Run菜单里的Run ‘xxxTest’ with Coverage,可以在运行测试时顺便跑出测试覆盖率。通过Analyze菜单里的Show Coverage Data,可以查看覆盖率的大致情况。而通过Generate Coverage Report,可以生成测试报告,在报告里可以看到具体的每一行是否运行过。这是覆盖率数据:
这是测试报告:
官方文档传送门:https://www.jetbrains.com/help/idea/viewing-code-coverage-results.html
点击Help菜单里的Productivity Guide,就能看到一张大表,记录着各功能的使用情况。注意一下使用频率低的,了解一下从未使用过的,很快就能成为Intellij IDEA的砖家了。
官方文档传送门:https://www.jetbrains.com/help/idea/productivity-guide.html
]]>废话不多说,在环境准备好的情况下,假设我们来测试驱动开发一个计算一天有多少个小时的API。参见以下的两分半小视频:
要是视频不清晰或看不到,就直接到腾讯视频中看720P吧。
如何才能做到”敲最少的键,编最多的码“呢?除了掌握技巧之外,就是多练习实践了。以下就是技巧的内容。
首先把环境准备一下。只要有src
和test
即可。我自己是一个默认的Maven新项目,在pom
中引用了junit
。
专业版的IDEA支持项目模板,如果你对默认的模板不满意,项目模板能够节省你的一部分操作。
test/java
中用快捷键Ctrl+N生成文件。JUnit
正是我事先创建好的文件模板,内容见下文的“功能简介”。这里的一个小诀窍是先按下u,可以过滤掉不需要的模板。HoursCalculatorTest
并回车,测试文件就此生成。should_get_24_hours_for_1_day
。new HoursCalculator()
。HoursCalculator
类还不存在,所以会报错,用快捷键F2移动到下一个错误处,再用快捷键Alt+Enter自动修复错误,选择Create class ‘HoursCalculator’。org.ggg
自动生成HoursCalculator
类。new HoursCalculator()
抽取为一个变量hoursCalculator
,Command+Shift+Enter结束本行,将光标跳至下一行开头。int hoursByDay = hoursCalculator.getHoursByDay(1)
来获取计算结果。小诀窍是只要输入hc
,IDEA就会提示hoursCalculator
。HoursCalculator
类中自动生成getHoursByDay
方法。days
。assertEquals(24,hoursByDay)
,还是Command+Shift+Enter结束本行(还会调整格式)。HoursCalculator
类准备修改实现。0
改为24
。test
是我事先创建好的活动模板,内容见下文的“功能简介”。should_get_48_hours_for_2_days
。之后按照类似上文的方式,实现并执行测试,红了。令方法返回24 * day
并再次执行测试,绿了。移动光标到测试方法之外,执行全部测试,都绿了,保证后一个实现不会破坏前一个实现。24
是一个magic number,所以我们要用Command+Alt+C将其变成一个常量,如hoursInDay
。不过常量应该还是大写的蛇式比较符合惯例,于是可以Shift+F6改名。HOURS_IN_DAY
。其实现在版本的IDEA已经会在快捷键改名时提示HOURS_IN_DAY
了,但是插件支持的功能更加丰富一些,并且也能在编辑非java文件时使用。org.ggg
中。还可以用Command+Alt+O来优化import部分。我用的是Mac OS X的Keymap。常用的快捷键要牢记,很多时候它决定了程序员的效率如何。JET BRAINS的各种语言的IDE快捷键都比较类似,花点精力记住它决不会吃亏。
顾名思义,文件模板即是新建文件时使用到的模板。我们在上面的步骤中使用的JUnit
活动模板如下:
它的语法基于Apache Velocity,支持变量,如${PACKAGE_NAME}
表示包名,${NAME}
表示用户输入的名称,等等。
可以通过在Preferences中搜索File and Code Templates,来创建或修改文件模板。也可以在一开始Ctrl+N时选择Edit File Templates…。
活动模板与文件模板类似,但它不需要新建文件,可以在文件的任何地方激活,只需要输入名字后加一个TAB即可。我们在上面的步骤中使用的test
活动模板如下:
其中的$END$
表示最后光标会出现在哪里。在此,表示光标最后会出现在方法体内,以便于继续编写实现。
可以通过在Preferences中搜索Live Templates,来创建或修改活动模板。
有许多常用的代码,例如getter、setter、constructor、equals&hashCode等等,IDEA都能够通过这个功能帮助自动生成。
我们在上面的步骤中使用了Ctrl+N生成了junit的测试方法,用Alt+Enter通过修复错误的方式来生成类和方法。
IDEA支持许多插件。插件的功能强大,能够做到从修改字符串到语言级别的支持。比如我们用的版本控制系统VCS就是用插件的方式开发的。
我们在上面的步骤中使用了string-manipulation插件。安装完插件,别忘了重启IntelliJ IDEA。你也可以编写自己的插件。
]]>技术债的起源和后果、解决方式等,网上一搜一大把,我这里就不再赘述了。这里总结一点常见的误区:
Martin Fowler把技术债分为四个象限,如下图所示:
项目在不断前进,做项目的人也是不断前进的。项目需要还债来让自己运转良好,人不也一样需要还债让自己进步吗?参考上图,我也画了张个人的技术债四象限,如下图所示:
这四个象限不都是技术债的源头吗?下面我们来具体分析一下每一个象限:
有意的-慎重的:清理。例如,我知道项目上需要用到drools,它对未来的项目可能会很有用,可惜当时没条件深入学习。又或者项目上用到了JJTree,它有些过时了,以后也基本用不上,不需要浪费时间在这上面。这是两个不同的例子,因为你是有意地做出了选择,所以凭你的经验来决定吧,是否应该把它放到你的个人技术债上。
有意的-草率的:思考。例如,当时工作太忙,虽然知道docker能够解决这个问题,但没时间去学,至于项目嘛,凑合能用就好了。现在回头想想,还能凑合吗?因为草率,所以需要思考;因为有意,所以还要选择。是否放入你的个人技术债,你自己决定吧。
无心的-慎重的:复盘。例如,当时不知道其实AWS可以满足项目的需求,但是现在知道了,很可能用AWS可以节省一大部分的开发和运维成本,但也可能有坑。在这种情况下,我们可以做一次复盘,如果项目再来一遍应该怎样?把收获到的经验用到下一个项目中吧。
无心的-草率的:求知。例如,我并不知道前端技术大爆炸有那么多的框架可选,现在我也不太了解,反正有活儿我就上JQuery。用一句绕口的话总结就是:不知道自己不知道什么。如果是这样,那么就应该先高层次地了解一下背景知识,起码让自己不至于抓瞎吧。之后再慢慢将自己的知识体系建立起来。
接下来就该给你的债排优先级,用时间“四象限”法,XY轴分别是重要性和紧急性。重要又紧急的债先还;重要不紧急的债可以制定计划;紧急但不重要的,不值得投入大把的时间,够用就好;不重要不紧急的就尽量放弃吧,除非这个债是你的兴趣爱好之所在。
2017年已经远离,是不是在收获了许多成果的同时,也留下了一些遗憾?2018年的余额也已充值完毕,去年(说不定去了好几年呢)欠下的债,该考虑怎么还一还了吧?
]]>《Java函数式编程》并不是一本关于Java的书,而是一本关于函数式编程的书。作者由浅入深地介绍了函数式编程的思维方式,并引导读者通过易于掌握的例子、练习和图表来学习和巩固函数式编程的基本原则和最佳实践。读者甚至可以在阅读的同时编写出自己的函数式类库!
与《Java 8函数式编程》相比(这是一本Java 8的函数式用法的入门佳作),本书侧重的是函数式的思维与实践,而非是Java 8的语法。如果你是一位看完基础语法书后喜欢接着看“Effective”系列的程序员,那么本书就有几分类似于“Effective”版,只不过它讲的是函数式而非是Java 8的“Effective”。语言容易过时或被淘汰,但是思想永存。
有幸受邀翻译本书。初见书名,心中不免有几分疑虑,难道又是一本教你怎么使用Java 8 lambda来函数式编程的书吗?翻了几页,方觉自己大误。本书其实意在如何从零开始,逐步理清函数式编程的思维方式并编写基础类库,不仅授之以鱼,而且授之以渔。只不过由于Java的受众实在太广,所以才使用这门语言罢了。
函数式编程有一个至关重要的前提,那就是函数的输出只能取决于函数的参数(我们会在书中看到生成随机数的例子)。初看上去似乎与Java这门面向对象的语言不搭。但语言只是工具而已,正如你也可以在Haskell中编写命令式风格的代码。在一个不太复杂、甚至非并发的常规Java系统中,由于程序内部状态的改变,多次调用同一个方法的返回值很可能是不一样的,更不用说所带来的副作用了。函数式编程中,确定的输入决定了确定的输出,就意味着只要参数对了,结果一定在预期中。也就是说,函数式编程没有无法重现的bug。在这样的前提下,单元测试相对容易实现,而且能极大地增强你的信心。(想想你对目前所在项目的单元测试有多大的信心?)许多个这样的函数复合起来,在不改变信心的同时能够提供更多更强大的功能,进而带来更大的收益,如无状态的线程安全、必要时才计算的惰性求值、加快多次执行速度的记忆化等等。
传统的命令式编程是计算机硬件的抽象,源自图灵机,其实就是外部输入、内部状态、对外部的输出以及对内部状态的改变。函数式编程源自λ演算,即将变量和函数替换为或值表达式并根据运算符计算。函数式编程相比命令式编程代码更简洁、可读性更强,这是因为它的思维方式更倾向于描述要什么,而不是怎么做。所以学习过程反而更加自然,并且不需要多么高深的数学基础。可是我们也知道,软件开发没有银弹。新的方法论也会带来新的问题,需要运用新知识来解决。幸运的是,新知识的坑已经有人帮你踩过了,高阶函数、偏应用函数、复合函数、柯里化、闭包……软件开发从来不缺术语。幸好它们并非高不可攀,作者将会在第二章中扫清你的疑虑,并在后续章节中挑战惰性求值、记忆化、状态处理、应用作用还有actor等更高级的技术。你说Monad?作者才不告诉你它究竟是什么,但是看完本书你自然就领悟了。
函数式编程不是万能药。它有自己擅长的领域,也有自己的弱项。函数式编程是级别更高的抽象。高级别抽象带来的收益就是易读、好写,可是有些低级别的事情(如果你真的需要的话)可能就不容易完成。函数式编程没有副作用,导致无法完成输入/输出操作。尽管如此,你也会在本书中看到一些解决办法。函数式编程没有变量,因此无法改变循环的终止条件,故而没有循环,严重依赖于用递归来抽象循环。在某些情况下可能会影响性能,所以你还会在本书中看到一些性能与情怀之间的权衡。绝大部分的编程最佳实践都是针对某个特定的场景而言的。因此脱离业务场景直接讨论技术并不可取。拥有函数式编程的思维,你就拥有了解决问题的另一种选择,但是条条大路通罗马,千万别钻牛角尖。程序是对现实世界的建模,“不要让世界适应你的模型。让你的模型适应世界。”
感谢作者Pierre-Yves Saumont,不仅写了这样一本令程序员们受益匪浅的书,而且耗费精力维护本书的后续重构,还耐心地回答我对书中的疑问,使我有机会提高中文版的翻译质量。
感谢永恒的侠少和刘舫,让我可以集中精力专注于翻译之上,并让本书得以出版。
感谢瑞民,虽然世事变幻莫测,但是你始终扮演了非常重要的角色。
感谢家人和朋友们,我永远离不开你们的鼓励和支持。
有一篇文章叫《世界是由懒人创造的》(真的是马云分享的吗?),大致意思就是懒人推动了世界的发展。因为懒,才能创造出一堆的发明,来让我们的生活更方便。当然了,文章弥漫着浓浓的调侃氛围。赞赏、批判这篇文章的人都有不少,至于我的观点嘛,想必从本文的标题中也能看出一二。但是请别忘记,原文最后也写了:“要懒出风格,懒出境界”。
在程序员的世界中,偷懒尤其重要。懒得造轮子?网上大把大把的开源库等着你试用。不想稍微改点代码就从头到位测一遍?那就用自动化测试吧。不想每次部署的时候手忙脚乱?那就上持续交付。不想每次总跟客户扯皮?那就搞敏捷,把客户变成团队的一员。不想让开发和运维互斗?那就拥抱DevOps,大家都在一条船上。可以说,“偷懒”是技术进步的原动力。有些人喜欢说“痛点驱动”,其实它们是一回事,因为没法儿偷懒,所以很“痛”啊。程序员们也非常厌恶重复性的劳动,例如填写工时、定期发送邮件、给别人权限、教新人如何配置环境等等等等。
可是光靠偷懒能够解决问题吗?要是你不想做那么无聊的事情,但是又没有解决的办法,如何才能推动世界进步呢?有道是“创新靠懒,实现靠勤”。唯有学习和思考不能偷懒。你有一个工具箱,你懒,那就用工具箱里的工具来让你懒得其所。可如果你的工具箱是空的,你怎么偷懒啊?有些人看上去非常的勤奋,整天忙个不停,似乎非常充实。但是,偶尔夜深失眠的时候,可能内心也会感觉到一阵恐慌吧。因为忙碌占据了他全部的时间,而真正需要的沉淀、思考、总结的时间基本没有。这样的人只是用勤奋来自欺欺人,掩盖自己懒得思考的本质。有个词叫“低品质勤奋者”很好地涵盖了这一类人。我也曾是其中一员,现在还不时会偷懒,但我已经知道了,不要“用身体的勤奋掩盖思想的懒惰”。
偷懒节省出来的时间都上哪儿去了?学习、思考、与家人相伴。偶尔的放纵没什么关系,但是主旋律还是要保持清醒的大脑,经常使其运转,如果大脑平时不怎么动,可能在关键时刻也就转不动了。如何开始恢复大脑的正常运转?从每天给自己留点独处的时间,深度思考一下今天做了什么有意义的事情,怎样还能做得更好开始吧。一定要坚持,无论刮风下雨,生病加班,因为以我自己的经验来看,一旦破例,很容易便会再次破例,从而使曾经的坚持迅速土崩瓦解。但是可以根据当天的状况灵活控制时间。写文章也是一种有效的思考总结的方法,在此也推荐给大家。试试看,只要走出第一步,总能找到一条适合自己的路。
最后让我们来膜拜一下著名的懒程序员们吧。
只要把自己的公钥保存在远程主机上就可以了,如果本机尚未生成公私钥对(可以通过ls ~/.ssh
查看是否存在以pub
为扩展名的文件),可以通过ssh-keygen
生成一个。之后把这个pub
文件的内容全部复制到远程主机上的~/.ssh/authorized_keys
中就能够实现无密码登录了。复制的过程也可以用以下命令实现:
Mac上默认没有ssh-copy-id
,可以通过以下命令安装:
配置完无密码登录后,在远程主机上执行命令很简单,只要在最后面加一个字符串即可:
如果命令很长,是个脚本,那就这么搞:
下面分享一段调试时查看远程日志的实用代码。如果你不知道自己的请求会被负载均衡到哪台服务器上去,可以试试下面这个ssh到所有服务器上执行tail -F
的小脚本(当然也能用cat
了):
当然看完日志以后,别忘了把ssh的进程杀掉:
稍微解释一下以上的两个参数:
ssh -f
: 让SSH在后台执行,之所以在后面再加一个&
,是因为想让所有机器并行来tail日志。ssh -o StrictHostKeyChecking=no
: 这样就看不到由于第一次连接或是机器指纹变更而出现的Are you sure you want to continue connecting (yes/no)?超级简单:
这样便可以通过如下系统设置通过远程主机上网了(以mac为例):
浏览器代理也是一样(以chrome插件SwitchyOmega为例):
如果你想让本地经由remote1访问remote2,可以这么做:
-L
后面的参数,表示本地端口:目标主机:目标主机端口,也就是说,往本地9999端口发出去的请求,会经由remote1传给remote2的80端口。为什么我们会需要这样的东东呢?原因可能有几种:
|
|
中间的localhost是相对remote.host.name而言的,也就是它自己。
在你的本地可以连通远程主机remote1和另一台远程主机remote2,而它们俩不能相互访问的情况下,如果你想让remote1能够访问remote2,就可以这么做:
这样的话,remote1的本地用户便可以便可以通过你的9999端口,访问remote2的80端口了。相当于你把自己变成了一台堡垒机!如果你有权限在其它机器上运行远程端口转发的命令,那你也可以把它变成堡垒机,把你自己的客户机变成remote1了。
SSH当然是可以用来复制文件的:
其中的参数-e none
,表示不转义任何字符。SSH默认会通过~
来转义一些控制语句。
但是既然我们有scp
,还用ssh
图个什么,用专业工具吧。
可以用sshfs来将远程的文件系统通过SFTP加载到本地。对于mac而言,可以用FUSE for macOS来实现。我还没有那样的需求,没试过,据说比较简单。有兴趣的读者可以自行尝试。
Windows的话,你可以就得试试Putty了。
用手机和平板来运维?你值得拥有!
JuiceSSH的基本功能时免费的,但是要想端口转发什么的就得收费了。用户体验很不错。
ConnectBot是完全免费的。
Prompt都说好,收费。
SSH: More than secure shell
SSH原理与运用(二):远程操作与端口转发
25 Best SSH Commands / Tricks
原理其实很简单,就是在宿主页面载入完毕后,运行自己的js脚本罢了,从而实现对浏览器渲染后的html进行改变。除了让你浏览的网页更加个性化以外,还能为你提供一键解决实际问题的需求,例如隐藏广告、自动签到薅羊毛、抢票、每天/每周在网页上填写考勤表等。
Tampermonkey不仅允许使用这些脚本,并且可以编写、管理及同步。安装步骤与一般的插件无异。安装好后,下面拿一个例子练手。
我们试试在百度上增加“Google一下”的按钮。首先单击插件图标,点击“添加新脚本”,于是便进入了Tampermonkey的编辑器。上面的几行注释就是Tampermonkey自己的语法,保存了一些元数据,包括脚本的名字、版本、在满足什么规则的网页上生效等。我们把@match
的内容修改为https://www.baidu.com/*
,这样便能在百度域名下的所有网页中生效啦。
打开百度,可以看到“百度一下”的按钮:id="su"
,我们将“Google一下”插入到其后即可。在// Your code here...
后面编写如下js代码:
保存并刷新百度,顺利地看到了灰色的“Google一下”。这说明我们的脚本起作用了。在插件图标的位置上页显示了一个红色的1
,说明当前网页上的生效脚本数量为1。但是点击“Google一下”什么也没有弹出。在控制台上可以看到出错了:Uncaught ReferenceError: googleIt is not defined。这是因为所有代码都是作为一个字符串被eval
的,所以应该使用动态的方式:
这回点击按钮就能够正常工作了!我们现在要做的,就是把alert
替换为打开Google页面即可:
短短几行代码,就搞定了。Tampermonkey内置了一些对象与函数,可以让我们很方便地实现一些功能,如打开新窗口可以使用GM_openInTab
:
第二个参数可以决定当前的焦点是老窗口还是新窗口。在使用内置函数之前,需要先@grant
一下,如:
否则无法生效。更多的内置函数可以在官方文档中查看。
我们新写的代码并不会直接出现在控制台中,如果需要调试,可以在代码中增加debugger;
,这样运行时就能自动停在这一行了。百度主页有引用jQuery,所以我们可以直接在代码中使用$
。如果需要的页面上没有jQuery,那么可以通过这行命令引入:
完整的代码如下:
不过这种做法存在两个问题:
但是获益的巨大几乎可以让我们完全无视这些缺陷。
脚本也是代码,也有自己的版本,也能被多人所共享使用。只要它在因特网上的url以.user.js
结尾,即可轻易分享给其他人。上面的这个例子我就放到了Github中。分享出去的时候,如果对方安装了tampermonkey,就可以看到脚本的安装界面了。更多的共享脚本可以单击插件图标,点击“获取新脚本”,或是参考这里来获得。
一般来说,我们写程序,都是连同测试代码一起提交,至少也是一起代码审查。有一个关于收费的项目,偏偏反其道而行之。这个项目的一些背景如下:
如果有一个对费率的修改,那么程序员除了往代码库中提交修改后的DSL,还会往S3更新json测试文件。这就带来了几个问题:
那么,为什么这个项目会选用这样的方案呢?原来,在设计的时候是这么考虑的:费率修改的需求来自于产品经理,希望测试数据能够由PM们提供。所以将会开发一个面向PM们的小系统,后台就是这个S3数据,这样的话到时候修改费率,就增加了一层来自需求方的保障。如果测试数据来自于代码库,那就很难通过页面来修改并提交代码库了。初衷还是不错的,但是仔细推敲下来,DEV自己的测试哪儿去了?这样运转起来后,是不是只会养成DEV把测试推给PM的习惯?
我觉得这里面有一个误区,就是把DEV自己的测试和PM的测试混为一谈了。实际上应该将它们分开来。为什么呢?DEV自己的测试其实本来就应该是代码的一部分,应该保存在代码库中。而PM的测试其实是对程序员测试的补充,也应该算是代码的一部分,如果能够放在代码库中固然是好,但是我们也不能对所有的PM们都抱有提交代码这样不切实际的期望,所以在这种情况下,S3算是一个权衡的方案。而CI上应该有两步,其一是DEV的测试,其二是PM的测试(在PM修改测试数据的小系统还没上线之前,可以暂不配置这个测试)。它们之间是顺序执行还是并发执行倒是无关紧要。但这样也有不尽如人意的地方:
顺便提一句,传统的PM测试数据是由DEV提供一个CSV格式,让PM填完之后由DEV添加到代码库中。在忽略DEV和PM的用户体验的情况下,这也是一个可行的方案。
到底什么样的逻辑应该进代码库,什么样的逻辑应该持久化呢?其实我们应该把逻辑区分为程序、配置和数据。
程序:
在代码库中,提供服务的主要功能。对其的修改通常都是改bug或是引入新功能。
配置:
在配置服务器中,但是配置的默认值很可能是在代码库中。经常需要修改,修改其值可以让程序表现出不同的处理逻辑。需要易于修改。
数据:
在持久化存储(一般是数据库)中,因用户而异,数量可能会比较大。随用户的操作而变化。需要有备份机制。
反推到上文所说:“收费的逻辑通过DSL配置在代码库中”。这段逻辑,也许应该是配置而非程序,因为它会经常需要修改。收费记录毋庸置疑,一定是数据了。从逻辑分类的角度上出发,你是否会发现其实自己现在的代码库中包含了太多的配置?
另外,虽然代码库似乎也可以用于配置或数据,但是最好还是别这么干,这里有一篇stack overflow的问答,解释得挺清楚的。
]]>我希望在项目中能够实现这样的功能:用户发送一个request,服务器就帮用户生成代码并生成一个commit到用户本地的git中,但是这不太可能,因为用户的环境并不是服务器的环境。进一步的方案是直接在服务器端clone git仓库(或是维持一份最新代码),服务器本地生成commit并push,这样做会有一些安全方面需要考虑的因素。我采用的是退一步的方案,即让服务器生成一个patch文件并上传到S3,以便用户稍后下载并apply到本地。
打开JGit的api包一看,各种git命令应有尽有,如apply、cherry-pick等。但惟独没有format-patch命令。网上一搜,甚少有人有这样的需求或问题,只有这篇文章比较靠谱,但是它介绍的侧重于diff而非生成patch。
还是得找找patch相关的代码。源代码搜遍也就这个Patch.java应该是patch文件的JGit模型,但是读完后发现,它只能把patch文件映射成这个模型,并不能反向从模型序列化为patch文件。
DiffCommand其实上还是调用的DiffFormatter和DiffEntry,所以看看这俩是否能够支持什么样的参数,来生成patch文件呢?可惜还是无果。DiffFormatter的API也不太直观,不容易理解。但是它能够做一些diff commit这样的事情。
走投无路之际,在git-format-patch上看到,这个命令其实是用来生成用邮件发送的patch文件。难怪patch文件的前几行看起来有From,有Subject什么的,也许它们不是必须的?那就可以试试把diff的结果当作patch直接写入文件。
首先创建一个git环境,a、b、c三个文件用来测试改删增:
运行完成后就能看到diff文件的内容了:
在程序中,如此这般运行git diff命令:
发现JGit的diff和Git的diff还是不太一样的。JGit的diff包含了新增文件的信息:
这就非常合适了。只要证明它能够被作为patch导入到git中即可。首先修改代码输出到文件:
然后清空修改过的文件:
现在尝试apply patch:
果然成功了。在不考虑冲突的情况下,看起来这一招还是管用的。但是由于缺失了commit的信息,所以运行git am /tmp/jgit.patch
就会报错:Patch format detection failed.有没有办法解决这个问题呢?当然了。我们现在知道了patch只不过是多了一些邮件信息罢了,那我们自己就可以生成。在try
内增加如下代码,模拟git format-patch
:
运行一下,然后尝试使用git am
:
果然可以直接生成commit。Mission Complete!
其实Linux已经提供了一个patch
命令,无需git即可直接应用patch文件:
而且还支持回滚(git apply也支持):
实际上patch文件一般是使用diff
命令来生成的:
这两个命令网上的教程不少,有兴趣的话可以自行搜索阅读。最后还是把环境恢复:
从用户指南的概念一节中可以看到,JGit的基本概念如下:
AnyObjectId
和ObjectId
表示。而它又包含了四种类型:让我们从一个最典型的用例开始吧。首先在/tmp/jgit/repo
中创建一个git仓库:
再创建一个clone该仓库的客户端:
输入git status
应该能够看到Initial commit,这样环境就没有问题了。然后提交一个文件,给仓库里来点库存:
动手时间。新建Maven工程,往pom.xml中增加dependency,最后的pom.xml看起来就像这样:
让我们先尝试clone一下这个仓库。因为client分为已经存在以及重新clone的两种,所以我们在src/main/java中新增一个RepositoryProvider
接口,用两种不同实现以示区分:
并实现之:
新增一个HelloJGit
主程序类:
直接运行HelloJGit
的main
函数,ls /tmp/jgit/
应该就能看到新clone出来的clientJava
文件夹了。
我们当然不希望总是在使用的时候才重新clone一个仓库,因为当仓库很大的时候可能会非常耗时。让我们在client
中再提交一个commit:
然后尝试直接从刚刚clone下来的clientJava中创建Repository:
然后把HelloJGit
的repoProvider
实例替换为RepositoryProviderExistingClientImpl
:
注意这次的路径中需要加上.git
才行。再次运行HelloJGit
的main
函数,便可以通过ls /tmp/jgit/clientJava
看到新提交的hello2.txt
文件了。
接下来尝试git add
、git commit
和git push
这几个最常用的命令。让我们往clientJava
中添加一个hello3.txt
文件并提交。如下修改HelloJGit
:
虽然操作多了,但是有了Repository
和Git
对象之后,看起来它们的实现都非常直观。运行main
函数之后,可以到client
文件夹中校验一下:
在我的机器上运行git log
,可以得到:commit 7841b8b80a77918f2ec45bcedb934e2723b16b5c (HEAD -> master, origin/master),以及另外两个commit。有兴趣的读者们可以自行尝试其它的git命令。
虽然上面两小节的内容对于普通需求来说已经大致上够用了,但是在概念一节中介绍到的其它概念,如Git对象、引用等还没有出场呢。我们再新建一个WalkJGit
的类,在main
函数中编写如下代码:
这回,Ref
和ObjectId
都出现了。在我的机器上,运行以上程序打印出来了AnyObjectId[7841b8b80a77918f2ec45bcedb934e2723b16b5c]。我们可以看到,取得HEAD
的Ref
,其ObjectId
其实就是在client
文件夹中运行git log
之后结果。除了HEAD
以外,repo.getAllRefs()
返回的Map
实例中还有refs/heads/master
和refs/remotes/origin/master
,在目前的情况下,它们的ObjectId
完全相同。那么如何获取其它的commit呢?那就是RevWalk
出场的时候。把main
函数中的内容替换为如下代码:
可以看到RevWalk
本身是实现了Iterable
接口的。通过对该对象进行循环,就可以获取所有的commit的RevCommit
对象。可以到client
文件夹确认一下,这些SHA-1字符串应该跟刚才git log
命令的结果相同。RevCommit
对象本身含有这个commit的所有信息,所以可以如下打印出来:
这样看起来是不是很有git log
的感觉呢?需要注意的是,RevWalk
线程不安全,并且像Stream
那样,只能使用一次。如果想要再来一次,就需要重新创建RevWalk
对象或是调用其reset
方法(还得重新markStart
!)。
要想看到每个commit中有什么内容,那就需要用到TreeWalk
了,它的思路和RevWalk
类似。尝试如下代码:
这样便可以显示仓库在每个commit时候的状态了。如果需要diff,那么还将需要用到DiffEntry
等类,本文就不再赘述了,有兴趣的读者可以参考这个类。
最后将环境还原:
在谈论数据时,人们经常将其与信息相混淆。其实信息来源于数据,但是并非所有的数据都承载着有用的信息。例如,对于dd if=/dev/zero of=ggg.txt bs=1k count=10000
这样的一个10M文件来说,里面的每一个bit都为0,所以并不能提供什么有意义的信息。而且,无意义和伪造的数据都会干扰和影响我们。通过对信息的处理,可以获取知识,以推动人类文明的发展。例如:通过测量星球的位置和对应的时间,我们可以得到数据;通过处理这些数据得到星球运动的轨迹,就是信息;通过信息总结出开普勒三定律,则是知识。而人类的智能往往体现在:获取数据→分析数据→建立模型→预测未知上。
现在我们所说的人工智能有两个定义:狭义的人工智能指的是20世纪五六十年代的研究机器智能的特定方法,即传统人工智能方法,专注于让机器像人一样地去思考;广义的人工智能指的是任何可以让计算机通过图灵测试的方法,即让一台机器和一个人在幕后,一位裁判同时与他们交流,看看裁判是否能够分辨出自己交流的对象是机器还是人。以翻译为例:传统的方法就是针对某两种语言编写大量的规则,以反映人类的思考方式;而现代的方法则是通过数据驱动,用机器学习的方式训练出翻译模型的各种参数。在互联网出现以前,很难获取到大量的有效数据,因而实用性不高。但是在如今的大数据时代,获取大量数据已经成为了可能。越来越多的信息可以使模型越来越准确,进而使翻译的效果越来越好。这里有个大数据预测美国大选的例子:2012年有人把互联网上公开的新闻、Facebook、Twitter等选战数据按照州来整理,竟然成功地预测了全部50+1个州的选举结果。
数据的作用过去常常被人们所忽视。首先是由于过去的数据量不足(少了大数据的大,Vast);其次是数据缺乏相关性(少了多维度,即多样性Variety)。而现在的数据量由于计算机本身的数据、传感器的数据以及旧信息的数字化,比过去增加了许多,使量变足以成为了质变。数据驱动方法过去的死穴在于,使用基于概率统计的模型会有很多小概率事件覆盖不到。只有提高数据的完备性才行。这在以前是很难做到的,比如搜集全国所有人的面孔。但是如今这样的事情也并非遥不可及。所以我们也许需要重新认识穷举法,在大数据时代它并不像想象中的那样笨。数据的相关性也非常重要。我上班的时候会经过一家广东肠粉的小吃店,招牌上“广”字的一点已经脱落,成了“厂东肠粉”。我们的智能当然能够判断出来这是广东而不是厂东,但是大数据呢?首先它并不能找到什么有意义的“厂东”,但是能找到广东,并且“厂”和“广”字形非常接近。但是这样也无法否定是不是有个小地方叫“厂东”,或是老板的名字叫“厂东”。所以需要交叉验证。接下来发现“广东”和“肠粉”两字经常出现,有相关性。这样数据的相关性便大大提升了可信度。要是有图片,还能根据“厂”的字形比另外三个字稍扁来做进一步的交叉验证,准确性就能够更上一层楼。实际上如果在baidu搜索“厂东肠粉”的时候,它已经会问你“您要找的是不是: 广东肠粉”。Google虽然并不提示你,但显示的搜索结果也都是广东肠粉。当大家都意识到数据的重要性后,市场上的竞争就从技术的竞争转变成了数据的竞争,智能问题已经演变成了数据问题。
我们现在说起机械思维,总觉得它是个贬义词。但其实它正是以前推动工业革命的要素。机械思维认为世界变化的规律是确定的,因此规律可以被认识,并且可以用公式或语言描述清楚,放之四海而皆准。但成也萧何败萧何,它的局限性正是否认了不确定性和不可知性。世界的不确定性首先来自影响世界的变量实在太多,以至于无法套用公式算出结果。其次世界本身也是不确定的,人类对于世界的观察将会改变世界本身,如量子力学的不确定性原理。但是不确定并不意味着没有规律可循。香农在概率论的基础上,用信息论将世界的不确定性与信息联系了起来,给了人们一种看待世界和处理问题的全新思路。
新思路为我们带来了大数据思维。它的核心是:数据中所包含的信息可以帮助我们消除不确定性,而数据之间的相关性在某种程度上可以取代原来的因果关系,帮助我们得到想知道的答案。例如,根据大数据的统计结果,可以发现在视频网站上投放零食的广告效果很好,我们可以据此猜出人们在看视频时喜欢吃零食。所以这种新的思维方法允许我们在不知道原因的情况下直接从大量数据中寻找答案,即无监督学习。虽然机器推算出来相关的事情只有一定的概率,但是世界本身就充满了不确定性,100%的准确率固然是好,但是90%的结果也是非常有价值的。这就是思维的革命。
还有产业的革命。套用一个公式:现有产业 + 新技术 = 新产业。例如:
在瓦特改良万能蒸汽机之后,很多上千年历史的古老行业都通过使用蒸汽机而变为新产业,如纺织业冲击了几千年来的家庭纺织业,而瓷器则由白色黄金变成了日用品等。并不需要每一个工厂都去制造蒸汽机,而大多数工厂都会受益于蒸汽机。到了19世纪末,电力的应用也催生了各种新产业,如建筑业通过使用电梯使人们可以把楼盖高,交通运输业通过电车、地铁等公共交通促进了城市的发展,形成了大都市。但是也不需要太多的供电公司,对于美国而言就是通用电气和西屋电气,而大多数公司都会受益于电。“二战”之后,许多产业在使用计算机之后产生了质的变化,如金融业、通信业等。计算机处理器是信息革命的代表产品,但是同样并不需要有很多生产它的公司,今天大部分的处理器都是来自Intel或AMD以及ARM公司所设计的产品,而大部分电脑和智能设备都离不开它们。正在到来的智能革命,也将催生和改造出许多的新产业,但同样,并不是所有公司都会掌握大数据或是培养出机器智能,而大多数公司都将受益于大数据和机器智能。
新革命的到来当然不会是一帆风顺的,首先要解决的是技术上的挑战。大量的数据必然需要大量的存储,并使查找和使用数据的时间剧增。早期存储数据的磁带和软盘根本不可能承担起存储如此海量数据的任务。硬盘虽然容量上去了,但是其存取速度仍然受限于机械运动。直到SSD的崛起和平价化,才使得在存储技术上适应了大数据。数据的传输也是一个挑战,直到移动互联网和WIFI技术的兴起,才使得大量数据的传输成为可能。而对数据的处理,则受益于摩尔定律,处理器速度增加并越来越便宜,从而导致并行处理技术如Map Reduce等的发展。也有目前还没完全解决的问题,如数据的标准化等。Google设计了一种称为Protocal Buffer的数据格式,并已开源供大家使用。
技术问题解决后,就是商业问题了。如何获得一个全集的大数据呢?例如,为了了解电视的收视率,显然不能再一个个地去发传单、打电话了。最好的方法是通过机顶盒记录用户的收视情况。但是掌握这些数据的生产厂商和有线电视运营商当然不会轻易地把这些数据分享出来。所以Google推出了自己的电视机顶盒Google TV,为获取数据进入电视广告市场做准备,但是销售结果很糟糕,据说后来每个季度退回来的机顶盒比卖出去的还多。以至于Google在2014年斥巨资收购了还在亏损状态的nest公司,以获取nest公司的产品(恒温器)在每一个家庭的数据。一些公司已经敏锐地发现了数据的价值,而另一些公司却捧着金饭碗要饭。
还有数据的安全。首先由于数据量大,数据一旦丢失或被盗,损失将是巨大的。一种行之有效的方式就是利用大数据本身的特点来保护大数据的信息安全。如果外来的入侵者侵入了计算机系统,由于对业务的不熟悉,他的操作很可能与众不同,因此可以通过与大数据的对比而被发现,从而被制止。还有就是对于隐私的保护。如果导航系统能够帮人们导航并避开拥堵路段,那也说明它知道每个人的行踪。一旦这些信息暴露出来,这是非常危险的。再比如说《大数据高手塔吉特:我知道你怀孕了!》。现在的很多公司都或多或少具备了这样的能力,只是大家不知道或者不注意而已。甚至连淘宝的商家都有可能收集到你的信息,从而决定给你寄真货还是寄假货。
最后,随着生产力的进一步发展,机器将会抢掉许多人的饭碗:工人、医生、律师、翻译、编辑、中间商等等。特斯拉的汽车装配厂都是由机器人操作,很少雇佣汽车行业的人员,而所雇的都是IT人员。机器将会从大数据中学习到各种专家的知识,甚至表现得更好。那未来如此多失业的人将会怎么办?目前人类还没有很好的办法,只能靠“拖”字决。一两代之后,无法掌握新技能的人也已经到了退休年龄了。AI会有更好的办法么?也许AI会发现只需要让大家陷入深深的睡眠就可以了。《黑客帝国》又向现实迈进了一步。但是AI毕竟来自于大数据,而不是全数据。有朝一日醒来的人类,也许能够通过制造不常见的场景,引发AI的bug,就像李世石曾经战胜AlphaGo的那一盘围棋一样。而埃隆·马斯克用“脑机接口”的宏伟蓝图来应对AI。既然人类可能最终会被AI消灭,那不如就让人类与AI成为一体。
“2%的人将控制未来,成为他们或被淘汰”。
]]>