Kotlin DSL详解

DSL简介

所谓DSL领域专用语言(Domain Specified Language/ DSL),其基本思想是“求专不求全”,不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言。总的来说 DSL 是为了解决系统(包括硬件系统和软件系统)构建初期,使用者和构建者的语言模型不一致导致需求收集的困难。

举一个具体的例子来说。在构建证券交易系统的过程中,在证券交易活动中存在许多专业的金融术语和过程。现在要为该交易过程创建一个软件解决方案,那么开发者/构建者就必须了解证券交易活动,其中涉及到哪些对象、它们之间的规则以及约束条件是怎么样的。那么就让领域专家(这里就是证券交易专家)来描述证券交易活动中涉及的活动。但是领域专家习惯使用他们熟练使用的行业术语来表达,解决方案的构建者无法理解。如果解决方案的模型构建者要理解交易活动,就必须让领域专家用双方都能理解的自然语言来解释。这种解释的过程中,解决方案的模型构建者就理解了领域知识。这个过程中双方使用的语言就被称为“共同语言”。

共同语言称为解决方案模型构建者用来表达解决方案中的词汇的基础。构建者将这些共同语言对应到模型中,在程序中就是模块名、在数据模型中就是实体名、在测试用例中就是对象。在上面的描述,如果要成功构建模型,则需要一种领域专家和构建者(也就是通常的领域分析师/业务分析师)都能理解的“共同语言”。如果能够让领域专家通过简单的编程方式描述领域中的所有活动和规则,那么就能在一定程度上保证描述的完整性。DSL 就是为了解决这些问题而提出的。

常见的DSL

常见的DSL在很多领域都能看到,例如:

  • 软件构建领域 Ant
  • UI 设计师 HTML
  • 硬件设计师 VHDL

DSL 与通用编程语言的区别

  • DSL 供非程序员使用,供领域专家使用;
  • DSL 有更高级的抽象,不涉及类似数据结构的细节;
  • DSL 表现力有限,其只能描述该领域的模型,而通用编程语言能够描述任意的模型;

DSL分类

根据是否从宿主语言构建而来,DSL 分为:

  • 内部 DSL(从一种宿主语言构建而来)
  • 外部 DSL(从零开始构建的语言,需要实现语法分析器等)

Android Gradle构建

Groovy是一种运行在JVM虚拟机上的脚本语言,能够与Java语言无缝结合,如果想了解Groovy可以查看IBM-DeveloperWorks-精通Groovy。

打开Android的build.gradle文件,会看到类似下面的一些语法。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

通过上面的Android的build.gradle配置文件可以发现,buildscript里有配置了repositories和dependencies,而repositories和dependencies里面又可以配置各自的一些属性。可以看出通过这种形式的配置,我们可以层次分明的看出整个项目构建的一些定制,又由于Android也遵循约定大于配置的设计思想,因此我们仅仅只需修改需要自定义的部分即可轻松个性化构建流程。

Groovy脚本-build.gradle

在Groovy下,我们可以像Python这类脚本语言一样写个脚本文件直接执行而无需像Java那样既要写好Class又要定义main()函数,因为Groovy本身就是一门脚本语言,而Gradle是基于Groovy语言的构建工具,自然也可以轻松通过脚本来执行构建整个项目。作为一个基于Gradle的项目工程,项目结构中的settings.gradle和build.gradle这类xxx.gradle可以理解成是Gradle构建该工程的执行脚本,当我们在键盘上敲出gradle clean aDebug这类命令的时候,Gradle就会去寻找这类文件并按照规则先后读取这些gradle文件并使用Groovy去解析执行。

Groovy语法

要理解build.gradle文件中的这些DSL是如何被解析执行的,需要介绍Groovy的一些语法特点以及一些高级特性,下面从几个方面来介绍Groovy的一些特点。

链式命令

Groovy的脚本具有链式命令(Command chains)的特性,根据这个特性,当你在Groovy脚本中写出a b c d的时候,Groovy会翻译成a(b).c(d)执行,也就是将b作为a函数的形参调用,然后将d作为形参再次调用返回的实例(Instance)中的c方法。其中当做形参的b和d可以作为一个闭包(Closure)传递过去。例如:

// equivalent to: turn(left).then(right)
turn left then right

// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours

// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow

// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good

// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }

