如何组织你的Scala代码
无关“设计模式”
核心是能否找到一些方式“优雅的支持依赖注入”,并且“利用Scala的丰富特性”,还“不准用框架”
无关“设计模式”
核心是能否找到一些方式“优雅的支持依赖注入”,并且“利用Scala的丰富特性”,还“不准用框架”
new
,代码结构就会简单随意到不需要考虑这些问题Macwire
, Scaldi
),也不需要考虑太多Scala中有:
trait
implicit
函数式
Monad
我们是否能找到一种只利用语言本身特性,以尽量优雅的方式来实现依赖注入,同时让我们的代码拥有良好的结构。
trait
and self type annotation
为了实现该功能,我们将会访问网络,取回页面的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))
}
}
注意pageFetcher
和imageExtractor
都是直接new
出来的,而不是注入进来的
我们没有办法对MyImageFinder
进行单元测试,只能:
复杂、不可靠、甚至不可能
这种熟悉的方式,我们在Java里经常这么干
class MyImageFinder(pageFetcher: PageFetcher,
imageExtractor: ImageExtractor) {
def find(url: String): Try[List[String]] = {
pageFetcher.fetch(url).map(html => imageExtractor.extractImages(html))
}
}
pageFetcher
与imageExtractor
通过构造参数注入进来
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
优点
问题
如果一个类中只有一个方法,为什么不把它变成一个函数?
通常的类定义是这样的(包含有一个或多个方法):
class PageFetcher {
def fetch(url: String): Try[String] = Try {
...
}
}
对于这里定义的fetch
,我们要调用它的时候,要写成这样:
pageFetcher.fetch("some-url")
即:名词.动词(参数)
pageFetcher.fetch
obj.method1
, obj.method2
, obj.method3
obj.method
是方法每个class
或者object
都定义为函数类型
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]) {
...
}
Function0[R]
: () => R
Function1[P1,R]
: P1 => R
Function2[P1,P2,R]
: (P1, P2) => R
String => Try[String]
是Function1[String, Try[String]]
的语法糖object FetchPage extends (String => Try[String]) {
def apply(url: String) = Try {
...
}
}
apply
方法,提供自己的逻辑FetchPage
是个函数,所以把它命名为一个动词FetchPage("some-url")
,等价于FetchPage.apply("some-url")
object FetchPage extends (String => Try[String]) {
def apply(url: String) = Try { ... }
}
FetchPage
是一个如假包换的函数,意味着:
FetchPage("some-url")
FetchPage andThen (s:String)=>s.doSomething
String => Try[String]
这种通用的类型,而不依赖FetchPage
这种具体的类型_ => "some-html"
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
方法,它是一个函数
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]
由于fetchPage
和extractImages
都是函数,我们在需要时可以调用它们的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)
}
}
直接把FetchPage
和ExtractImage
传进去,因为它们都是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]
,我已经晕了
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了,看起来是不是清楚了许多?
优点
问题
type alias
有哪些子类,跳转不便type alias
的声明通常放在package object
中,而不能放在实现类的旁边,不方便查看(Scala不支持在top level声明type alias)利用scala提供的trait
和self 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
但是对于普通的项目来说,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))
}
}
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))
}
}
trait
定义trait
中只声明方法self type annotation
语法声明依赖:self: PageFetcher with ImageExtractor =>
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"))
}
}
通过with
和override
,不需要使用mockito
优点
问题
trait
和self 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
只使用了dep1
,method2
只使用了dep2/dep3
,但是我需要同时向MyService
注入所有的依赖。
如果不看实现,我不知道method1
和method2
有哪些依赖。
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
没有构造参数了,它们都移到了相应的方法中
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))
}
implicit
,意味着我们有办法避免显式传入参数implicit
作用于所有括号中的全部参数,并不只针对dep1
或dep2
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
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"))
}
}
同样将dep2
和dep3
声明为implicit
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
内部调用fetch
、extractImages
和find
时,不需要显式传入依赖
优点
问题
implicit
难以追踪:可以从函数参数中找,也可以从所在的类及import
中找与“通过构造参数注入依赖”的方式相比,这种方式并没有带来更多的好处(不推荐)
改进方案:Reader monad
?
trait
和self type annotation
注入依赖试试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参数注入依赖”方式中的很多问题,但是只在这种极为简单的情况下看起来很简单。
当情况复杂的时候,会变得很复杂(见下页)
trait
和self type annotation
注入依赖