如何组织你的Scala代码

无关“设计模式”

核心是能否找到一些方式“优雅的支持依赖注入”,并且“利用Scala的丰富特性”,还“不准用框架”

如果…

  1. 如果我们的代码不使用依赖注入,直接new,代码结构就会简单随意到不需要考虑这些问题
  2. 如果scala特性不丰富(如同Java),那我们选择不多,就不用想太多了
  3. 如果使用某些DI框架,可沿用框架制定的规则(如Macwire, Scaldi),也不需要考虑太多

但是…

Scala中有:

  1. trait
  2. implicit
  3. 函数式
  4. Monad

我们是否能找到一种只利用语言本身特性,以尽量优雅的方式来实现依赖注入,同时让我们的代码拥有良好的结构。

取材

  1. 实际项目
    • Constructor parameters
    • Objects as functions
    • implicit method parameters
  2. 杨云的Scala培训代码
    • (simple cake) trait and self type annotation
  3. 考虑过但是未在项目中使用的
    • Reader monad

用例说明


为了实现该功能,我们将会访问网络,取回页面的html内容,然后对其包含的<img>标签进行解析,取出图片的地址。

(将会使用java/scala内置的或者第三方类库,简化实现)

如果不进行依赖注入

根据url取回网页html代码

import scala.sys.process._
import scala.util.Try

class PageFetcher {
  def fetch(url: String): Try[String] = Try {
    val output = new ByteArrayOutputStream
    (new URL(url) #> output).!!
    output.toString("UTF-8")
  }
}

从html代码中,找出所有图片地址

import org.jsoup.Jsoup
import org.jsoup.nodes.Element

class ImageExtractor {
  def extractImages(html: String): List[String] = {
    val doc = Jsoup.parse(html)
    val imgs = doc.select("img").listIterator()
               .asInstanceOf[JIterator[Element]].toList
    imgs.map(_.attr("src"))
  }
}

组合在一起:

class MyImageFinder {
  val pageFetcher = new PageFetcher
  val imageExtractor = new ImageExtractor

  def find(url: String): Try[List[String]] = {
    pageFetcher.fetch(url).map(html => imageExtractor.extractImages(html))
  }
}

注意pageFetcherimageExtractor都是直接new出来的,而不是注入进来的

问题

我们没有办法对MyImageFinder进行单元测试,只能:

  1. 提供一个真实的url,通过网络取回数据,进行测试
  2. 在本地启动一个http server

复杂、不可靠、甚至不可能

为了让我们更容易的编写单元测试并且利用Scala的各种豪华特性来体验Scala的强大灵活就让我们想尽各种办法来注入你的依赖吧

使用contructor parameters进行注入

这种熟悉的方式,我们在Java里经常这么干

class MyImageFinder(pageFetcher: PageFetcher, 
                    imageExtractor: ImageExtractor) {
  def find(url: String): Try[List[String]] = {
    pageFetcher.fetch(url).map(html => imageExtractor.extractImages(html))
  }
}

pageFetcherimageExtractor通过构造参数注入进来

在入口处组装

object Main {
  val pageFetcher = new PageFetcher
  val imageExtractor = new ImageExtractor
  val myImageFinder = new MyImageFinder(pageFetcher, imageExtractor)

  def main(args: Array[String]) {
    val url = args.head
    myImageFinder.find(url)
  }
}

(这样的入口类不用写单元测试)

测试

trait Mocking extends Scope {
  val pageFetcher = mock[PageFetcher]
  val imageExtractor = mock[ImageExtractor]
  val imageFinder = new MyImageFinder(pageFetcher, imageExtractor)
}

用一个trait,把各mock类圈起来,方便后面使用

class MyImageFinderSpec extends Specification with Mockito {

  "MyImageFinder" should {
    "return images successfully if ..." in new Mocking {

      pageFetcher.fetch("test-url") returns Success("some-html")
      imageExtractor.extractImages("some-html") returns List("a.png", "b.png")

      val images = imageFinder.find("test-url")

      images must beASuccessfulTry(List("a.png", "b.png"))
    }
  }

}

new Mocking中,可以直接使用前面定义的mock

总结

优点

问题

回顾及提问