Groovy也支持某个方法传入空参数,但需要为该空参数的方法加上圆括号。例如:

// equivalent to: select(all).unique().from(names)
select all unique() from names

如果链式命令(Command chains)的参数是奇数,则最后一个参数会被当成属性值(Property)访问。例如:

// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies

操作符重载

有了Groovy的操作符重载(Operator overloading),==会被Groovy转换成equals方法,这样你就可以放心大胆地使用==来比较两个字符串是否相等了,在我们编写gradle脚本的时候也可以尽情使用。关于Groovy的所有操作符重载(Operator overloading)可以查阅:Operator overloading官方教程

委托

委托(DelegatesTo)可以说是Gradle选择Groovy作为DSL执行平台的一个重要因素了。通过委托(DelegatesTo)可以很简单的定制一个控制结构体(Custom control structures),例如下面的代码。

email {
    from '[email protected]'
    to '[email protected]'
    subject 'The pope has resigned!'
    body {
        p 'Really, the pope has resigned!'
    }
}

接下来可以看下解析上述DSL语言生成的代码。

def email(Closure cl) {
    def email = new EmailSpec()
    def code = cl.rehydrate(email, this, this)
    code.resolveStrategy = Closure.DELEGATE_ONLY
    code()
}

上述转换后的DSL语言,先定义了一个email(Closure)的方法,当执行上述步骤1的时候就会进入该方法内执行,EmailSpec是一个继承了参数中cl闭包里所有方法,比如from、to等等的一个类(Class),通过rehydrate方法将cl拷贝成一份新的实例(Instance)并赋值给code,code实例(Instance),通过rehydrate方法中设置delegate、owner和thisObject的三个属性将cl和email两者关联起来被赋予了一种委托关系,这种委托关系可以这样理解:cl闭包中的from、to等方法会调用到email委托类实例(Instance)中的方法,并可以访问到email中的实例变量(Field)。DELEGATE_ONLY表示闭包(Closure)方法调用只会委托给它的委托者(The delegate of closure),最后使用code()开始执行闭包中的方法。

Kotlin和anko进行Android开发

anko

Anko 是一个 DSL (Domain-Specific Language), 它是JetBrains出品的,用 Kotlin 开发的安卓框架。它主要的目的是用来替代以前XML的方式来使用代码生成UI布局。
下面看一下传统的xml界面实现布局文件。

<LinearLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_height="match_parent"
 android:layout_width="match_parent">

 <EditText
 android:id="@+id/todo_title"
 android:layout_width="match_parent"
 android:layout_heigh="wrap_content"
 android:hint="@string/title_hint" />

 <Button
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:text="@string/add_todo" />
LinearLayout>

使用Anko之后,可以用代码实现布局,并且button还绑定了点击事件的代码如下。

verticalLayout {
 var title = editText {
 id = R.id.todo_title
 hintResource = R.string.title_hint
 }
 button {
 textResource = R.string.add_todo
 onClick { view -> {
 // do something here
 title.text = "Foo"
 }
 }
 }
}

可以看到 DSL 的一个主要优点在于,它需要很少的代码即可理解和传达某个领域的详细信息。

OkHttp封装

OkHttp是一个成熟且强大的网络库,在Android源码中已经使用OkHttp替代原先的HttpURLConnection。很多著名的框架例如Picasso、Retrofit也使用OkHttp作为底层框架。本文使用Kotlin代码对它进行简单的封装,代码如下:

import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
import io.reactivex.schedulers.Schedulers
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import java.util.concurrent.TimeUnit


class RequestWrapper {

 var url:String? = null

 var method:String? = null

 var body: RequestBody? = null

 var timeout:Long = 10

 internal var _success: (String) -> Unit = { }
 internal var _fail: (Throwable) -> Unit = {}

 fun onSuccess(onSuccess: (String) -> Unit) {
 _success = onSuccess
 }

 fun onFail(onError: (Throwable) -> Unit) {
 _fail = onError
 }
}

fun http(init: RequestWrapper.() -> Unit) {
 val wrap = RequestWrapper()

 wrap.init()

 executeForResult(wrap)
}

private fun executeForResult(wrap:RequestWrapper) {

 Flowable.create<Response>({
 e -> e.onNext(onExecute(wrap))
 }, BackpressureStrategy.BUFFER)
 .subscribeOn(Schedulers.io())
 .subscribe(
 { resp ->
 wrap._success(resp.body()!!.string())
 },

 { e -> wrap._fail(e) })
}

