面向对象六大基本原则实战:从零重构支持引擎切换的网络框架

引言

我们以封装网络框架的例子来讲解面向对象的六大基本原则,来逐步解决其中出现的种种问题。

原始代码与初步封装的痛点

以 OkHttp 获取 Github 某个用户的所有仓库为例,首先是没有进行任何封装的代码:

引入 OkHttp 依赖:

kotlin 复制代码
implementation("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()
    }
}

最后是执行请求的地方,为了遵循单一职责原则,我们将之前依赖的 OkHttpClientFileHttpCache 作为参数传入。

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")
    }
)

链式构建 - 优化参数调用

当支持的功能越来越多,方法的参数也就越来越长,导致一些不必要的参数必须手动传入 nullfalse 才行。我们利用链式调用来解决这个问题。

就是将请求参数的配置放到一个中间载体上,该载体每调用一个配置方法后都会返回自身,从而实现链式配置。

构建器的核心职责是收集纯粹的业务请求数据,不应该涉及第三方网络框架中的类。收集完成后,会产出一个数据模型,我们只需将这个数据模型转交给统一的分发中心去执行即可。

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 内部依然强依赖了 OkHttpClientokhttp3.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,传入给 HttpUtilsFileHttpCache 实例,我们可以随时替换为 MemoryHttpCacheDatabaseHttpCache

那只要实现了接口就是里氏替换(也就是多态)吗?

不一定,灵魂在于"要遵守行为契约"。子类在替换父类时,不能破坏父类原有的行为预期

例如上层预期 readCache 在没有缓存时返回 null,如果你在子类找不到缓存时,自作聪明地抛出了一个异常导致程序崩溃。

即使它实现了 ICache 接口,但也严重违背了里氏替换原则,因为它改变了父类定义的行为预期。

依赖倒置原则 - 依赖抽象接口

依赖倒置原则(Dependency Inversion Principle,简称 DIP),它的核心是高层模块无需关心低层模块的实现细节,实现依赖的是抽象。

想必大家在 MVMM 架构中,已经有看到过 "契约定义"、"依赖倒置" 相关的字眼。

它主要是为了模块间的解耦,在经过上面的改造,我们的 HttpUtils 完美体现了 DIP:

依赖 ICache 抽象,而不是具体的 FileHttpCache

依赖 INetworkEngineIRequest 抽象,不含有任何 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 方法之后。

相关推荐
Sylvia33.2 小时前
从轮询到推送:足球数据API架构演进与火星数据技术拆解
java·服务器·网络·python·websocket·架构
Wang's Blog4 小时前
AI Agent白手起家60: 多智能体架构解析与 LangGraph 入门
人工智能·架构·wpf
天空之城--4 小时前
Android Flutter行业动态与学习参考(2026年8月)
android·flutter
某林2125 小时前
从“仿真能跑”到“真机能走”:一套轮腿机械狗强化学习系统的工程化实践
人工智能·stm32·嵌入式硬件·架构·机器人·机械狗
quantdash_cc5 小时前
基于 QuantDash 5 分钟 K 线的网格交易策略参数网格搜索寻优实战
android·开发语言·pandas·量化·quantdash
cxr8285 小时前
从“对象本体”到“关系本体”
人工智能·架构
FII工业富联科技服务6 小时前
制造业AI规模化落地架构:从Task Agent到Factory Agent Brain的三级智能体演进解析
大数据·人工智能·架构
敲代码的玉米C6 小时前
我修的那个 bug,制造了另一个 bug
前端·人工智能·架构
杉氧6 小时前
原生交互:如何在 Flutter 中嵌入 Android/iOS 原生组件并解决手势冲突
android·flutter·dart