Просмотр исходного кода

refactor(R4.3): 解开 ReadBookConfig 与排版 gateway 的双向依赖

`ReadBookConfig` 是排版数据的全局存储,却持有 `readStyleGateway`,在
`clearMissingTextFont()` 里反过来命令领域层改样式并落盘;而 gateway 的实现
`ReadBookStyleConfigRepository` 又反过来读 `ReadBookConfig` 一百多处。存储与领域层
互指,权威归属说不清,两边也都没法单独测。

那条反向引用**只服务这一个方法**。把 `clearMissingTextFont()` 移进
`ReadStyleGateway`(实现落在 `ReadBookStyleConfigRepository`,body 就是它自己原本被
远程调用的那两步),消费方 `ChapterProvider.getTypeface` 的 getOrElse 分支改成直接
取 gateway——那里已经在用 `ReadConfig`(同样走 Koin),风格一致,且是字体文件丢失的
错误路径,多一次 Koin 查找无所谓。

于是 `ReadBookConfig` 删掉 `lateinit readStyleGateway`、`attachGateway()`、
`clearMissingTextFont()` 与三个 domain import,`App.onCreate` 少一行
`attachGateway(get())`。方向变成单向:
ChapterProvider → ReadStyleGateway → ReadBookStyleConfigRepository → ReadStyleRepository,
`ReadBookConfig` 只向下依赖 `ReadStyleRepository`(文件读写)。

**未动**:`readSettingsGateway` 保留——那是只读的上游设置,不构成环;护栏也只锁
`ReadStyleGateway`,不一刀切禁所有 gateway。gateway 实现读 `ReadBookConfig` 的一百多处
仍在,属 R4.1。

护栏 `ReadBookConfigDependencyDirectionTest` 注入回归反证过会红。
全量单测 438 → 439。

回归清单:把当前排版方案的字体设成一个随后删掉的字体文件,重新排版应回落系统字体
且字体设置被清空(这是唯一走到 clearMissingTextFont 的路径);另外确认开书、切换
排版方案、导入导出配置一切正常(App.onCreate 的初始化少了一步)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5151561 3 месяцев назад
Родитель
Сommit
1c5d7382ee

+ 0 - 1
app/src/main/java/io/legado/app/App.kt

@@ -123,7 +123,6 @@ class App : Application(), ImageLoaderFactory {
             readStyleRepository = get(),
             readSettingsGateway = get(),
         )
-        ReadBookConfig.attachGateway(get())
         if (legacyLanguage != null) {
             get<AppLocaleGateway>().migrateLegacyLanguage(legacyLanguage)
         }

+ 5 - 0
app/src/main/java/io/legado/app/data/repository/ReadBookStyleConfigRepository.kt

@@ -80,6 +80,11 @@ class ReadBookStyleConfigRepository(
         publishState()
     }
 
+    override fun clearMissingTextFont() {
+        updateCurrentStyle(ReadStyleMutation.StringValue(ReadStyleStringKey.TextFont, ""))
+        save()
+    }
+
     override fun applyPreset(index: Int): Boolean {
         val preset = DefaultData.readConfigs.getOrNull(index) ?: return false
         val copy = GSON.fromJsonObject<ReadBookConfig.Config>(GSON.toJson(preset)).getOrNull()

+ 5 - 0
app/src/main/java/io/legado/app/domain/gateway/ReadStyleGateway.kt

@@ -19,6 +19,11 @@ interface ReadStyleGateway {
      */
     fun notifyModeChanged()
     fun updateCurrentStyle(mutation: ReadStyleMutation)
+
+    /**
+     * 排版时发现当前字体文件已不存在——清空字体设置并落盘,下次排版回落到系统字体。
+     */
+    fun clearMissingTextFont()
     fun applyPreset(index: Int): Boolean
     fun addStyle(): Int
     fun deleteCurrentStyle(): Boolean

+ 0 - 15
app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt

@@ -10,9 +10,6 @@ import io.legado.app.constant.PageAnim
 import io.legado.app.data.entities.HighlightRule
 import io.legado.app.data.repository.ReadStyleRepository
 import io.legado.app.domain.gateway.ReadSettingsGateway
-import io.legado.app.domain.gateway.ReadStyleGateway
-import io.legado.app.domain.gateway.ReadStyleMutation
-import io.legado.app.domain.gateway.ReadStyleStringKey
 import io.legado.app.help.DefaultData
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.ReadSessionState
@@ -30,7 +27,6 @@ import java.io.InputStream
 @Keep
 object ReadBookConfig {
     private lateinit var readStyleRepository: ReadStyleRepository
-    private lateinit var readStyleGateway: ReadStyleGateway
     private lateinit var readSettingsGateway: ReadSettingsGateway
     private val readSettings get() = readSettingsGateway.currentSettings
 
@@ -44,10 +40,6 @@ object ReadBookConfig {
         initShareConfig()
     }
 
-    internal fun attachGateway(readStyleGateway: ReadStyleGateway) {
-        this.readStyleGateway = readStyleGateway
-    }
-
     // region Tip position constants
     const val tipNone = 0
     const val tipChapterTitle = 1
@@ -148,13 +140,6 @@ object ReadBookConfig {
         }
     }
 
-    fun clearMissingTextFont() {
-        readStyleGateway.updateCurrentStyle(
-            ReadStyleMutation.StringValue(ReadStyleStringKey.TextFont, "")
-        )
-        readStyleGateway.save()
-    }
-
     fun getAllPicBgStr(): ArrayList<String> {
         return readStyleRepository.getAllPicBgStr(configList)
     }

+ 3 - 1
app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt

@@ -14,6 +14,7 @@ import androidx.core.os.postDelayed
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.help.book.BookContent
+import io.legado.app.domain.gateway.ReadStyleGateway
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig.dottedBase
 import io.legado.app.help.config.ReadBookConfig.dottedRatio
@@ -29,6 +30,7 @@ import io.legado.app.utils.isPad
 import io.legado.app.utils.spToPx
 import io.legado.app.utils.textHeight
 import kotlinx.coroutines.CoroutineScope
+import org.koin.core.context.GlobalContext
 import splitties.init.appCtx
 import java.io.File
 
@@ -987,7 +989,7 @@ object ChapterProvider {
                 }
             }
         }.getOrElse {
-            ReadBookConfig.clearMissingTextFont()
+            GlobalContext.get().get<ReadStyleGateway>().clearMissingTextFont()
             Typeface.SANS_SERIF
         } ?: Typeface.DEFAULT
     }

