逸言

Akka中的Remote Actor

| Comments

Akka的设计目标就是为分布式准备的,因此所有Actor之间的交互都是通过消息,且所有动作都是异步的。这种做法就是为了确保Akka的所有功能无论是在单独的JVM,还是包含了成百上千机器的Cluster,都是可用的。

然而,本地与分布式总是存在区别,主要牵涉到两点:

  • 消息需要支持序列号;

  • 消息传递的可靠性问题;

为了保证本地处理与分布式处理的透明化,Akka几乎没有特别为Remoting Layer提供专门的API,区别仅在于配置。开发者只需遵循Actor设计的原则,然后在配置中指定Actor子树的远程配置即可。当然,在代码层面,Akka也提供了唯一与众不同的API,就是在包含部署信息的Props中,可以允许设置Deploy实例。不过,这件事情是可以配置的。倘若两者都设置了,配置文件优先。

若要支持Scale up,Akka提供了多个Actor子树以支持并行处理。然后以多种方式进行路由。开发者唯一要做的事情是声明一个确定的Actor作为“withRouter”,事实上就是创建一个路由Actor,它能够生成所需类型Children的数量值,该数量值是可以配置的。

Remote Actor

要调用Akka的Remote Actor,则需要对Remote Actor进行部署。首先,我们可以通过Akka的官方网站下载Akka的库。我这里下载的是2.2.3版本。下载后解压。与部署有关的目录包括bin和deploy。在bin目录下是运行Akka的脚本。而在deploy目录下,除了Readme文件外,此时为空。

现在,我们可以编写一个Remote Actor。Akka要求我们定义一个Actor,以及它对应的Application。Remote Actor与普通的Actor定义没有什么区别,例如:

package com.agiledon.akka

import akka.actor.Actor

class RemoteActor extends Actor {
  def receive = {
    case message: String =>
      sender ! message + "got something"
  }
}

但对于Application而言,则要求派生自akka kernel下的Bootable。akka kernel是单独的一个包,并没有包含在akka-actor中。因此需要定义依赖,例如使用sbt:

scalaVersion := "2.10.2"

libraryDependencies += "com.typesafe.akka" % "akka-actor_2.10" % "2.2.3"

libraryDependencies += "com.typesafe.akka" % "akka-kernel_2.10" % "2.2.3"

定义的Application如下所示:

package com.agiledon.akka

import akka.kernel.Bootable
import akka.actor.{Props, ActorSystem}
import com.typesafe.config.ConfigFactory


class RemoteNodeApplication extends Bootable {
  val system = ActorSystem("RemoteNodeApp", ConfigFactory.load().getConfig("RemoteSys"))

  def startup = {
    system.actorOf(Props[RemoteActor], name = "remoteActor")
  }

  def shutdown = {
    system.shutdown()
  }
}

我们需要实现startup与shutdown方法。而在Application中,会加载配置文件application.conf的配置信息创建ActorSystem。配置如下:

RemoteSys {
    akka {
        actor {
            provider = "akka.remote.RemoteActorRefProvider"
        }
        remote {
            enabled-transports = ["akka.remote.netty.tcp"]
            netty.tcp {
                hostname = "192.168.3.34"
                port = 2552
            }
        }
    }
}

端口号2552也是Akka的默认端口号。

部署Remote Actor

application.conf文件应该放到resources目录下。编译打包。然后将编译得到的jar包拷贝到akka的deploy目录下。然后转移到bin目录下,执行akka脚本(windows下是执行akka.bat): ./akka com.agiledon.akka.RemoteNodeApplication

akka命令后面是application类的full name。如果一切正常,就可以显示如下界面:

Client(Local) Actor

要调用部署了的Remote Actor,客户端的Actor可以通过将Remote Actor的address传递给actorSelection()方法(之前的版本为actorFor,目前已经被Deprecated),以此来获得Remote Actor的Ref。如下所示:

package com.agiledon.akka

import akka.actor.{ActorLogging, Actor}
import akka.util.Timeout
import akka.pattern.ask
import scala.concurrent.Await
import scala.concurrent.duration._

class LocalActor extends Actor with ActorLogging {
  val remoteActor = context.actorSelection("akka.tcp://RemoteNodeApp@192.168.3.34:2552/user/remoteActor")
  implicit val timeout = Timeout(5 seconds)

  def receive = {
    case message: String =>
      val future = (remoteActor ? message).mapTo[String]
      val result = Await.result(future, timeout.duration)
      log.info("Message received from server -> {}", result)
  }
}