  1. 不使用依赖注入
  2. 利用构造参数注入依赖

Objects as Functions

如果一个类中只有一个方法,为什么不把它变成一个函数?

通常的类定义是这样的(包含有一个或多个方法):

class PageFetcher {
  def fetch(url: String): Try[String] = Try {
    ...
  }
}

对于这里定义的fetch,我们要调用它的时候,要写成这样:

pageFetcher.fetch("some-url")

即:名词.动词(参数)

存在的问题

  1. 表达重复: pageFetcher.fetch
  2. 多个职责: obj.method1, obj.method2, obj.method3
  3. 无法很好利用scala对函数的支持,因为obj.method是方法

如何解决

每个class或者object都定义为函数类型

定义一个函数类型的FetchPage

object FetchPage extends (String => Try[String]) {
  def apply(url: String) = Try {
    val output = new ByteArrayOutputStream
    (new URL(url) #> output).!!
    output.toString("UTF-8")
  }
}

(后面详细讲解)

object FetchPage extends (String => Try[String]) {
    ...
}
  1. Scala中,每一个函数都有类型
  2. Scala定义了大量trait用来表示函数类型:
    • Function0[R]: () => R
    • Function1[P1,R]: P1 => R
    • Function2[P1,P2,R]: (P1, P2) => R
  3. String => Try[String]Function1[String, Try[String]]的语法糖
  4. 我们可以继承一个函数类型,它本身或者它的实例也将是一个函数

object FetchPage extends (String => Try[String]) {
  def apply(url: String) = Try {
    ...
  }
}
  1. 我们可以继承一个函数类型,实现其apply方法,提供自己的逻辑
  2. FetchPage是个函数,所以把它命名为一个动词
  3. 可直接调用FetchPage("some-url"),等价于FetchPage.apply("some-url")

object FetchPage extends (String => Try[String]) {
  def apply(url: String) = Try { ... }
}

FetchPage是一个如假包换的函数,意味着:

  1. 我们可以这样调用: FetchPage("some-url")
  2. 我们可以这样组合:FetchPage andThen (s:String)=>s.doSomething
  3. 我们可以依赖String => Try[String]这种通用的类型,而不依赖FetchPage这种具体的类型
  4. 我们可以方便的构造出一些函数帮助测试: _ => "some-html"

ExtractImages

object ExtractImages extends (String => List[String]) {
  def apply(html: String): List[String] = {
    val doc = Jsoup.parse(html)
    val imgs = doc.select("img").listIterator().asInstanceOf[JIterator[Element]].toList
    imgs.map(_.attr("src"))
  }
}

ExtractImages继承自String => List[String],实现了apply方法,它是一个函数

FindMyImages

class FindMyImages(fetchPage: String => Try[String], 
    extractImages: String => List[String]) 
    extends (String => Try[List[String]]) {
  def apply(url: String): Try[List[String]] = {
    fetchPage(url).map(extractImages)
  }
}

注意:FindMyImages依赖于通用的String => Try[String]类型,而不是具体的FetchPage类型

fetchPage: String => Try[String], extractImages: String => List[String]

由于fetchPageextractImages都是函数,我们在需要时可以调用它们的andThen/compose等方法,组合多个函数:

func1 andThen func2
func2 compose func1

(这里的例子没有体现)

组装

object MainObjectAsFunctions {

  val findMyImages = new FindMyImages(FetchPage, ExtractImages)

  def main(args: Array[String]) {
    val url = args.head
    findMyImages(url)
  }

}

直接把FetchPageExtractImage传进去,因为它们都是object

测试

class FindMyImagesSpec extends Specification {
  "MyImageFinder" should {
    "return a failure if pageFetcher can't get the page" in {
      val findMyImages = new FindMyImages(
        _ => Failure(new Throwable("test-connection-error")),
        _ => ???
      )
      val images = findMyImages("any-url")
      images must beAFailedTry.which(_.getMessage === "test-connection-error")
    }
  }
}

注意到那两个mock出来的函数了吗?

class FindMyImagesSpec extends Specification {