+ 63 - 0
app/src/test/java/io/legado/app/help/config/ReadBookConfigDependencyDirectionTest.kt

@@ -0,0 +1,63 @@
+package io.legado.app.help.config
+
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import java.io.File
+
+/**
+ * R4.3 —— `ReadBookConfig` 与 `ReadStyleGateway` 之间的依赖必须单向。
+ *
+ * 原来是个环:`ReadBookConfig`(排版数据的全局存储)持有 `readStyleGateway` 并在
+ * `clearMissingTextFont()` 里反过来命令它改样式、落盘;而 gateway 的实现
+ * `ReadBookStyleConfigRepository` 又大量读 `ReadBookConfig`。存储反向命令拥有它的领域层,
+ * 谁是权威就说不清了,也让两边都没法单独测。
+ *
+ * 现在方向是单向的:消费方(`ChapterProvider`)直接找 gateway,`ReadBookConfig` 只向下
+ * 依赖 `ReadStyleRepository`(文件读写)。
+ *
+ * 注意 `readSettingsGateway` 不在此列——那是**只读**上游设置,不构成环。
+ */
+class ReadBookConfigDependencyDirectionTest {
+
+    @Test
+    fun `ReadBookConfig 不反向依赖 ReadStyleGateway`() {
+        val source = stripComments(
+            mainSourceFile("io/legado/app/help/config/ReadBookConfig.kt").readText()
+        )
+        val violations = buildList {
+            if (Regex("""\bReadStyleGateway\b""").containsMatchIn(source)) {
+                add("引用了 ReadStyleGateway")
+            }
+            if (Regex("""\bReadStyleMutation\b""").containsMatchIn(source)) {
+                add("引用了 ReadStyleMutation(在向领域层下达样式变更)")
+            }
+            if (Regex("""\battachGateway\b""").containsMatchIn(source)) {
+                add("又出现了 attachGateway 这类事后回填的反向引用")
+            }
+        }
+        assertTrue(
+            "ReadBookConfig 又反向依赖排版 gateway 了:${violations.joinToString()}。\n" +
+                "排版存储不该命令拥有它的领域层——需要改样式的调用方请自己拿 " +
+                "ReadStyleGateway(ChapterProvider.getTypeface 是现成例子)。",
+            violations.isEmpty(),
+        )
+    }
+
+    private companion object {
+        fun stripComments(text: String): String = text
+            .replace(Regex("""/\*[\s\S]*?\*/"""), "")
+            .replace(Regex("""//[^\n]*"""), "")
+
+        fun mainSourceFile(relativePath: String): File {
+            var directory: File? = File("").absoluteFile
+            while (directory != null) {
+                for (prefix in listOf("src/main/java", "app/src/main/java")) {
+                    val candidate = File(directory, "$prefix/$relativePath")
+                    if (candidate.isFile) return candidate
+                }
+                directory = directory.parentFile
+            }
+            error("从 ${File("").absolutePath} 向上找不到 $relativePath")
+        }
+    }
+}