private fun onExecute(wrap:RequestWrapper): Response? {

 var req:Request? = null
 when(wrap.method) {

 "get","Get","GET" -> req =Request.Builder().url(wrap.url).build()
 "post","Post","POST" -> req = Request.Builder().url(wrap.url).post(wrap.body).build()
 "put","Put","PUT" -> req = Request.Builder().url(wrap.url).put(wrap.body).build()
 "delete","Delete","DELETE" -> req = Request.Builder().url(wrap.url).delete(wrap.body).build()
 }

 val http = OkHttpClient.Builder().connectTimeout(wrap.timeout, TimeUnit.SECONDS).build()
 val resp = http.newCall(req).execute()
 return resp
}

封装完后,调用方式如下:

 http {
 url = "http://www.163.com/"
 method = "get"
 onSuccess {
 string -> L.i(string)
 }
 onFail {
 e -> L.i(e.message)
 }
 }

可以看到这种调用方式很像RxJava的流式风格,也很像前端的fetch请求。post的方式类似:

 var json = JSONObject()
 json.put("xxx","yyyy")
 ....

 val postBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"),json.toString())

 http {
 url = "https://......"
 method = "post"
 body = postBody
 onSuccess {
 string -> L.json(string)
 }

 onFail {
 e -> L.i(e.message)}
 }

封装图像处理框架

在Android开发时候,选择图片加载库,一般会选择一些比较常用,知名度比较高的库,这里介绍一款新的图像处理框架cv4j ,cv4j 支持使用滤镜。

 CV4JImage cv4jImage = new CV4JImage(bitmap);
 CommonFilter filter = new NatureFilter();
 Bitmap newBitMap = filter.filter(cv4jImage.getProcessor()).getImage().toBitmap();
 image.setImageBitmap(newBitMap);

如果使用的是rxJava方式,还可以这样用:

RxImageData.bitmap(bitmap).addFilter(new NatureFilter()).into(image);

下面是使用dsl的方式来封装。

package com.cv4j.rxjava

import android.app.Dialog
import android.graphics.Bitmap
import android.widget.ImageView
import com.cv4j.core.datamodel.CV4JImage
import com.cv4j.core.filters.CommonFilter

/**
 * only for Kotlin code,this class provides the DSL style for cv4j
 */
class Wrapper {

 var bitmap:Bitmap? = null

 var cv4jImage: CV4JImage? = null

 var bytes:ByteArray? = null

 var useCache:Boolean = true

 var imageView: ImageView? = null

 var filter: CommonFilter? = null

 var dialog: Dialog? = null
}

fun cv4j(init: Wrapper.() -> Unit) {
 val wrap = Wrapper()

 wrap.init()

 render(wrap)
}

private fun render(wrap: Wrapper) {

 if (wrap.bitmap!=null) {

 if (wrap.filter!=null) {
 RxImageData.bitmap(wrap.bitmap).dialog(wrap.dialog).addFilter(wrap.filter).isUseCache(wrap.useCache).into(wrap.imageView)
 } else {
 RxImageData.bitmap(wrap.bitmap).dialog(wrap.dialog).isUseCache(wrap.useCache).into(wrap.imageView)
 }

 } else if (wrap.cv4jImage!=null) {

 if (wrap.filter!=null) {
 RxImageData.image(wrap.cv4jImage).dialog(wrap.dialog).addFilter(wrap.filter).isUseCache(wrap.useCache).into(wrap.imageView)
 } else {
 RxImageData.image(wrap.cv4jImage).dialog(wrap.dialog).isUseCache(wrap.useCache).into(wrap.imageView)
 }
 } else if (wrap.bytes!=null) {

 if (wrap.filter!=null) {
 RxImageData.bytes(wrap.bytes).dialog(wrap.dialog).addFilter(wrap.filter).isUseCache(wrap.useCache).into(wrap.imageView)
 } else {
 RxImageData.bytes(wrap.bytes).dialog(wrap.dialog).isUseCache(wrap.useCache).into(wrap.imageView)
 }
 }

}

关于cv4j更多的介绍可以查看:cv4j官方介绍

你可能感兴趣的:(Kotlin,开发语言系列,DSL,Kotlin,Anko)