  "MyImageFinder" should {
    "return images successfully if ..." in {
      val findMyImages = new FindMyImages(
        { case "test-url" => Success("some-html-code-contains-images")},
        { case "some-html-code-contains-images" => List("a.png", "b.png")}
      )

      val images = findMyImages("test-url")

      images must beASuccessfulTry(List("a.png", "b.png"))
    }
  }

}

如果你要求传入特定的参数,使用{case =>}声明一个偏函数

问题:缺少线索

object FetchPage extends (String => Try[String])

object ExtractImages extends (String => List[String])

class FindMyImages(fetchPage: String => Try[String], 
    extractImages: String => List[String]) 
    extends (String => Try[List[String]])

这么多String => Try[String], String => List[String],我已经晕了

type alias 和 package object

package object some_package {
  type FetchPage = String => Try[String]
  type ExtractImages = String => List[String]
  type FindMyImages = String => Try[List[String]]
}

这里声明了一个package object,名为some_package。所有位于some_package下的类,可以直接访问这里定义的各type alias

package some_package

object FetchPage extends FetchPage
object ExtractImages extends ExtractImages
class WillFindMyImages(fetchPage: FetchPage, extractImages: ExtractImages)
    extends FindMyImages

这里我们可以引用前面定义的type alias了,看起来是不是清楚了许多?

总结

优点

问题

回顾及提问

  1. 不使用依赖注入
  2. 利用构造参数注入依赖
  3. 定义函数对象,把函数当依赖注入

你听过说Cake pattern吗

利用scala提供的traitself type annotation来注入依赖




http://jonasboner.com/2008/10/06/real-world-scala-dependency-injection-di/


This pattern is first explained in Martin Oderskys’ paper Scalable Component Abstractions (which is an excellent paper that is highly recommended) as the way he and his team structured the Scala compiler

Simple Cake

但是对于普通的项目来说,Cake Pattern过于复杂,所以出现了很多简化的方案,比如:


由于我们这里的方案与thin-cake有所不同,所以我使用了一个不同的名字simple-cake

来自“通过构造参数注入”的普通例子

class PageFetcher { 
    def fetch(url:String):Try[String] = ???
}
class ImageExtractor {
    def extractImages(html:String):List[String] = ???
}
class MyImageFinder(pageFetcher: PageFetcher, imageExtractor: ImageExtractor) {
    def find(url:String):Try[List[String]] = {
        pageFetcher.fetch(url).map(html => imageExtractor.extractImages(html))
    }
}

Simple Cake改写后的代码

trait PageFetcher { 
    def fetch(url:String):Try[String] = ???
}
trait ImageExtractor {
    def extractImages(html:String):List[String] = ???
}
trait MyImageFinder {
  self: PageFetcher with ImageExtractor =>
  def find(url: String): Try[List[String]] = {
    fetch(url).map(html => extractImages(html))
  }
}

特点

  1. 各小功能都使用trait定义
  2. 每个trait中只声明方法
  3. 通过self type annotation语法声明依赖:self: PageFetcher with ImageExtractor =>
  4. 直接调用依赖中的方法来实现新的功能
  5. 在入口处混入所有需要的trait(见下页)

组装

object Main extends MyImageFinder with PageFetcher with ImageExtractor {

  def main(args: Array[String]) {
    val url = args.head
    find(url)
  }
}

注意直接在Main上混入了所有的trait,不需要手动注入依赖

测试

"MyImageFinder" should {
  "return images successfully if ..." in {
    val imageFinder = new MyImageFinder with PageFetcher with ImageExtractor {
      override def fetch(url: String) = url match {
        case "test-url" => Success("some-html") }
      override def extractImages(html: String) = html match {
        case "some-html" => List("a.png", "b.png") }
    }
    val images = imageFinder.find("test-url")
    images must beASuccessfulTry(List("a.png", "b.png"))
  }
}

通过withoverride,不需要使用mockito

特点

优点

  1. 易实现、易理解
  2. 巧妙利用了scala的语言特性
  3. mock方式比较简单

问题

  1. 需要注意多个trait中是否同名方法(通过更有描述性的命名和测试来保证正确性)

回顾及提问

