引言
我们以封装网络框架的例子来讲解面向对象的六大基本原则,来逐步解决其中出现的种种问题。
原始代码与初步封装的痛点
以 OkHttp 获取 Github 某个用户的所有仓库为例,首先是没有进行任何封装的代码:
引入 OkHttp 依赖:
kotlinimplementation("com.squareup.okhttp3:okhttp:5.4.0") implementation("com.squareup.okhttp3:logging-interceptor:5.4.0") // 日志拦截器
kotlin
/**
* OkHttpClient 实例
*/
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder().apply {
connectTimeout(10, TimeUnit.SECONDS)
readTimeout(10, TimeUnit.SECONDS)
// 添加日志拦截器
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
this.addInterceptor(loggingInterceptor)
}.build()
}
/**
* 获取 Github 仓库
*/
fun fetchGithubRepos(username: String = "InsertKoinIO") {
val originUrl =
"https://api.github.com/users/${username}/repos"
val httpUrl = originUrl.toHttpUrlOrNull() ?: run {
Log.e("OkHttp", "Invalid URL: $originUrl")
return
}
val finalUrl = httpUrl.newBuilder()
.addQueryParameter("type", "owner")
.addQueryParameter("direction", "desc")
.addQueryParameter("sort", "updated")
.addQueryParameter("per_page", "3")
.addQueryParameter("page", "1")
.build()
val request = Request.Builder()
.url(finalUrl)
.header("Accept", "application/vnd.github.v3+json")
.get()
.tag("github_repos") // 用于页面销毁时取消请求
.build()
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e("OkHttp", "Request failed", e)
}
override fun onResponse(call: Call, response: Response) {
response.use { resp ->
if (resp.isSuccessful) {
val jsonStr = resp.body.string()
Log.d("OkHttp", "Response:\n$jsonStr")
} else {
Log.e("OkHttp", "Request failed code: ${resp.code}")
}
}
}
})
}
这段代码有什么问题?
如果每处都这样写,当需要增加一个共有参数时,必须要去修改代码中所有进行网络请求的地方。同时对于公共参数,每次调用时都要临时进行添加。
如果需要对结果的成功判断进行调整,也需要穿梭各个页面去修改代码。
此时,我们可以进行一些简单的封装、抽取公共逻辑:创建一个 get 函数,将公共参数的拼接、Request 对象的创建放到函数内部,并使用统一的成功/失败回调。
kotlin
object HttpUtils {
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder().apply {
connectTimeout(10, TimeUnit.SECONDS)
readTimeout(10, TimeUnit.SECONDS)
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
this.addInterceptor(loggingInterceptor)
}.build()
}
/**
* 通用GET请求封装
* @param baseUrl 原始url字符串
* @param queryParams 查询参数
* @param headers 请求头
* @param cache 是否缓存
* @param tag 请求标记,用于取消
* @param onSuccess 成功回调,已经拿到response字符串
* @param onFailure 失败回调,异常+错误码
*/
fun okHttpGet(
baseUrl: String,
queryParams: Map<String, String> = emptyMap(),
headers: Map<String, String> = emptyMap(),
cache: Boolean = false,
tag: Any? = null,
onSuccess: (String) -> Unit,
onFailure: (code: Int?, e: IOException?) -> Unit
) {
val httpUrl = baseUrl.toHttpUrlOrNull() ?: run {
onFailure(null, null)
Log.e("OkHttp", "Invalid URL: $baseUrl")
return
}
val urlBuilder = httpUrl.newBuilder()
queryParams.forEach { (k, v) ->
urlBuilder.addQueryParameter(k, v)
}
val finalUrl = urlBuilder.build()
if (cache) {
TODO("先省略缓存逻辑")
}
val requestBuilder = Request.Builder()
.url(finalUrl)
.get()
.tag(tag)
headers.forEach { (k, v) ->
requestBuilder.header(k, v)
}
val request = requestBuilder.build()
okHttpClient.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
onFailure(null, e)
}
override fun onResponse(call: Call, response: Response) {
response.use { resp ->
if (resp.isSuccessful) {
val bodyStr = resp.body.string()
onSuccess(bodyStr)
} else {
onFailure(resp.code, null)
}
}
}
})
}
}
调用示例
kotlin
fun fetchGithubRepos(username: String = "InsertKoinIO") {
val originUrl = "https://api.github.com/users/${username}/repos"
val queryMap = mapOf(
"type" to "owner",
"direction" to "desc",
"sort" to "updated",
"per_page" to "3",
"page" to "1"
)
val headerMap = mapOf(
"Accept" to "application/vnd.github.v3+json"
)
okHttpGet(
baseUrl = originUrl,
queryParams = queryMap,
headers = headerMap,
cache = false,
tag = "github_repos",
onSuccess = { jsonStr ->
Log.d("OkHttp", "Response:\n$jsonStr")
},
onFailure = { code, e ->
if (e != null) {
Log.e("OkHttp", "Request error", e)
} else {
Log.e("OkHttp", "Http fail code:$code")
}
}
)
}
但这远远不够,代码耦合在了一起,没有任何的扩展性可言。如果想要替换某个功能的实现,会非常复杂。
单一职责原则 - 拆分请求与缓存
面对团在一起的代码,就需要用到单一职责原则(Single Responsibility Principle,简称 SRP)。
简单来说,就是一个类只负责完成一件事,不要让类变得臃肿。
我们来进行拆分上述类,拆分为执行请求的 HttpUtils,承载请求数据的 RequestModel,以及用于缓存的 FileHttpCache。
首先是缓存:
kotlin
/**
* 极简磁盘缓存
*/
class FileHttpCache(context: Context, private val cacheValidMs: Long) {
/**
* 缓存目录
*/
private val cacheRoot = File(context.cacheDir, "http_cache")
.apply { if (!exists()) mkdirs() }
/**
* url转md5文件名,避开非法字符
*/
private fun url2File(url: String): File {
val md5 = MessageDigest.getInstance("MD5")
val digest = md5.digest(url.toByteArray(StandardCharsets.UTF_8))
val fileName = digest.joinToString("") { "%02x".format(it) }
return File(cacheRoot, fileName)
}
/**
* 写入缓存
*/
fun writeCache(url: String, responseText: String) {
val file = url2File(url)
// 过期时间
val expireTime = System.currentTimeMillis() + cacheValidMs
FileOutputStream(file).use {
val buf = ByteBuffer.allocate(8)
buf.putLong(expireTime)
it.write(buf.array()) // 写入过期时间
it.write(responseText.toByteArray(StandardCharsets.UTF_8)) // 写入响应内容
}
}
/**
* 读取缓存
*/
fun readCache(finalUrl: String): String? {
val file = url2File(finalUrl)
if (!file.exists()) return null
return try {
FileInputStream(file).use { fis ->
val expireBuf = ByteArray(8)
fis.read(expireBuf) // 读取过期时间
val expireTime = ByteBuffer.wrap(expireBuf).long
if (System.currentTimeMillis() > expireTime) {
// 过期
file.delete()
return null
}
fis.readBytes().toString(StandardCharsets.UTF_8)
}
} catch (_: Exception) {
file.delete()
null
}
}
}
然后是请求数据模型:
kotlin
class RequestModel(
val baseUrl: String,
val queryParams: Map<String, String> = emptyMap(),
val headers: Map<String, String> = emptyMap(),
val tag: Any? = null
) {
/**
* 获取最终完整的url字符串,用于网络请求和缓存key
*/
fun getFinalUrl(): String? {
val uri = baseUrl.toUri()
// 简单校验
val scheme = uri.scheme
if (scheme.isNullOrBlank() ||
!(scheme.equals("http", ignoreCase = false) ||
scheme.equals("https", ignoreCase = false))
) {
return null
}
if (uri.host.isNullOrBlank()) {
return null
}
val builder = uri.buildUpon()
queryParams.forEach { (k, v) ->
builder.appendQueryParameter(k, v)
}
return builder.build().toString()
}
}
最后是执行请求的地方,为了遵循单一职责原则,我们将之前依赖的 OkHttpClient 和 FileHttpCache 作为参数传入。
kotlin
// 全局单例线程池,专门用于磁盘IO
private val diskIoExecutor = Executors.newSingleThreadExecutor()
class HttpUtils(
private val okHttpClient: OkHttpClient,
private val httpCache: FileHttpCache?
) {
/**
* 执行OkHttp GET请求
*/
fun okHttpGet(
baseUrl: String,
queryParams: Map<String, String> = emptyMap(),
headers: Map<String, String> = emptyMap(),
cache: Boolean = false,
tag: Any? = null,
onSuccess: (String) -> Unit,
onFailure: (code: Int?, e: IOException?) -> Unit
) {
val requestModel = RequestModel(baseUrl, queryParams, headers, tag)
val finalUrl = requestModel.getFinalUrl()
if (finalUrl.isNullOrEmpty()) {
onFailure(null, null)
return
}
// 开启缓存,子线程读取缓存
if (cache && httpCache != null) {
diskIoExecutor.execute {
val cacheContent = httpCache.readCache(finalUrl)
if (!cacheContent.isNullOrEmpty()) {
// 命中缓存
onSuccess(cacheContent)
} else {
executeNetworkCall(requestModel, finalUrl, true, onSuccess, onFailure)
}
}
return
}
// 未命中缓存 / 不开启缓存
executeNetworkCall(requestModel, finalUrl, cache, onSuccess, onFailure)
}
private fun executeNetworkCall(
requestModel: RequestModel,
finalUrl: String,
cache: Boolean = false,
onSuccess: (String) -> Unit,
onFailure: (code: Int?, e: IOException?) -> Unit
) {
// 在这里组装 OkHttp 的 Request
val requestBuilder = Request.Builder().url(finalUrl).get().tag(requestModel.tag)
requestModel.headers.forEach { (k, v) -> requestBuilder.header(k, v) }
okHttpClient.newCall(requestBuilder.build()).enqueue(
object : Callback {
override fun onFailure(call: Call, e: IOException) {
onFailure(null, e)
}
override fun onResponse(call: Call, response: Response) {
response.use { resp ->
if (resp.isSuccessful) {
val bodyStr = resp.body.string()
if (cache && httpCache != null) {
// 写缓存
httpCache.writeCache(finalUrl, bodyStr)
}
onSuccess(bodyStr)
} else {
onFailure(resp.code, null)
}
}
}
}
)
}
}
调用示例
kotlin
// 创建缓存,有效时长5分钟
val cache = FileHttpCache(this@MainActivity, 1000 * 60 * 5L)
// 演示,所以才临时创建,否则应该作为单例
val okHttpClient = OkHttpClient.Builder().apply {
connectTimeout(10, TimeUnit.SECONDS)
readTimeout(10, TimeUnit.SECONDS)
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
this.addInterceptor(loggingInterceptor)
}.build()
val httpUtils = HttpUtils(okHttpClient, cache)
val username = "InsertKoinIO"
val originUrl = "https://api.github.com/users/${username}/repos"
val queryMap = mapOf(
"type" to "owner",
"direction" to "desc",
"sort" to "updated",
"per_page" to "3",
"page" to "1"
)
val headerMap = mapOf(
"Accept" to "application/vnd.github.v3+json"
)
httpUtils.okHttpGet(
baseUrl = originUrl,
queryParams = queryMap,
headers = headerMap,
cache = true,
tag = "github_repos",
onSuccess = {
Log.d("HttpUtils", "onSuccess: $it")
},
onFailure = { code, e ->
Log.d("HttpUtils", "onFailure: $code, $e")
}
)
链式构建 - 优化参数调用
当支持的功能越来越多,方法的参数也就越来越长,导致一些不必要的参数必须手动传入 null 或 false 才行。我们利用链式调用来解决这个问题。
就是将请求参数的配置放到一个中间载体上,该载体每调用一个配置方法后都会返回自身,从而实现链式配置。
构建器的核心职责是收集纯粹的业务请求数据,不应该涉及第三方网络框架中的类。收集完成后,会产出一个数据模型,我们只需将这个数据模型转交给统一的分发中心去执行即可。
kotlin
class HttpUtils(
private val okHttpClient: OkHttpClient,
private val httpCache: FileHttpCache?
) {
inner class GetCall internal constructor() {
private var baseUrl: String = ""
private val queryParams = mutableMapOf<String, String>()
private val headers = mutableMapOf<String, String>()
private var cache: Boolean = false
private var tag: Any? = null
fun baseUrl(url: String): GetCall {
this.baseUrl = url
return this
}
fun queryParam(key: String, value: String): GetCall {
queryParams[key] = value
return this
}
fun queryParams(map: Map<String, String>): GetCall {
queryParams.putAll(map)
return this
}
fun header(key: String, value: String): GetCall {
headers[key] = value
return this
}
fun headers(map: Map<String, String>): GetCall {
headers.putAll(map)
return this
}
fun cache(enable: Boolean): GetCall {
this.cache = enable
return this
}
fun tag(tagObj: Any?): GetCall {
this.tag = tagObj
return this
}
/**
* 统一触发请求
*/
fun call(onSuccess: (String) -> Unit, onFailure: (code: Int?, e: Exception?) -> Unit) {
// 构建纯净的数据模型
val requestModel = RequestModel(baseUrl, queryParams, headers, tag)
val finalUrl = requestModel.getFinalUrl()
if (finalUrl.isNullOrEmpty()) {
onFailure(null, IllegalArgumentException("Invalid URL"))
return
}
// 将数据模型交给调度中心
dispatchRequest(requestModel, finalUrl, cache, onSuccess, onFailure)
}
}
// 开启链式调用
fun get(): GetCall = GetCall()
// 单例线程池,专门用于磁盘IO
private val diskIoExecutor = Executors.newSingleThreadExecutor()
/**
* 统一的缓存与网络调度逻辑
*/
private fun dispatchRequest(
requestModel: RequestModel,
finalUrl: String,
cache: Boolean,
onSuccess: (String) -> Unit,
onFailure: (code: Int?, e: Exception?) -> Unit
) {
if (cache && httpCache != null) {
diskIoExecutor.execute {
val cacheContent = httpCache.readCache(finalUrl)
if (!cacheContent.isNullOrEmpty()) {
onSuccess(cacheContent)
} else {
executeNetworkCall(requestModel, finalUrl, true, onSuccess, onFailure)
}
}
return
}
executeNetworkCall(requestModel, finalUrl, cache, onSuccess, onFailure)
}
// ... 省略executeNetworkCall函数 ...
}
当然完善的链式调用需要考虑异常路径,例如,当用户没有传入请求路径时,我们在执行 call() 时可以抛出异常来提示用户。
调用示例
kotlin
val cache = FileHttpCache(this@MainActivity, 1000 * 60 * 5L)
val okHttpClient = OkHttpClient.Builder().apply {
connectTimeout(10, TimeUnit.SECONDS)
readTimeout(10, TimeUnit.SECONDS)
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
this.addInterceptor(loggingInterceptor)
}.build()
val httpUtils = HttpUtils(okHttpClient, cache)
httpUtils.get()
.baseUrl("https://api.github.com/users/InsertKoinIO/repos")
.queryParam("type", "owner")
.queryParam("sort", "updated")
.cache(true)
.tag("github_req")
.call(
onSuccess = { result ->
// 注意:此时依然在子线程
Log.d("HttpUtils", "Response:\n$result")
},
onFailure = { code, ex ->
Log.e("HttpUtils", "Error: $code", ex)
}
)
开闭原则 - 网络引擎切换
现在还有问题吗?
当然有,我们现在的 HttpUtils 内部依然强依赖了 OkHttpClient 和 okhttp3.Request。如果此时需要换一个网络框架,比如切到 Ktor。
我们不仅要去改动网络调用的地方,还可能要改上层的业务。
这就涉及到了开闭原则(Open-Closed Principle,简称 OCP),对扩展开放,对修改关闭。当有新需求时,不应该去改动已经有的旧代码,而是应该通过写新代码来扩展功能。
在实践中,就是通过抽象 和接口来完成,底层细节交由子类实现。
为了彻底解耦网络引擎,我们设计了三组接口:缓存接口 ICache,请求数据接口 IRequest,以及网络引擎接口 INetworkEngine。
1. ICache:
kotlin
/**
* 抽象的缓存接口,方便扩展内存 / 数据库 / 磁盘缓存
*/
interface ICache {
fun readCache(key: String): String?
fun writeCache(key: String, content: String)
}
// 让之前的 FileHttpCache 实现该接口即可
2. IRequest:
纯粹的业务请求抽象(
RequestModel)我们已经实现过了,这里简单修改一下即可。
kotlin
/**
* 纯粹的业务请求抽象
*/
interface IRequest {
fun getFinalUrl(): String?
fun getMethod(): String
fun getHeaders(): Map<String, String>
fun getTag(): Any?
}
class GetHttpRequest(
private val baseUrl: String,
private val queryParams: Map<String, String> = emptyMap(),
private val headers: Map<String, String> = emptyMap(),
private val tag: Any? = null
) : IRequest {
override fun getMethod() = "GET"
override fun getHeaders() = headers
override fun getTag() = tag
override fun getFinalUrl(): String? {
// ... 省略逻辑 ...
}
}
3. INetworkEngine:
kotlin
interface INetworkEngine {
fun execute(
request: IRequest,
onSuccess: (String) -> Unit,
onFailure: (code: Int?, e: Exception?) -> Unit
)
}
/**
* 具体的 OkHttp 引擎实现,将底层细节完全隔离在这里
*/
class OkHttpEngine(private val okHttpClient: OkHttpClient) : INetworkEngine {
override fun execute(
request: IRequest,
onSuccess: (String) -> Unit,
onFailure: (code: Int?, e: Exception?) -> Unit
) {
val finalUrl =
request.getFinalUrl() ?: return onFailure(null, IllegalArgumentException("Invalid URL"))
val builder = Request.Builder().url(finalUrl).tag(request.getTag())
request.getHeaders().forEach { (k, v) -> builder.header(k, v) }
if (request.getMethod() == "GET") {
builder.get()
}
okHttpClient.newCall(builder.build()).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
onFailure(null, e)
}
override fun onResponse(call: Call, response: Response) {
response.use { resp ->
if (resp.isSuccessful) {
onSuccess(resp.body.string())
} else {
onFailure(resp.code, null)
}
}
}
})
}
}
最后是我们的 HttpUtils,它已经完全脱离了底层,只负责指挥调度:
kotlin
/**
* 链式构建器基类
*/
abstract class BaseRequestCall {
protected var baseUrl: String = ""
protected val queryParams = mutableMapOf<String, String>()
protected val headers = mutableMapOf<String, String>()
protected var cache: Boolean = false
protected var tag: Any? = null
fun baseUrl(url: String): BaseRequestCall {
this.baseUrl = url
return this
}
fun queryParam(key: String, value: String): BaseRequestCall {
queryParams[key] = value
return this
}
fun queryParams(map: Map<String, String>): BaseRequestCall {
queryParams.putAll(map)
return this
}
fun header(key: String, value: String): BaseRequestCall {
headers[key] = value
return this
}
fun headers(map: Map<String, String>): BaseRequestCall {
headers.putAll(map)
return this
}
fun cache(enable: Boolean): BaseRequestCall {
this.cache = enable
return this
}
fun tag(tagObj: Any?): BaseRequestCall {
this.tag = tagObj
return this
}
/**
* 核心:由子类实现,产出纯净的业务请求数据模型
*/
protected abstract fun createHttpRequest(): IRequest
fun call(onSuccess: (String) -> Unit, onFailure: (code: Int?, e: Exception?) -> Unit) {
val requestModel = createHttpRequest()
HttpUtils.instance.dispatchRequest(requestModel, cache, onSuccess, onFailure)
}
}
// GET 请求构建子类
class GetCall : BaseRequestCall() {
override fun createHttpRequest(): IRequest {
return GetHttpRequest(baseUrl, queryParams, headers, tag)
}
}
为了方便全局调用和统一管理引擎,我们将 HttpUtils 改造成了单例模式:
kotlin
class HttpUtils(
private val engine: INetworkEngine,
private val cache: ICache?
) {
companion object {
lateinit var instance: HttpUtils
fun init(engine: INetworkEngine, cache: ICache?) {
instance = HttpUtils(engine, cache)
}
}
// 请求入口
fun get(): GetCall = GetCall()
// 可以随时扩展 POST 请求
// fun post(): PostCall = PostCall()
/**
* 统一请求分发中心:负责调度缓存与网络,完全不关心具体实现细节
*/
fun dispatchRequest(
request: IRequest,
enableCache: Boolean,
onSuccess: (String) -> Unit,
onFailure: (code: Int?, e: Exception?) -> Unit
) {
val finalUrl = request.getFinalUrl()
if (finalUrl.isNullOrEmpty()) {
onFailure(null, IllegalArgumentException("Invalid URL"))
return
}
// 1. 处理缓存逻辑
if (enableCache && cache != null) {
diskIoExecutor.execute {
val cacheContent = cache.readCache(finalUrl)
if (!cacheContent.isNullOrEmpty()) {
onSuccess(cacheContent)
} else {
executeNetworkAndCache(request, finalUrl, true, onSuccess, onFailure)
}
}
return
}
// 2. 无缓存,直接走网络
executeNetworkAndCache(request, finalUrl, false, onSuccess, onFailure)
}
/**
* 执行网络请求并回写缓存
*/
private fun executeNetworkAndCache(
request: IRequest,
finalUrl: String,
shouldCache: Boolean,
onSuccess: (String) -> Unit,
onFailure: (code: Int?, e: Exception?) -> Unit
) {
engine.execute(
request,
onSuccess = { result ->
// 请求成功后,子线程异步写入缓存
if (shouldCache && cache != null) {
// 虽然该回调通常位于子线程,可以直接执行,但是为了统一,还是交给专用的单例线程池去处理
diskIoExecutor.execute { cache.writeCache(finalUrl, result) }
}
onSuccess(result)
},
onFailure = { code, e ->
onFailure(code, e)
}
)
}
}
现在的调用和之前基本没差:
kotlin
HttpUtils.init(
OkHttpEngine(okHttpClient),
FileHttpCache(this@MainActivity, 1000 * 60 * 5L)
)
HttpUtils.instance.get()
.baseUrl("https://api.github.com/users/InsertKoinIO/repos")
.queryParam("type", "owner")
.cache(true)
.tag(this)
.call({ result ->
// 成功
}, { code, err ->
// 失败
})
但是扩展性大幅增强了,如果想换 Kotr。没问题!立马就换。
只需增加 KtorEngine : INetworkEngine,然后在 init 中进行替换,一行业务代码都不用改!
里氏替换原则 - 遵守行为契约
里氏替换原则(LSP)的重点在于,所有用到父类 / 抽象接口的地方必须能使用子类透明地进行替换。
是不是有点熟悉,其实它正是实现"开闭原则"的重要方式,我们刚刚也有运用。
比如前面定义的 ICache,传入给 HttpUtils 的 FileHttpCache 实例,我们可以随时替换为 MemoryHttpCache 或 DatabaseHttpCache。
那只要实现了接口就是里氏替换(也就是多态)吗?
不一定,灵魂在于"要遵守行为契约"。子类在替换父类时,不能破坏父类原有的行为预期。
例如上层预期 readCache 在没有缓存时返回 null,如果你在子类找不到缓存时,自作聪明地抛出了一个异常导致程序崩溃。
即使它实现了 ICache 接口,但也严重违背了里氏替换原则,因为它改变了父类定义的行为预期。
依赖倒置原则 - 依赖抽象接口
依赖倒置原则(Dependency Inversion Principle,简称 DIP),它的核心是高层模块无需关心低层模块的实现细节,实现依赖的是抽象。
想必大家在 MVMM 架构中,已经有看到过 "契约定义"、"依赖倒置" 相关的字眼。
它主要是为了模块间的解耦,在经过上面的改造,我们的 HttpUtils 完美体现了 DIP:
依赖 ICache 抽象,而不是具体的 FileHttpCache。
依赖 INetworkEngine 和 IRequest 抽象,不含有任何 OkHttp 的类。
这就是真正的依赖倒置,高层不再被底层的具体框架所"绑架"。
接口隔离原则 - 细化接口职责
接口隔离原则(Interface Segregation Principle,简称 ISP),将臃肿的接口进行拆分为专用的小接口,只放功能、职责相近的接口。
例如缓存接口中就没有多余的方法,职责就是做缓存的读写。如果后续需要增加缓存清理逻辑,应该新增 ICacheExpire 接口,而不是往 ICache 中塞方法导致接口膨胀。
ISP 还可以提升框架的易用性,在我们之前的代码中,网络请求回来后是在子线程中的,如果我们在回调中直接操作 UI 就会导致崩溃。
为了解决这个问题,我们可以增加一个负责线程调度的接口 IDispatcher:
kotlin
/**
* 线程调度抽象
*/
interface IDispatcher {
fun dispatch(block: () -> Unit)
}
// Android 环境下的主线程切换
class MainThreadDispatcher : IDispatcher {
private val handler = Handler(Looper.getMainLooper())
override fun dispatch(block: () -> Unit) {
handler.post(block)
}
}
在 init 时注入即可,同时将 onSuccess 和 onFailure 调用替换为下面的结果派发函数。
kotlin
/**
* 结果派发:将回调安全地切换到目标线程(如 Android 主线程)
*/
private fun deliverSuccess(result: String, onSuccess: (String) -> Unit) {
dispatcher?.dispatch { onSuccess(result) } ?: onSuccess(result)
}
private fun deliverFailure(
code: Int?,
e: Exception?,
onFailure: (code: Int?, e: Exception?) -> Unit
) {
dispatcher?.dispatch { onFailure(code, e) } ?: onFailure(code, e)
}
调用示例
less
HttpUtils.init(
OkHttpEngine(okHttpClient),
FileHttpCache(this@MainActivity, 1000 * 60 * 5L),
MainThreadDispatcher()
)
HttpUtils.instance.get()
.baseUrl("https://api.github.com/users/InsertKoinIO/repos")
.queryParam("type", "owner")
.header("Accept", "application/vnd.github.v3+json")
.cache(true)
.tag(this)
.call({ result ->
// 可以在这放心更新UI
Log.d("HttpUtils", "current thread name: ${Thread.currentThread().name}")
}, { code, err ->
})
这样,我们就将线程切换这个独立的职责通过小接口隔离了出来。在 Android 环境下注入 MainThreadDispatcher,在测试环境下,可以注入 SyncDispatcher(即直接执行),非常灵活易于外部使用。
迪米特法则 - 隐藏底层细节
最少知识原则(Least Knowledge Principle,简称 LKP),也叫迪米特法则(Law of Demeter, LoD)。
一个对象应该尽可能少了解其他对象,不需要知道其他对象中的底层细节,只需要了解公开的接口即可。
在前面的链式构建器中,使用方完全无法看到底层细节,我们不知道URL是怎么拼接的,不知道缓存是怎么存储的,不知道网络到底用的是 OkHttp 还是 HttpURLConnection。
这一切,都被完美地隐藏在了一个 call 方法之后。