前面幾天大部分都在研究怎麼跟 AI 一起寫 Code,但從今天開始,我想把重點慢慢拉回 Android 本身。
剛好這次拿來實驗的 Project 是我以前自己寫的,所以與其一直讓 AI 幫我找問題,我更想做另一件事:
回頭看以前自己做的 Android 設計,現在還會不會做出一樣的選擇?
第一個就從這次一直碰到的 Timer 開始。
目前 Pomodoro 和 Stopwatch 共用一個 TimeManager,而且它是一個 Singleton。
PomodoroFragment ─┐
↓
TimeManager
↑
StopWatchFragment ┘
當初這樣做的理由其實很好理解。
Timer 不應該跟著某一個 Fragment 消失,Pomodoro 和 Stopwatch 又需要共用「現在到底是哪一種計時正在執行」的狀態,所以把計時集中到同一個地方管理很直覺。
但這次重新修改 Stopwatch 時,也剛好讓這個設計的代價全部跑出來。
如果把計時直接放在 Fragment:
Fragment
↓
Timer
↓
更新畫面
最大的問題就是 Timer 太靠近 UI。
Fragment 可以因為切換畫面、重新建立等原因離開,但使用者認知中的「計時」不應該因此重新開始。
所以當時把 Timer 往 UI 外面移是合理的。
TimeManager 負責目前模式、計時狀態與經過時間,例如:
enum class Mode {
IDLE,
POMODORO,
BREAK,
STOPWATCH
}
enum class TimeState {
IDLE,
RUNNING,
PAUSED
}
Fragment 只負責觀察狀態並更新畫面。
這樣至少解決了一件事:
Timer 的生命週期不再直接綁在某一個畫面上。
這次加入 Stopwatch 後,最明顯的問題就是 Pomodoro 和 Stopwatch 其實不是兩套獨立 Timer。
它們都在操作同一個 TimeManager。
所以:
Pomodoro Running
↓
TimeManager.mode = POMODORO
切換 Stopwatch
↓
還是同一個 TimeManager
這也是為什麼後來需要處理兩個 Timer 互斥,以及不同 Fragment 到底應不應該回應目前的時間。
Singleton 讓共享狀態變簡單,但同時也代表:
只要其中一個功能修改共享狀態,其他觀察者都有可能受到影響。
這次曾經出現 Stopwatch 計時後,Pomodoro 畫面也跟著顯示 Stopwatch 時間,就是很典型的例子。
問題不一定是 Singleton 本身,而是「共享了什麼」。
如果只看到:
object TimeManager
然後直接討論「Singleton 好不好」,其實有點太快。
現在回頭看,我反而會先拆開裡面的狀態:
目前是哪種 Timer
目前 Running / Paused / Idle
開始時間
經過時間
目標時間
接著再問:
這些資料是不是本來就代表「目前 App 唯一正在進行的計時 Session」?
如果產品規則本來就是:
同一時間只能存在一個計時
那 Pomodoro 和 Stopwatch 共用一份目前計時狀態,其實不是奇怪的事情。
甚至可以說,互斥不是後來為了解決 Singleton 問題硬加的限制,而是 Domain 本身就需要表達的規則。
但如果需求是:
Pomodoro 可以跑
同時 Stopwatch 也可以跑
那一個全域 TimeManager 只保存:
mode = POMODORO
或:
mode = STOPWATCH
就根本無法完整表達這個需求。
所以 Architecture 合不合理,其實跟 Requirement 有直接關係。
以前看到 Singleton,很容易直接想到:
這是不是不好的設計?
但現在我比較想先問:
這個物件代表的是什麼?
如果它代表的是:
App 目前唯一的計時 Session
那它本來就需要被多個地方共享。
真正可以重新思考的是,它是不是一定要透過全域 Singleton 取得,以及依賴關係能不能更清楚。
例如現在:
Fragment
↓
直接取得 TimeManager
如果未來 Project 變大,也許可以變成由上層建立並提供同一個實例:
Application Scope
↓
Timer Manager
↓
ViewModel
↓
UI
共享狀態還是在,但「全域都可以直接拿到它」和「這個物件的生命週期是全域」其實是兩件不同的事情。
這也是我現在回頭看舊 Code 覺得很有意思的地方。
以前很容易把它們混在一起:
我要讓 Timer 一直存在
↓
那就做 Singleton
但現在重新拆開看,其實至少有三個問題:
Timer 應該活多久?
哪些功能應該共享同一份 Timer 狀態?
誰負責建立並持有這個物件?
這三題的答案不一定都是「Singleton」。
Singleton 解決的是實例只有一份以及取得方式的問題,但它不會自動幫我決定狀態應該怎麼切、生命週期怎麼管理,也不代表所有使用者都應該直接依賴它。
如果只是問:
Android 可以用 Singleton 管理 Timer 嗎?
很容易得到一堆「可以,但要注意……」的答案。
但拿自己以前真的寫過、而且這次又真的因為新增 Stopwatch 遇到共享狀態問題的 Code 回頭看,就會比較清楚:
當初為什麼這樣設計?
它解決了什麼?
這次新增需求後,哪個假設開始產生限制?
如果今天重寫,我會保留什麼,又會改什麼?
我覺得這比單純記:
Singleton 不好,不要用。
有用很多。
Architecture 很少是單純的「這樣寫對不對」,比較像是「這個設計當初解決了什麼,而現在的需求還適不適合它」。
接下來我想繼續用這種方式回頭看以前寫過的 Android Code,不是要證明以前寫錯了,而是重新把當時沒有想清楚的「為什麼」補回來。