同样需要定义配置文件,但无需指定hostname与port了:

LocalSys {
  akka {
      actor {
          provider = "akka.remote.RemoteActorRefProvider"
      }
  }
}

编写Application,使其可以被运行以调用Remote Actor:

package com.agiledon.akka

import akka.actor.{Props, ActorSystem}
import com.typesafe.config.ConfigFactory

object LocalNodeApplication extends App {
  val system = ActorSystem("LocalNodeApp", ConfigFactory.load().getConfig("LocalSys"))

  val localActor = system.actorOf(Props[LocalActor], name = "localActor")
  localActor ! "hello demo actor"

  Thread.sleep(4000)
  system.shutdown()

}

在客户端模块的根目录下创建build.sbt文件:


name := "actor-local"

version := "1.0"

scalaVersion := "2.10.2"

libraryDependencies += "com.typesafe.akka" % "akka-actor_2.10" % "2.2.3"

libraryDependencies += "com.typesafe.akka" % "akka-remote_2.10" % "2.2.3"

转移到客户端模块的根目录下,执行sbt命令,进入SBT。执行compile命令编译客户端模块actor-local。然后执行run命令,若运行成功,即可看到如下信息:

[info] Running com.agiledon.akka.LocalNodeApplication
[INFO] [02/18/2014 19:11:47.461] [run-main] [Remoting] Starting remoting
[INFO] [02/18/2014 19:11:47.725] [run-main] [Remoting] Remoting started; listening on addresses :[akka.tcp://LocalNodeApp@192.168.3.38:2552]
[INFO] [02/18/2014 19:11:48.066] [LocalNodeApp-akka.actor.default-dispatcher-3] [akka://LocalNodeApp/user/localActor] Message received from server -> hello demo actor got something

Akka中的Actor System

| Comments

图中表示的是一个Actor System,它显示了在这个Actor System中最重要实体之间的关系。

Actor Reference

一个Actor引用是ActorRef的子类型,主要目的是发送消息给它表示的Actor。Actor可以通过访问self字段来得到自身的引用;若要访问发送消息的Actor的引用,则访问sender字段。

Actor Path

可以认为Actor Path是通过字符串对Actor层级关系进行组合用以标识唯一Actor的一种方式。我们在创建Actor Path时,不用创建Actor;但如果没有创建对应的Actor,则不能创建Actor Reference。还可以创建一个Actor,再终止它,然后再以相同的Actor Path再创建一个新的Actor。新创建的Actor是Actor的新化身(Incarnation),但与旧的Actor并不是同一个。对于这个新化身而言,持有旧Actor的Actor Reference并不是有效的。消息发送给旧的Actor Reference,但不会被传递给新化身,即使它们具有相同的路径。

Actor Path包含协议、位置和actor的层级。如下是一些Actor Path的实例:

//purely local
"akka://my-sys/user/service-a/worker1"                   

// remote"akka.tcp://my-sys@host.example.com:5678/user/service-b" 

//clustered (Future Extension)"cluster://my-cluster/service-c"

有两种方式可以获得Actor Reference:创建Actor或查找。

要创建Actor,可以调用ActorSystem.actorOf(),它创建的Actor在guardian actor之下;接着可以调用ActorContext.actorOf()在刚才创建的Actor内生成Actor树。这些方法会返回新创建的Actor的引用。每个Actor都可以直接访问Actor Context来或得它自身、Parent以及所有Children的引用。

要查找Actor Reference,则可调用ActorSystem.actorSelection()方法。要获得限定到特定Actor的生命周期中的ActorRef,可以使用sender引用来发送一条消息如内建的Identity消息给Actor。

在查找ActorRef时,可以使用绝对路径或相对路径。如果是相对路径,可以用两个点(..)表示parent actor。例如:

context.actorSelection("../brother") ! msg

使用绝对路径的例子:

context.actorSelection("/user/ServiceA") ! msg

还可以使用通配符查询逻辑的Actor层级,例如下面的例子就是发送消息给除当前Actor之外的所有同级Actor(因为..代表parent,所以这里就意味找当前Actor的parent的下级Actor):

context.actorSelection("../*") ! msg

区别:actorOf vs. actorSelection vs. actorFor

  • actorOf:创建一个新的Actor。创建的Actor为调用该方法时所属的Context下的直接子Actor;
  • actorSelection:当消息传递来时,只查找现有的Actor,而不会创建新的Actor;在创建了selection时,也不会验证目标Actors是否存在;
  • actorFor(已经被actorSelection所deprecated):只会查找现有的Actor,而不会创建新的Actor。

远程部署的相互影响

当一个Actor创建一个Child时,Actor的系统部署器会决定这个新的Actor究竟属于同一个JVM,还是另一个节点。如果是后一种情况,Actor的创建就会通过在不同JVM的网络连接而触发,这属于不同的Actor系统。远程系统会将新的Actor放在一个特定的路径下,且新Actor的Supervisor应该是一个远程的Actor引用。而且,context.parent(Supervisor的引用)与context.path.parent(actor path的父节点)表示的不是同一个Actor。如下图所示:

注意图中展现的两个不同的Actor系统之间的Route关系。在左边的Actor系统中,Child Actor属于Remote ActorRef,它指向了右边远端Actor系统中的一个Actor节点,该Actor对于右边的Actor系统而言,属于Local ActorRef,但它的Parent Actor却是一个Remote ActorRef,它指向了左边对应的Local ActorRef。

Actor Path的Top-Level Scopes

Actor路径的根为”/”,而后续层级包括:”/user”, “/system”, “deadLetters”, “/temp”, “/remote”。

这里体现了Akka遵循“简单”原则的设计目标:层级中的任何事物都是Actor,且所有Actor的功能都采用同样的方式。

黄桷坪美院的涂鸦街

| Comments

昨天去黄桷坪的美院涂鸦一条街。虽然楼宇有些破破烂烂,墙上的涂鸦显得色彩浓厚而杂乱,但那种艺术的氛围还是让我感觉到愉悦。这种涂鸦原本就是要用颜色来描绘一种情绪,又或是依据建筑的外观涂抹一种符合其风格的自然肖像,当然也可以是卡通人物,将凸起的阳台,匍匐的树藤,蔓延的冰冷水管,或屋顶废弃的烟囱连接为一个魔幻世界。这就是艺术让人着迷的地方了,那就是所谓的“创造”。

这幅的颜色涂抹并不张扬,在绿树下显得线条更柔和又抽象:

下面这幅让我产生建筑密集症:

这一幅具有萌趣,但很破烂,那些外挂的空调外机反而成了装饰:

这种蓝色是我最爱的,虽然破旧,我反而觉得更融洽:

Scala支持与Java的隐式转换

| Comments

Neal Ford在几年前提出的“Poly Programming”思想,已经逐渐成为主流。这种思想并非是为了炫耀多语言的技能,然后选择“高大上”。真正的目的在于更好地利用各种语言处理不同场景、不同问题的优势。

由于都运行在JVM上,Java与Scala之间基本能做到无缝的集成,区别主要在于各自的API各有不同。由于Scala为集合提供了更多便捷的函数,因此,Java与Scala在集合之间的互操作,或许是在这种多语言平台下使用最为频繁的。

Scala中操作Java集合

两种情况需要在Scala中操作Java集合。一种是Scala调用了其他的Java库,针对Java集合需要转换为Scala集合,如此才能享受Scala集合提供的福利;另一种是编写了Scala程序,但需要提供给Java库,为了更好地无缝集成,要让Java库体会不到Scala的存在。

跳出舒适区

| Comments

今天,微信群“改变自己”发布了一篇张辉的文章《一个人的旅程:逃离舒适圈》——之所以选择一个人从北京到拉斯维加斯,就是希望挑战一下自己的孤独和不安感觉。读到这篇文章,让我想起自己的一次远行,何其相似。当我只有一个人面对一个陌生环境时,我也常常会陷入一种焦虑不安的情绪之中。区别只在于张辉的一个人旅程是主动选择,而我则是迫于无奈。

那是在前年,我自己一人独自上路,乘坐从北京到布里斯本(Brisbane)的飞机。飞机经由香港转机到布里斯本,而在之前还要在凯恩斯(Keynes)做短暂停留。

问题就出在这里,我一直以为在香港转机后,是直飞布里斯本。所以当飞机即将到达凯恩斯时,听到通知凯恩斯即将到达的广播时,就开始了自己的纠结:我是否坐错了飞机呢?实在按奈不住,终于忍不住问询了旁边的一位菲律宾人,得到完全无误的确认后,我才长舒了一口气。那种纠结真是折磨,虽然在理智上认为这种错误不可能发生,可是心里总是放却不下。当我走出布里斯本机场,上了公司派来接我的汽车后,似乎整个人才彻底轻松下来,开始与司机聊起布里斯本那美丽的景色来。同样是语言不通,同样是陌生人,飞机上的我格外地拘束,而坐在这白人小伙的车上,突然就变得自如了许多,就好似被压抑的洪水,一下子冲出了闸门。

一个人非得要经历一些陌生事物,陌生景色,才会慢慢变得硬线条,粗神经,降低对周遭环境的敏感度。只有如此,才能变得更加适应这个社会。我记得第一次出国到洛杉矶,相对于兴奋,更显得紧张和不安。可惜那一次因为是与同事同行,缺乏了一次绝佳的锻炼机会。工作之余,我们几个人一起租车到棕榈泉,到拉斯维加斯,到胡佛大坝。因为同行还有一位美国同事,所以很多事情都已安排好,自己完全不用费心考虑。

到布里斯本的一次,让我体味到寂寞孤独的同时,也让我心智成熟了许多。仔细思考,这种独自一人的旅行或许并不能让你收获什么知识,但它就像醍醐灌顶一般,不知不觉就打通了你的任督二脉。你突然会变得独立,变得勇敢,变得对陌生无畏。于是在布里斯本,我开始习惯在周末独自一人在布里斯本街头闲逛,在布里斯本河畔的餐馆就餐,到Queen Street的超市去购物,到Wedding Lawn看那些新人们举行婚礼,到South Bank体味艺术的人文气味;最后趁着复活节的空闲,独自一人去了一趟悉尼。我想,以后再将我扔到某个陌生环境,我至少不会恐惧了,我会学着正确地面对。

这正是我为何在去年要申请离开Office的项目出外做咨询,为何希望作为Coach申请到印度参加TWU。我希望尝试一些新东西,试着给自己一些挑战,这在我司,一个流行说法就是“跳出舒适区”。在一个熟悉的环境,每天面对熟悉的人,做着熟悉的事情,久而久之,自己的行为就会被惯性推动,然后成为习惯,产生惰性。这就好似在一个重力场一般,只要重力发生一丁点改变,我们的身体都会敏感的察觉,并通过神经中枢系统传达这种不适意。然而一旦熟悉了这种重力,我们在这样的重力场中的任何行为都会变得轻松自如。一旦重力再一次改变,即使是调整到之前的值,身体仍然需要做再一次调整。

只有不断地调整才能让自己不断的进步。从去年9月至现在,我在客户处经历了两个咨询项目,体验了许多与交付项目完全不同的经历和压力,学到了很多知识,能力也得到了很大提高,显然也增强了我的自信心。尤其是当咨询项目完成一个阶段时,看到客户在我的影响下向好的方向发生改变,工作效率和开发质量得到很大的提高,这种成就感是无与伦比的。

去年因为咨询项目的缘故,印度之行最后还是泡汤了。孤身一人到印度,然后与各个国家的TWer沟通,那种压力会更大吧。若能再跳出现在这个舒适区挑战一下自己,一定受益匪浅吧。所以今年,我还要再次申请到印度的机会!

Akka的Actor及其Supervisors

| Comments

声明:本文主要内容来源于Akka官方网站的Akka Scala Documentation文档。

Actor的Best Practice

在文档16页,给出了Actor的Best Practice,包括:

  • Actor应是一个好的协作者;
  • 不要在Actor之间传递可变对象;
  • Actor是行为与状态的容器;这意味着状态与行为应封装在Message中;
  • 顶级Actor是Error Kernel最内部的一部分,这有利于错误处理。

Actor

17页定义了Actor:

An actor is a container for State, Behavior, a Mailbox, Children and a Supervisor Strategy.

Actor对象可以分为内部与外部,外部以引用方式传递。这使得我们可以重启Actor而无需更新任何地方的引用(这是指内部的重启);可以将实际的Actor对象放在远端主机;可以发送消息给完全不同应用程序的Actor。

Actor状态可以是显式的状态机(例如使用FSM模块)或者计数器、一组侦听器、待处理的请求等。从概念上讲,每个Actor都拥有属于自己的轻量级线程,保护它不会被系统的其余部分影响。我们在编写Actor时,就不用担心并发。

每个Actor都有一个(恰好一个)Mailbox,所有Sender会将消息入队到Mailbox中。入队的顺序按照消息发送的时间顺序。Mailbox有多种实现,默认为FIFO。但也可以根据优先级考虑出队顺序,实现算法则不相同。

AKKA与其他Actor模型不同的是:当前的行为总是会处理下一个出队的消息,而不会去扫描Mailbox,获得下一个匹配的消息。因此,当处理消息失败,就会认为是失败,除非这个行为被重写了。

每个Actor都是一个潜在的Supervisor:如果该Actor创建了一个Child去执行子任务,就会自动来管理这些Child。Children的列表放在Actor的Context中,Actor可以访问他们。创建或停止的操作分别为: Liquid error: invalid byte sequence in US-ASCII

看起来,这种变更会实时反映出来;但事实上是以异步的方式在后台执行,它并不会阻塞Supervisor。

Supervisor处理失败场景的策略在创建Actor时就被确定,因而在Actor创建之后不能改变。一个Actor只有一个策略,因此,如不同的策略被运用到Actor的不同Child,就会被分组,会按照策略去匹配Supervisor,而非构建时的分类。

一旦Actor被终止,就会释放资源。在其Mailbox中的消息会被转发给系统的“dead letter mailbox”;然后该Mailbox会被替换为系统的Mailbox。所有新发来的消息也会作为Dead letter转发到系统的Mailbox。可以向Event Bus注册一个TestEventListener,监听dead letter的转发。这样就可以对错误写日志。


Supervision

Supervisor会将任务委派给下级(subordinate),并能响应这些下级的失败。若下级侦测到失败(例如抛出异常),就要暂停它自身以及它的所有下级,并发送消息给它的Supervisor,以标识该失败。这时,Supervisor有四种选择:

  • 重新获得(Resume)下级,并保持其累加的内部状态;
  • 重新启动(Restart)下级,清除其累加的内部状态;
  • 永久地终止下级;
  • 扩大(Escalate)失败,从而使得自身也失败。

重要的一点是要认识到一个Actor就是Supervision层次的一部分。

对于Actor类的hook方法preRestart()默认行为是在重启(restarting)之前,是终止所有的children(这个过程是递归的)。但是,该方法可以被重写。

Top-Level Supervisors

一个Actor系统在创建之初,至少有三个Actor,如下图所示:

1.The Guardian Actor

它是用户创建的Actor的parent,命名为“/user”。使用system.actorOf()方法创建的Actor都是它的children。这意味着只要这个Actor终止了,系统中所有常规的Actor都会被关掉。在Akka 2.1中,可以设置Supervisor Strategy,配置项为akka.actor.guardian-supervisor-strategy,对应类名为SupervisorStrategyConfigurator。倘若这个Guardian Actor扩大了失败,按照前面描述的Supervisor策略,它会使得root guardian终止该Actor,从而使得这个Actor下的所有子Actor都停止,即关掉了整个Actor系统。

2.The System Guardian

名为“/system”。主要是为了在常规Actor被终止时,做到按序的shut-down顺序。它可以监控User Guardian。可以管理Top-Level的System Actor采用一种策略,可以在除了ActorInitializationException与ActorKilledException之外的异常出现时,无限制地重启它。

3.The Root Guardian

由于每个真正的Actor都有一个supervisor,因此,root guardian的supervisor不是一个真正的Actor。

当出现如下三类失败错误时,就可能Restart Actor:

  • 在收到特定消息时,发生系统错误,如编程的错误;
  • 在处理消息时,因为一些外部资源的原因出现错误;
  • Actor的内部状态出现问题

Restart的过程:

  1. 暂停Actor(这意味着在Restart期间,不会处理常规的消息,直到它被Resume)。同时,还会递归地暂停所有的children;
  2. 调用旧实例的preRestart钩子方法(默认情况下,会发送终止消息给所有children,调用children的postStop())。
  3. 等待所有的children被终止(调用context.stop())。这个过程是非阻塞的;
  4. 通过调用原来提供的工厂去创建新的Actor实例;
  5. 调用新实例的postRestart()方法(默认情况下,仍然要先调用preStart());
  6. 将restart的请求发送给执行第3步时没有被kill掉的children;然后遵循第2步递归地对children执行restart;
  7. resume actor。

Lifecycle Monitoring

对于Monitoring而言,能监控的状态就是alive到dead之间的迁移。因此,在Akka中,Lifecycle Monitoring指的就是DeathWatch。Monitoring主要指的是监控其他的Actor,而非Supervision层次中的Actor。

监控的Actor(Monitoring Actor)如果接受到一条Terminated消息,默认行为就会抛出DeathPactException。要侦听Terminated消息,可以调用ActorContext.watch(targetActorRef);停止监听则调用ActorContext.unwatch(targetActorRef)。

如果Supervisor不能简单地重启其Children,又必须终止他们,例如在初始化Actor时出现了错误,就可以使用Monitoring。此时,可以侦听这些children,然后重新创建他们,或者安排时间重试。

使用Monitoring的另一种常见情形是,在缺乏外部资源,且该外部资源属于该Actor的一个children,Actor需要失败。如果第三方通过system.stop(child)或者发送PoisonPill去终止child,supervisor就会受到影响。

ScalaTest的测试风格

| Comments

ScalaTest几乎已经成为Scala语言默认的测试框架,而在JVM平台下,无论是否使用Scala进行开发,我认为仍有尝试ScalaTest的必要。这主要源于它提供了多种表达力超强的测试风格,能够满足各种层次的需求包括单元测试、BDD、验收测试、数据驱动测试。正如ScalaTest的创建者Bill Venners所说:

A guiding design principle of ScalaTest is that different people on a team should be able look at each others test code and know immediately what’s going on.

ScalaTest is designed to make it easy for you to customize your testing tool to meet your current needs, and for the built-in traits at least, make it easy for anyone who comes along later to read and understand your code.

UT与IT的风格选择

ScalaTest一共提供了七种测试风格,分别为:FunSuite,FlatSpec,FunSpec,WordSpec,FreeSpec,PropSpec和FeatureSpec。这就好像使用相同的原料做成不同美味乃至不同菜系的佳肴,你可以根据自己的口味进行选择。以我个人的偏好来看,我倾向于选择FlatSpec或FunSpec(类似Ruby下的RSpec)来编写单元测试与集成测试。虽然FunSuite的方式要更灵活,而且更符合传统测试方法的风格,区别仅在于test()方法可以接受一个闭包,但坏处恰恰就是它太灵活了。而FlatSpec和FunSpec则通过提供诸如it、should、describe等方法,来规定书写测试的一种模式,例如前者明显的“主-谓-宾”结构,后者清晰的分级式结构,都可以使团队的测试更加规范。如下是ScalaTest官方网站的提供的FunSuite、FlatSpec和FunSpec的三种风格样例。

//FunSuite
import org.scalatest.FunSuite

class SetSuite extends FunSuite {
  test("An empty Set should have size 0") {
      assert(Set.empty.size == 0)
  }
      test("Invoking head on an empty Set should produce NoSuchElementException") {
      intercept[NoSuchElementException] {
          Set.empty.head
      }
  }
}

//FlatSpec
import org.scalatest.FlatSpec

class SetSpec extends FlatSpec {
  "An empty Set" should "have size 0" in {
      assert(Set.empty.size == 0)
  }
      it should "produce NoSuchElementException when head is invoked" in {
      intercept[NoSuchElementException] {
          Set.empty.head
      }
  }
}

//FunSpec
import org.scalatest.FunSpec

class SetSpec extends FunSpec {
  describe("A Set") {
      describe("when empty") {
          it("should have size 0") {
              assert(Set.empty.size == 0)
          }
                  it("should produce NoSuchElementException when head is invoked") {
              intercept[NoSuchElementException] {
                  Set.empty.head
              }
          }
      }
  }
}

至于WordSpec和FreeSpec,要么太复杂,要么可读性稍差,要么惯用法风格有些混杂,个人认为都不是太好的选择,除非你已经习惯了这种风格。

数据驱动测试风格

JUnit对类似表数据的Fixture准备提供了Parameterized支持,但非常不直观,而且还需要为测试编写构造函数,然后定义一个带有@Parameters标记的静态方法。TestNG的DataProvider略好,但通过在测试方法上指定DataProvider的方式,仍然不尽如人意。ScalaTest提供的PropSpec充分利用了Scala函数式语言的特性,使得代码更简单,表达性也更强:

import org.scalatest._
import prop._
import scala.collection.immutable._

class SetSpec extends PropSpec with TableDrivenPropertyChecks with Matchers {
  val examples =
    Table(
      "set", BitSet.empty, HashSet.empty[Int], TreeSet.empty[Int]
    )
  property("an empty Set should have size 0") {
    forAll(examples) { set =>
      set.size should be(0)
    }
  }
  property("invoking head on an empty set should produce NoSuchElementException") {
    forAll(examples) { set =>
      a [NoSuchElementException] should be thrownBy { set.head }
    }
  }
}

验收测试风格

我们会推荐由PO(或者需求分析人员BA)与测试人员结对编写验收测试的业务场景,然后由开发人员和测试人员结对实现该场景。Cocumber、JBehave、Twist乃至Robot、Fitness都可以用于编写这样的验收测试(Fitness与Robot更接近实例化需求的方式)。这些工具有一个特点是业务场景与测试支持代码完全是分开的。例如Cucumber将业务场景放到feature文件中,而将测试支持代码放到rb文件中。JBehave类似。这样的好处是feature文件很干净,很纯粹,与技术实现没有任何关系,且有利于生成Living Document。然而,这种分离方式在带来良好可读性的同时,也带来维护成本的增加。

ScalaTest在提供类似Feature的验收测试Spec时,并没有将业务场景与测试支持代码分开,而是采用了混合的方式来表现:

import org.scalatest.{ShouldMatchers, GivenWhenThen, FeatureSpec}

class TVSetTest extends FeatureSpec with GivenWhenThen with ShouldMatchers{
  info("As a TV Set owner")
  info("I want to be able to turn the TV on and off")
  info("So I can watch TV when I want")
  info("And save energy when I'm not watching TV")

  feature("TV power button") {
    scenario("User press power button when TV is off") {
      Given("a TV set that is switched off")
      val tv = new TVSet
      tv.isOn should be (false)

      When("The power button is pressed")
      tv.pressPowerButton

      Then("The TV should switch on")
      tv.isOn should be (true)
    }
  }
}

ScalaTest的FeatureSpec支持常见的Given-When-Then模式。在上面的代码段中,info提供了对Feature的基本描述,然后提供了feature与scenario两个层级。熟悉Cucumber和JBehave的人对此应该不会陌生。测试支持代码直接写在Given、When、Then方法下,因而针对同一个Feature,只产生一个scala文件。这就意味着测试支持代码与自然语言描述是处于同一级的,准确地说,他们其实就属于同一个测试。开发时,PO(或者需求)与测试可以先编写FeatureSpec的骨架,即info-feature-scenario以及Given-When-Then部分。一旦编写好这个FeatureSpec,就可以提交到版本管理库。当开发人员与需求、测试一起Kick Off要做的Story时,就可以根据这个FeatureSpec进行,然后,要求开发人员在完成Story的实现前,与测试结对完成它的测试实现代码。

由于ScalaTest还提供了Tag等功能,我们还可以通过对测试提取基类或者Trait有效地对这些测试进行重用,保证测试代码的可维护性。由于只需要维护一个scala,成本会降低许多,也不需要在业务场景和测试支持代码之间跳转,降低维护的难度。唯一的缺点是它天然不支持Living Document。但是我们发现这些自然语言描述实则都集中在FeatureSpec提供的方法中,我们完全可以自行开发工具或插件,完成对场景描述以及步骤的提取,生成我们需要的文档。

目前,我的同事杨云已经将ScalaTest作为编写验收测试的工具引入到项目中。受他启发,在我当前的项目中也选择使用了ScalaTest作为验收测试的框架。考虑到IDE支持尤其是重构等方面的工具支持,以及构建中对测试运行、测试覆盖率检查等的支持,目前我并没有考虑在单元测试和集成测试中使用ScalaTest。之所以如此,还是源于对成本与收益的考量。

说明:文章的代码片段全部来自ScalaTest官方网站。

摘录《树上的男爵》

| Comments

卡尔维诺《树上的男爵》书写了这么一段难以言说、无可名状的美丽的段落,让我惊叹,甚至是那种心悸的喜悦。这样的笔下的景色,仿佛让我身临其境,又似乎另有一种魔力,如夕阳之下各色景观都涂上了一层金色的光亮。这一段落并非那种精致的美,但却自有一种纤细与磅礴,很奇怪的矛盾杂糅,然后蹦出一种奇异的美。

他的天地已经变了,这是一个由架在空中的细长而弯曲的桥,由粗糙树皮上的结节、瘤子和皱褶,由透过或疏或密的树叶挡起的帷幕而变幻着深浅的绿色阳光组成的世界,微风一吹,树叶的柄就抖动不已,而当树干摇摆时整棵树的叶子就像一方纱巾飘动起来。而我们的世界呢,是平贴在地面上的,我们看到的是比例失调的形象,我们当然不理解他在那上面的感受。夜里他倾听着树木如何用它的细胞在树干里记下代表岁月的年轮,树霉如何在北风中扩大斑点,在窝里熟睡的小鸟瑟缩着将脑袋钻进最暖和的翅膀下的羽毛里,毛毛虫蠕动,伯劳鸟腹中的蛋孕育成功。有的时候,原野静悄悄,耳膛内只有细微的响动,一声粗号,一声尖叫,一阵野草迅疾瑟瑟声,一阵流水淙淙响,一阵踏在泥土和石子上的蹄声,而蝉鸣声高出一切之上。响声一个接一个消失,听觉不断辨别出新的声音,就像那拆着一团毛线的手指,感觉到每根毛线变得越来越细,细得几乎感触不到了。同时青蛙一直在鸣唱,作为一种背景并不影响其它声音的传播,如同太阳光不因星星的不断闪烁而起变化。相反,每当风吹起或吹过,每一种声音都会起变化并成为新的声音,留在耳膛内最深处的只有隐隐约约的呼啸声或低吟声,那是大海。

要多么细致的观察,多么敏感的心灵,与大自然的脉搏一起跳动,放开身体的所有触觉、嗅觉与视觉,才能从灵魂中涌现出这样的文字,最后再借助一只魔笔润色,天然地凸显出来,就好像它自天地诞生以来一直就存在,只是从未有人发现,偶然的,被在天地之间嬉戏的卡尔维诺发现了。是的,是被发现了,而不是创作出来了。

读《被禁锢的头脑》

| Comments

米沃什在《被禁锢的头脑》中,描述了战时东欧的白色恐怖:

此时带着套马索的骑马者就会出现。那就是‘囚车’,即停在街角,用帆布遮盖着的大卡车。行人根本预见不到那里会有危险,当他们路过那个街角时,会突然感觉有一支枪顶住他。它也许会被关进集中营,或者会被推到墙下,用胶布封上嘴巴以防止他喊出反对占领国的口号,然后就被枪毙。这一切都令城市居民心惊胆战,迫使他们俯首帖耳。为了避免这种不幸,最好的办法就是足不出户。但是作为一家之主的父亲必须外出挣钱,弄点供养他的妻儿老小的面包和菜汤。每到晚上,家里人就开始心绪不宁,担心父亲是否还回得来。这种情况已经持续多年,所以人们渐渐觉得他们居住的这座城市简直就是危机四伏的原始森林,20世纪人的命运,与那些每天跟毒蛇猛兽作生死搏斗的穴居人的命运没什么两样。

没有经历过这种生活的人阅读这样的文字,仍然可以体会那种惶惶的恐怖,这种恐慌如慢性毒药一般啃啮人的心灵。与其这般在极大恐惧中担惊受怕,过着如丧家犬的生活,不知什么时候会抛尸街头或者入牢笼中受非人的折磨,真还不如扛起枪冲出战壕面对冷酷而喧嚣的枪林弹火呢。

可视化架构与DDD

| Comments

从DDD的角度,领域逻辑的分析可以运用战略方法Bounded Context。可是,一个问题是:如何获得Bounded Context ?

我查看了许多关于Bounded Context的书籍与文章,虽然都着重强调了它的重要性,也给出了一些实例,却对如何从需求——>Boundex Context这一点上语焉不详。

我的初步设想是通过绘制场景图(但并不成熟)。我认为有三种绘制场景图的方式:商业画布,体验地图和流程图。我认为,商业画布可以作为需求分析(尤其针对初创产品)的起点。商业画布如下图所示:

采用这种规范化的方式来推导商业模型,可以激发我们的灵感,理清我们的思路,以便我们思考为何要做这个产品,产品应该具备哪些功能。结合优点和缺点、成本等因素,我们可以藉此判断和决策功能的优先级,从而得到MVP。这个过程需要大量运用即时贴,让整个商业模型呈现。经过取舍后,就可以针对产品绘制场景图。此时,场景图可以采用Experience Map或流程图来体现。Experience Map的例子如下图所示:

由于商业画布本身提供了“客户”项,我们应该创建Persona,找准人物角色的特征来“搜寻”需求。绘制了场景图后,就能够确定用例了,此时,可辅以ATDD帮助确定Story。在确定了用例后,可以识别Bounded Context,并通过Context Map确定上下文之间的关系。