Dart 是強型別語言,我們會宣告各式各樣的物件,來幫助我們完成功能。所有物件最頂層雖然是 Object,但實際上使用時,卻能分出許多不同的物件,有 Widget、有 Controller,有些負責打 API,有些負責處理流程。在這之中,我們常常使用到其中之一種的就是 Value Object,讓不同物件之間可以傳遞資料,一個經典的情境就是,當我們打 Web API 取回 json 資料後,我們會把它轉成一個相對應的 Value Object,以便畫面顯示或者使用者操作功能。
final response = await dio.get("my/api");
return User.fromJson(response.data);
在上面的例子中,response.data 是個 dynamic 物件,我們會把 dynamic 物件轉成 User 物件。
大多數時候,我們用在外部 API 取回資料物件時,會把它轉成強型別,能讓我們享受幾個好處:
雖然 Dart 有反射功能,但是 Flutter 由於因為一些原因,在 Flutter 禁用反射。所以當我們取回 http reposone 時,我們必須自己針對每個物件實現 fromJson。
https://dartpad.dev/?id=71f5603a74f415f800339d3af816a458
當我們的程式需要常常與不同 API 互動時,我們會產生大量的 fromJson 程式碼,每個物件中的 fromJson 的細節不同,但整體的行為都是把 dynamic 轉成特定類別。Flutter 雖然沒有反射,但是我們還是有一些辦法來讓我們不用每次都寫一次 fromJson。
Google 在 pub.dev 中提供了 json_serializable 套件,讓我們可以自動產生 Json 序列化的程式碼。當我們在程式中引用 json_serializable 與 build_runner 之後,然後在 User 類別上加上 @JsonSerializable ,最後執行 build_runner。
https://dartpad.dev/?id=8556bc9249cbfd8c31d291170411409d
flutter pub run build_runner
最後我們可以在檔案中看到自動產生的 user.g.dart 程式碼。
https://dartpad.dev/?id=1b68fbc60c6f0e4494632cc8f36db818
除此了使用 json_serializable 之外,我們也可以選擇使用 freezed。freezed 除了能幫我們自動產生 json 序列化的方法之外,也能幫我們產生 Value Object 也常常需要複寫的方法,例如用來比較兩個物件是否相同的 == 運算子,或者是複製並修改的 copyWith 方法。
https://dartpad.dev/?id=0265a639151c710f3b3f69836dd9ce77
https://dartpad.dev/?id=32f3dbcc5819c8387b54672562ef0da3
有些時候,我們可能會在兩個頁面使用相同的 API,拿回相同的物件,但用不同的資料來處理每個頁面的畫面或流程。在下面的例子中,UserInfoScreen 與 VipProductListScreen 都會呼叫 User API 取回 User 物件,兩個頁面都分別只使用部分 User 的成員。
https://dartpad.dev/?id=e4e5838f7c188cb545af329f618f8973
在這上面的例子中,假設今天我們建立了一個新的 API,讓我們可以直接呼叫取得 User 的 isVip,此時當我們想要替換原本 API 時,需要改動的範圍就比較大,因為 VipProductListScreen 直接引用了 User。假設今天 VipProductListScreen 只從 RemoteApi 拿回來只有 isVip,而不是整個 User 物件,那我們就能更容易抽換實作。
https://dartpad.dev/?id=8f96dfd25748069648e6aba58bb12fe8
雖然 Value Object 並不繼承任何介面,但讓所有地方都直接使用 User,也是不太符合介面隔離原則的概念。
客戶不應被迫使用對其而言無用的方法或功能。
在開發的過程中,我們常常會設計各式各樣的 Value Object,如何善用工具節省開發時間,是一項直得研究的課題。雖然 Value Object 很容易使用,但是開發時也要思考一下,是否當前的 Value Object 是最適合的設計,持續的檢視與改善,讓程式永遠維持在一個容易維護的狀態。