Tùy biến model trong Swift
Bài đăng này đã không được cập nhật trong 4 năm
-
Là lập trình viên, chúng ta thường làm việc trên các ứng dụng và hệ thống bao gồm nhiều thành phần được kết nối nhiều cách khác nhau. Đặc biệt là khi sử dụng ngôn ngữ lập trình có tính chặt chẽ cao như
Swift
thì khó tìm ra cách model phầndata
vừa thỏa mãn trình biên dịch vừa cócodebase
dễ đọc. -
Chúng ta hãy xem xét một trường hợp bao gồm việc mô hình hóa nhiều tùy chỉnh của cùng một
model data
và vài kỹ thuật , phương pháp khác nhau cho phép chúng ta xử lýdata
một cách an toàn.
1/ Mixed structures:
- Giả sử rằng chúng ta đang làm việc trên một ứng dụng nấu ăn bao gồm cả video và công thức nấu ăn dưới dạng văn bản và
content
chúng ta được trả về từweb service
dưới dạngJSON
như sau:
{
"items": [
{
"type": "video",
"title": "Making perfect toast",
"imageURL": "https://image-cdn.com/toast.png",
"url": "https://videoservice.com/toast.mp4",
"duration": "00:12:09",
"resolution": "720p"
},
{
"type": "recipe",
"title": "Tasty burritos",
"imageURL": "https://image-cdn.com/burritos.png",
"text": "Here's how to make the best burritos...",
"ingredients": [
"Tortillas",
"Salsa",
...
]
}
]
}
-
Cấu trúc
JSON
như bên trên là cực kỳ phổ biến nhưng việc tạo cácstruct
phù hợp với nó có thể khá khó khăn. Chúng ta nhận được mộtarray
cácitem
gồm công thức nấu ăn và video. Chúng ta sẽ cần viếtmodel
mà chúng ta có thểdecode
đồng thời cả hai tùy chỉnh đó. -
Một cách để làm điều đó là tạo ra một
enum
ItemType
bao gồm các trường hợp cho hai tùy chỉnh của chúng ta cũng như mộtmodel data
hợp nhất có chứa tất cả các thuộc tính mà chúng ta gặp phải và đóng góiItemCollection
:
enum ItemType: String, Decodable {
case video
case recipe
}
struct Item: Decodable {
let type: ItemType
var title: String
var imageURL: URL
var text: String?
var url: URL?
var duration: String?
var resolution: String?
var ingredients: [String]?
}
struct ItemCollection : Decodable {
var items: [Item]
}
- Cách tiếp cận trên cho phép chúng ta
decode
JSON
nhưng nó vẫn chưa tối ưu vì chúng ta buộc phải triển khai phần lớn các thuộc tính dưới dạng tùy chỉnh chẳng hạn nhưVideoPlayer
này:
class VideoPlayer {
...
func playVideoItem(_ item: Item) {
// We can't establish a compile-time guarantee that the
// item passed to this method will, in fact, be a video.
guard let url = item.url else {
assertionFailure("Video item doesn't have a URL: \(item)")
return
}
startPlayback(from: url)
}
}
2/ Complete polymorphism:
- Chúng ta cố gắng
model data
một tập hợp dữ liệu thành nhiều kiểu khác nhau. Chúng ta có thể tạo mộtprotocol
Item
chứa tất cả các thuộc tính được chia sẻ giữa hai biến thể cũng như haitype
riêng biệt:
protocol Item: Decodable {
var type: ItemType { get }
var title: String { get }
var imageURL: URL { get }
}
struct Video: Item {
var type: ItemType { .video }
var title: String
var imageURL: URL
var url: URL
var duration: String
var resolution: String
}
struct Recipe: Item {
var type: ItemType { .recipe }
var title: String
var imageURL: URL
var text: String
var ingredients: [String]
}
- Chúng ta cũng muốn sửa đổi trình bao bọc
ItemCollection
của chúng ta để bao gồm cácarray
riêng cho từngtype
trong haitype
vì nếu không làm thế chúng ta sẽ phải liên tục nhập các giá trị choVideo
hoặcRecipe
:
struct ItemCollection : Decodable {
var videos: [Video]
var recipes: [Recipe]
}
- Để có thể sử dụng lại các implement
Item
vàItemCollection
từ trước đồng thời đổi tên chúng để phù hợp với mục đích mới :
private extension ItemCollection {
struct Encoded: Decodable {
var items: [EncodedItem]
}
struct EncodedItem: Decodable {
let type: ItemType
var title: String
var imageURL: URL
var text: String?
var url: URL?
var duration: String?
var resolution: String?
var ingredients: [String]?
}
}
- Chúng ta đã sẵn sàng để
Decodable
nhưng vì chúng ta sẽ cần phải thêm một vài tùy chọn khi thực hiện điều đó:
extension ItemCollection {
struct MissingEncodedValue: Error {
var name: String
...
}
private func unwrap<T>(_ value: T?, name: String) throws -> T {
guard let value = value else {
throw MissingEncodedValue(name: name)
}
return value
}
}
- Bây giờ hãy viết
decode
thực tế. Chúng ta sẽ bắt đầu bằng cáchdecode
một phiên bản của . Chúng ta sẽ chuyển đổi các mục củaItemCollection
thànhVideo
vàRecipe
như sau:
extension ItemCollection {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let collection = try container.decode(Encoded.self)
for item in collection.items {
switch item.type {
case .video:
try videos.append(Video(
type: item.type,
title: item.title,
imageURL: item.imageURL,
url: unwrap(item.url, name: "url"),
duration: unwrap(item.duration, name: "duration"),
resolution: unwrap(item.resolution, name: "resolution")
))
case .recipe:
try recipes.append(Recipe(
type: item.type,
title: item.title,
imageURL: item.imageURL,
text: unwrap(item.text, name: "text"),
ingredients: unwrap(item.ingredients, name: "ingredients")
))
}
}
}
}
-
Thay vì coi các
instance
là các cách triển khai khai riêng biệt cóprotocol
hãy coi chúng là cácinstance
của cùng mộtmodel
. Điều đó sẽ có tác động khá lớn đến công đoạn cuối của chúng ta cấu trúc mô hình. -
Hãy đổi tên
protocol
Item
trước đó thànhItemVariant
và bỏ thuộc tínhtype
của nó:
protocol ItemVariant: Decodable {
var title: String { get }
var imageURL: URL { get }
}
- Chúng ta mô hình hóa loại
item
thực tế của chúng ta dưới dạngenum
như sau:
enum Item {
case video(Video)
case recipe(Recipe)
}
- Chúng ta có thể đơn giản hóa rất nhiều việc triển khai
decode
diễn ra hoàn toàn trong chính loạiItem
mới và chỉ cần kiểm tra từng giá trị loại của mụcJSON
để quyết định loạidecode
nào:
extension Item: Decodable {
struct InvalidTypeError: Error {
var type: String
...
}
private `enum `CodingKeys: CodingKey {
case type
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "video":
self = .video(try Video(from: decoder))
case "recipe":
self = .recipe(try Recipe(from: decoder))
default:
throw InvalidTypeError(type: type)
}
}
}
- Do việc triển khai
Item
của chúng ta chịu trách nhiệmdecode
cácinstance
của chính nó và giờ chúng ta có thểItemCollection
được trả lại trở lại đơn giản làarray
các giá trịItem
cho phép dựa vào triển khai mặc định củaDecodable
:
struct itemCollection : Decodable {
var items: [Item]
}
- Lợi ích là c chúng ta tiếp tục sử dụng các mô hình chuyên dụng đồng thời giữ cho
decode
của chúng ta đơn giản và thứ tựitem
tổng thể được giữ nguyên nhưng đi kèm với nhược điểm là yêu cầu giải nén từngitem
trước khi sử dụng:
extension ItemCollection {
func allTitles() -> [String] {
items.map { item in
switch item {
case .video(let video):
return video.title
case .recipe(let recipe):
return recipe.title
}
}
}
}
-
Mặc dù chúng ta sẽ phải tiếp tục viết
code
như trên bất cứ khi nào chúng ta cần truy cậpdata
cụ thể cho công thức nấu ăn hoặcVideo
. Chúng ta có thể làm để cung cấp quyền truy cập trực tiếp vào bất kỳ thuộc tính được xác định trongprotocol
ItemVariant
và sử dụng tính năng tra cứusubscription
củaSwift
. -
Việc thêm thuộc tính
@dynamicMemberLookup
vào khai báoitem
chính của chúng ta:
@dynamicMemberLookup
enum Item {
case video(Video)
case recipe(Recipe)
}
- Từ
Swift 5.1
chúng ta có thêm cách hỗ trợ cho cácsubscription
theo cách hoàn toàn an toàn và dễ dàng như sau:
extension Item {
subscript<T>(dynamicMember keyPath: KeyPath<ItemVariant, T>) -> T {
switch self {
case .video(let video):
return video[keyPath: keyPath]
case .recipe(let recipe):
return recipe[keyPath: keyPath]
}
}
}
- Bây giờ chúng ta có thể truy cập bất kỳ thuộc tính nào được chia sẻ giữa
Video
vàRecipe
(thông quaprotocol
ItemVariant
) như thể thuộc tính đó được xác định trong chính loạiItem
. Chúng ta có thể chuyển đổi phương thứcallTitle
thành như sau :
extension ItemCollection {
func allTitles() -> [String] {
items.map(\.title)
}
}
All rights reserved