《Kotlin 反应式编程》使用 RxKotlin 实现一个极简的 http DSL ( Reactive Programming Using Rx Kotlin )《Kotlin 反应式编程》使用

《Kotlin 反应式编程》使用 RxKotlin 实现一个极简的 http DSL Reactive Programming Using Rx Kotlin

https://github.com/ReactiveX/RxKotlin

RxKotlin: RxJava bindings for Kotlin

使用 RxKotlin 实现一个极简的 http DSL ( Reactive Programming Using Rx Kotlin )

我们现在已经基本知道 Kotlin 中 DSL 的样子了。但是这些 DSL 都是怎样实现的呢?本节我们就通过实现一个极简的http DSL来学习创建 DSL 背后的基本原理。

在这里我们对 OkHttp 做一下简单的封装,实现一个类似 jquery 中的 Ajax 的 http 请求的DSL。

OkHttp 是一个成熟且强大的网络库,在Android源码中已经使用OkHttp替代原先的HttpURLConnection。很多著名的框架例如Picasso、Retrofit也使用OkHttp作为底层框架。

提示: 更多关于OkHttp 的使用可参考: http://square.github.io/okhttp/

创建 Kotlin Gradle 项目

我们首先使用 IDEA 创建 Kotlin Gradle 项目

螢幕快照 2017-07-23 18.43.04.png

然后,在 build.gradle 里面配置依赖

    compile 'com.github.ReactiveX:RxKotlin:2.1.0'
    compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.8.1'
    compile group: 'com.alibaba', name: 'fastjson', version: '1.2.35'

其中,RxKotlin是ReactiveX 框架对 Kotlin 语言的支持库。我们这里主要用RxKotlin来进行请求回调的异步处理。

我们使用的是 'com.github.ReactiveX:RxKotlin:2.1.0' , 这个库是在 https://jitpack.io 上,所以我们在repositories配置里添加 jitpack 仓库

repositories {
    maven { url 'https://jitpack.io' }
    ...
}

RxKotlin

ReactiveX是Reactive Extensions的缩写,一般简写为Rx,最初是LINQ的一个扩展,由微软的架构师Erik Meijer领导的团队开发,在2012年11月开源。

Rx扩展了观察者模式用于支持数据和事件序列。Rx是一个编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步I/O(非阻塞)数据流。

Rx库支持.NET、JavaScript和C++ 。Rx近几年越来越流行,现在已经支持几乎全部的流行编程语言了。一个语言列表如下所示:

Rx 支持的编程语言

项目主页

Java

RxJava : https://github.com/ReactiveX/RxJava

JavaScript

RxJS:https://github.com/ReactiveX/rxjs

C#

Rx.NET:https://github.com/Reactive-Extensions/Rx.NET

C#(Unity)

UniRx:https://github.com/neuecc/UniRx

Scala

RxScala:https://github.com/ReactiveX/RxScala

Clojure

RxClojure:https://github.com/ReactiveX/RxClojure

C++

RxCpp:https://github.com/Reactive-Extensions/RxCpp

Lua

RxLua:https://github.com/bjornbytes/RxLua

Ruby

Rx.rb:https://github.com/Reactive-Extensions/Rx.rb

Python:

RxPY:https://github.com/ReactiveX/RxPY

Go

RxGo:https://github.com/ReactiveX/RxGo

Groovy

RxGroovy:https://github.com/ReactiveX/RxGroovy

JRuby

RxJRuby:https://github.com/ReactiveX/RxJRuby

Kotlin

RxKotlin:https://github.com/ReactiveX/RxKotlin

Swift

RxSwift:https://github.com/kzaher/RxSwift

PHP

RxPHP:https://github.com/ReactiveX/RxPHP

Elixir

reaxive:https://github.com/alfert/reaxive

Dart

RxDart:https://github.com/ReactiveX/rxdart

Rx的大部分语言库由ReactiveX这个组织负责维护。Rx 比较流行的库有RxJava/RxJS/Rx.NET等,当然未来RxKotlin也必将更加流行。

提示: Rx 的社区网站是: http://reactivex.io/ 。 Github 地址:https://github.com/ReactiveX/

Http请求对象封装类

首先我们设计Http请求对象封装类如下

class HttpRequestWrapper {

    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 success(onSuccess: (String) -> Unit) {
        success = onSuccess
    }

    fun error(onError: (Throwable) -> Unit) {
        fail = onError
    }
}

HttpRequestWrapper的成员变量和函数说明如下表

成员

说明

url

请求 url

method

请求方法,例如 Get、Post 等,不区分大小写

body

请求头,为了简单起见我们直接使用 OkHttp的RequestBody类型

timeout

超时时间ms,我们设置了默认值是10s

success

请求成功的函数变量