  1. 不使用依赖注入
  2. 利用构造参数注入依赖
  3. 定义函数对象,把函数当依赖注入
  4. 利用traitself type annotation注入依赖

通过方法参数注入依赖

我不喜欢向类的构造器中注入依赖,因为如果有多个方法,我不知道它们将被哪个方法调用

我希望依赖出现在方法的参数列表中

class MyService(dep1: Dep1, dep2: Dep2, dep3: Dep3) {
  def method1(s:String) = dep1.doIt(s)
  def method2(s:String) = dep2.doIt(dep3.doIt(s))
}

method1只使用了dep1method2只使用了dep2/dep3,但是我需要同时向MyService注入所有的依赖。

如果不看实现,我不知道method1method2有哪些依赖。

把依赖移到每个方法中

class MyService {
  def method1(s:String, dep1: Dep1) = dep1.doIt(s)
  def method2(s:String, dep2: Dep2, dep3: Dep3) = dep2.doIt(dep3.doIt(s))
}

MyService没有构造参数了,它们都移到了相应的方法中

存在的问题

  1. 很多方法可能都有长长的参数列表
  2. 如果一个方法调用多个方法,那么它需要提供所有需要的参数(更长了)
  3. 难以分清哪些参数是依赖,哪些是我们真正要处理的
  4. 向一个方法中增删依赖时,会递归影响所有调用它的方法(以及测试),修改量巨大
  5. 在调用一个方法时必须显式传入所有的依赖,影响阅读

curry 和 implicit

class MyService {
  def method1(s:String)(implicit dep1: Dep1) = dep1.doIt(s)
  def method2(s:String)(implicit dep2: Dep2, dep3: Dep3) = dep2.doIt(dep3.doIt(s))
}
  1. 第一个括号里是我们要处理的参数,第二个括号里是要注入的依赖
  2. 第二个括号标记为implicit,意味着我们有办法避免显式传入参数
  3. implicit作用于所有括号中的全部参数,并不只针对dep1dep2

PageFetcher

class PageFetcher {
  def fetch(url: String)(implicit dep1: Dep1, dep2: Dep2): Try[String] = Try {
    val output = new ByteArrayOutputStream
    (new URL(url) #> output).!!
    output.toString("UTF-8")
  }
}

为了突出依赖的个数,我额外定义了几个依赖Dep1, Dep2, Dep3。注意它们被声明为implicit

ImageExtractor

class ImageExtractor {
  def extractImages(html: String)(implicit dep2: Dep2, dep3: Dep3): List[String] = {
    val doc = Jsoup.parse(html)
    val imgs = doc.select("img").listIterator().asInstanceOf[JIterator[Element]].toList
    imgs.map(_.attr("src"))
  }
}

同样将dep2dep3声明为implicit

MyImageFinder

object MyImageFinder {
  def find(url: String)(implicit pageFetcher: PageFetcher, imageExtractor: ImageExtractor, dep1: Dep1, dep2: Dep2, dep3: Dep3): Try[List[String]] = {
    pageFetcher.fetch(url).map(html => imageExtractor.extractImages(html))
  }
}

注意,有一大堆参数都声明为implicit了。

由于dep1/dep2/dep3都声明为implicit了,我们不需要显式把它们传给pageFetcher.fetch(url)以及imageExtractor.extractImages(html),编译器会帮我们。

如果没有implicit,我们必须写成:

pageFetcher.fetch(url)(dep1,dep2)
  .map(html => imageExtractor.extractImages(html)(dep2,dep3))

注意注入的(dep1,dep2)(dep2,dep3)是否影响阅读?

组装

object Main {
  implicit val pageFetcher = new PageFetcher
  implicit val imageExtractor = new ImageExtractor
  implicit val dep1 = new Dep1
  implicit val dep2 = new Dep2
  implicit val dep3 = new Dep3

  def main(args: Array[String]) {
    var url = args.head
    MyImageFinder.find(url)
  }
}

依赖都声明为implicit,不需要显式传给find

测试

trait Mocking extends Scope {
  implicit val pageFetcher: PageFetcher = mock[PageFetcher]
  implicit val imageExtractor: ImageExtractor = mock[ImageExtractor]
  implicit val dep1: Dep1 = mock[Dep1]
  implicit val dep2: Dep2 = mock[Dep2]
  implicit val dep3: Dep3 = mock[Dep3]
}

注意:当我们在trait中声明implicit值时,最好显式给出类型,不然在某些情况下会因为scala的类型推断而报错

class MyImageFinderSpec extends Specification with Mockito {