fail

请求失败的函数变量

fun success(onSuccess: (String) -> Unit)

请求成功回调函数

fun error(onError: (Throwable) -> Unit)

请求失败回调函数

http 执行引擎

我们直接调用 OkHttp 的 Http 请求 API

private fun call(wrap: HttpRequestWrapper): Response {

    var req: Request? = null
    when (wrap.method?.toLowerCase()) {
        "get" -> req = Request.Builder().url(wrap.url).build()
        "post" -> req = Request.Builder().url(wrap.url).post(wrap.body).build()
        "put" -> req = Request.Builder().url(wrap.url).put(wrap.body).build()
        "delete" -> req = Request.Builder().url(wrap.url).delete(wrap.body).build()
    }

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

它返回请求的响应对象Response。

我们在OkHttpClient.Builder().connectTimeout(wrap.timeout, TimeUnit.MILLISECONDS).build()中设置超时时间的单位是 TimeUnit.MILLISECONDS

我们通过wrap.method?.toLowerCase()处理请求方法的大小写的兼容。

使用 RxKotlin 完成请求响应的异步处理

我们首先新建一个数据发射源:一个可观察对象(Observable),作为发射数据用

    val sender = Observable.create<Response>({
        e ->
        e.onNext(call(wrap))
    })

其中,e 的类型是 io.reactivex.Emitter (发射器),它的接口定义是

public interface Emitter<T> {
    void onNext(@NonNull T value);
    void onError(@NonNull Throwable error);
    void onComplete();
}

其方法功能简单说明如下:

方法

功能

onNext

发射一个正常值数据(value)

onError

发射一个Throwable异常

onComplete

发射一个完成的信号

这里,我们通过调用onNext方法,把 OkHttp 请求之后的响应对象Response 作为正常值发射出去。

然后我们再创建一个数据接收源:一个观察者(Observer)

    val receiver: Observer<Response> = object : Observer<Response> {
        override fun onNext(resp: Response) {
            wrap.success(resp.body()!!.string())
        }

        override fun onError(e: Throwable) {
            wrap.fail(e)
        }

        override fun onSubscribe(d: Disposable) {
        }

        override fun onComplete() {
        }

    }

receiver 的 onNext 函数接收 sender 发射过来的数据 Response, 然后我们在函数体内,调用这个响应对象,给 wrap.success 回调函数进行相关的赋值操作。同样的,onError 函数中也执行相应的赋值操作。

最后,通过 subscribe 订阅函数来绑定 sender 与 receiver 的关联:

sender.subscribe(receiver)

作为接收数据的 receiver (也就是 观察者 (Observer) ),对发送数据的 sender (也就是可被观察对象( Observable)) 所发射的数据或数据序列作出响应。

这种模式可以极大地简化并发操作,因为它创建了一个处于待命状态的观察者,在未来某个时刻响应 sender 的通知,而不需要阻塞等待 sender 发射数据。这个很像协程中的通道编程模型。

DSL主函数 ajax

我们的ajax DSL主函数设计如下:

fun ajax(init: HttpRequestWrapper.() -> Unit) {
    val wrap = HttpRequestWrapper()
    wrap.init()
    doCall(wrap)
}

其中,参数init: HttpRequestWrapper.() -> Unit 是一个带接收者的函数字面量,它的类型是init = Function1<com.kotlin.easy.HttpRequestWrapper, kotlin.Unit>。 HttpRequestWrapper是扩展函数init()的接收者,点号 . 是扩展函数修饰符。

我们在函数体内直接调用了这个函数字面量 wrap.init() 。这样的写法可能比较难以理解,这个函数字面量 init 的调用实际上是 init.invoke(wrap) ,就是把传入 ajax 的函数参数直接传递给 wrap 。为了更简单的理解这个 init 函数的工作原理,我们通过把上面的 ajax 函数的代码反编译成对应的 Java 代码如下:

   public static final void ajax(@NotNull Function1 init) {
      Intrinsics.checkParameterIsNotNull(init, "init");
      HttpRequestWrapper wrap = new HttpRequestWrapper();
      init.invoke(wrap);
      doCall(wrap);
   }

也就是说,ajax 函数的一个更容易理解的写法是

fun ajax(init: HttpRequestWrapper.() -> Unit) {
    val wrap = HttpRequestWrapper()
    init.invoke(wrap)
    doCall(wrap)
}

我们在实际应用的时候,可以直接把 init 写成Lambda 表达式的形式,因为接收者类型HttpRequestWrapper 可以从上下文推断出来。

我们这样调用 ajax 函数:

ajax {
    url = testUrl
    method = "get"
    success {
        string ->
        println(string)
        Assert.assertTrue(string.contains("百度一下"))
    }
    error {
        e ->
        println(e.message)
    }
}

下面是几个测试代码示例:

package com.kotlin.easy

import com.alibaba.fastjson.JSONObject
import okhttp3.MediaType
import okhttp3.RequestBody
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4

/**
 * Created by jack on 2017/7/23.
 */

@RunWith(JUnit4::class)
class KAjaxTest {

    @Test fun testHttpOnSuccess() {
        val testUrl = "https://www.baidu.com"
        ajax {
            url = testUrl
            method = "get"
            success {
                string ->
                println(string)
                Assert.assertTrue(string.contains("百度一下"))
            }
            error {
                e ->
                println(e.message)
            }
        }

    }

    @Test fun testHttpOnError() {
        val testUrl = "https://www2.baidu.com"

        ajax {
            url = testUrl
            method = "get"
            success {
                string ->
                println(string)
            }
            error {
                e ->
                println(e.message)
                Assert.assertTrue("connect timed out" == e.message)
            }
        }
    }


    @Test fun testHttpPost() {
        var json = JSONObject()
        json.put("name", "Kotlin DSL Http")
        json.put("owner", "Kotlin")
        val postBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json.toString())
        ajax {
            url = "saveArticle"
            method = "post"
            body = postBody
            success {
                string ->
                println(string)
            }
            error {
                e ->
                println(e.message)
            }
        }
    }


    @Test fun testLambda() {
        val testUrl = "https://www.baidu.com"

        val init: HttpRequestWrapper.() -> Unit = {
            this.url = testUrl
            this.method = "get"
            this.success {
                string ->
                println(string)
                Assert.assertTrue(string.contains("百度一下"))
            }
            this.error {
                e ->
                println(e.message)
            }
        }
        ajax(init)
    }

到这里,我们已经完成了一个极简的 Kotlin Ajax DSL。

本节工程源码: https://github.com/EasyKotlin/chatper14_kotlin_dsl_http

本章小结

相比于Java,Kotlin对函数式编程的支持更加友好。Kotlin 的扩展函数和高阶函数(Lambda 表达式),为定义Kotlin DSL提供了核心的特性支持。

使用DSL的代码风格,可以让我们的程序更加直观易懂、简洁优雅。如果使用Kotlin来开发项目的话,我们完全可以去尝试一下。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏进击的程序猿

The Clean Architecture in PHP 读书笔记(二)

设计模式是对软件中通用问题的总结,有了设计模式,方便我们进行交流,譬如一说MVC,我们就知道是怎么回事了,不然我们必须巴拉巴拉一大堆话去描述,不易于传播、交流,...

794
来自专栏程序员的诗和远方

20180930_ARTS_week14

想到这个的思路是因为解的过程中,发现如果遇到左边的,需要存起来,后面可能有用(类似入栈),遇到右边的,需要比对前一个值,并且比完如果匹配就没用了(这类似出栈)。...

822
来自专栏WindCoder

Java设计模式学习笔记—建造者模式

文章最后“Java设计模式笔记示例代码整合”为本系列代码整合,所有代码均为个人手打并运行测试,不定期更新。本节内容位于其Builder包(package)中。

822
来自专栏十月梦想

node通过路由获取不同用户信息

具体功能:使用不同url判断是老师或者学生,老师的工号4-6位,学生学号8-10位,否则提示学号不正确,

774
来自专栏Kirito的技术分享

八个层面比较 Java 8, RxJava, Reactor

这是一篇译文,原文出处(http://alexsderkach.io/comparing-java-8-rxjava-reactor/)。其实很久以前我就看完了...

7196
来自专栏NetCore

[原创]Fluent NHibernate之旅(四)-- 关系(下)

最近一直忙着准备去旅行的东东,所以进度慢下来了,明天就要出发了,嘿嘿,在出发前,把多对多给写完吧。如果你第一次看这个系列,可以先看看先前几篇,了解下。 一、开篇...

21110
来自专栏后端技术探索

php进阶

基本数据类型和数组都为真复制,即为真副本,当属性为对象时,为假复制,改变副本仍会影响原对象.解决方案:

1361
来自专栏后端技术探索

业务id转密文短链的一种实现思路

买家通过电商app下单后,会受到一条短信,短信内容中包括改订单详情页面的h5地址连接,因为是出现在短信中,所以对连接有要求:1.尽量短;2.安全性考虑,订单在数...

953
来自专栏晨星先生的自留地

一次freebuf溯源引发的木马后门分析

1864
来自专栏游戏开发那些事

【Unity游戏开发】Lua中的os.date和os.time函数

  最近马三在工作中经常使用到了lua 中的 os.date( ) 和 os.time( )函数,不过使用的时候都是不得其解,一般都是看项目里面怎么用,然后我就...

1394

扫码关注云+社区