  "MyImageFinder" should {
    "return images successfully if ..." in new Mocking {
      pageFetcher.fetch("test-url") returns Success("some-html")
      imageExtractor.extractImages("some-html") returns List("a.png", "b.png")

      val images = MyImageFinder.find("test-url")

      images must beASuccessfulTry(List("a.png", "b.png"))
    }
  }
}  

new Mocking内部调用fetchextractImagesfind时,不需要显式传入依赖

总结

优点

  1. 依赖声明在方法的参数列表中,方便查看当前方法的依赖
  2. 测试某方法时只需要给出它所需要的依赖

问题

  1. implicit难以追踪:可以从函数参数中找,也可以从所在的类及import中找
  2. 因为调用其它方法时不需要显式传入依赖,所以不知道哪个依赖被谁用了
  3. 难以知道某个依赖是否已经没有用,是否可以删掉

与“通过构造参数注入依赖”的方式相比,这种方式并没有带来更多的好处(不推荐)

改进方案:Reader monad?

回顾及提问

  1. 不使用依赖注入
  2. 利用构造参数注入依赖
  3. 定义函数对象,把函数当依赖注入
  4. 利用traitself type annotation注入依赖
  5. 通过方法的implicit参数注入依赖

我是函数式死忠,我要用Monad

试试Reader monad?

随便讲,随便听

Reader monad可看作是对“通过implicit注入依赖”的一种改进

在所有方法拥有一个共同的单一依赖的情况下,使用Reader看起来是一种很优雅的方案。

但是,一旦不同的方法有不同的依赖,或者有不同的返回值时,就会遇到各种各样的麻烦。

而解决方案对于不熟悉函数式及Manod的人(e.g. 我)来说,如同天书一般。

一个抄来的简单例子

来自于下面这篇讲解Reader的博客:

http://blog.originate.com/blog/2013/10/21/reader-monad-for-dependency-injection/

基础类

case class User(email: String, 
                supervisorId: Int, 
                firstName: String, 
                lastName: String)

trait UserRepository {
  def get(id: Int): User
  def find(username: String): User
}

有些方法需要依赖

import scalaz.Reader
trait Users {
  def getUser(id: Int) = Reader((userRepository: UserRepository) =>
    userRepository.get(id)
  )
  def findUser(username: String) = Reader((userRepository: UserRepository) =>
    userRepository.find(username)
  )
}

在需要注入依赖的地方,定义一个新的函数,用Reader(...)包起来,返回一个Reader monad.

object UserInfo extends Users {
  def userEmail(id: Int) = getUser(id) map (_.email)
  def userInfo(username: String) =
    for {
      user <- findUser(username)
      boss <- getUser(user.supervisorId)
    } yield Map(
      "fullName" -> s"${user.firstName} ${user.lastName}",
      "email" -> s"${user.email}",
      "boss" -> s"${boss.firstName} ${boss.lastName}"
    )
}

可以使用for表达式<-语法对Reader类型的值进行操作,条理清晰,且不需要传入依赖

注入依赖

val userRepository = getUserPepositoryFromSomeWhere()

UserInfo.userEmail(123)(userRepository)
UserInfo.userInfo("Freewind")(userRepository)

只需在最终使用的地方传入依赖即可。

避免了“implicit参数注入依赖”方式中的很多问题,但是只在这种极为简单的情况下看起来很简单。

当情况复杂的时候,会变得很复杂(见下页)

如果你想了解更多

  1. 介绍Reader的文章,也是前面例子的出处
  2. Functional programming in scala作者关于Reader的一个演讲视频
  3. 我遇到困难后,在stackoverflow上提的一些问题:

回顾及提问

  1. 不使用依赖注入
  2. 利用构造参数注入依赖
  3. 定义函数对象,把函数当依赖注入
  4. 利用traitself type annotation注入依赖
  5. 通过方法的implicit参数注入依赖
  6. Reader Monad

示例源代码

http://github.com/freewind/scala-code-organize

谢谢