Tùy biến model trong Swift
Bài đăng này đã không được cập nhật trong 5 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ư Swiftthì khó tìm ra cách model phầndatavừa thỏa mãn trình biên dịch vừa cócodebasedễ đọ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 datavà vài kỹ thuật , phương pháp khác nhau cho phép chúng ta xử lýdatamộ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à contentchúng ta được trả về từweb servicedưới dạngJSONnhư 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 JSONnhư bên trên là cực kỳ phổ biến nhưng việc tạo cácstructphù hợp với nó có thể khá khó khăn. Chúng ta nhận được mộtarraycácitemgồm công thức nấu ăn và video. Chúng ta sẽ cần viếtmodelmà 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 enumItemTypebao gồm các trường hợp cho hai tùy chỉnh của chúng ta cũng như mộtmodel datahợ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 decodeJSONnhư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ưVideoPlayernà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 datamộ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ộtprotocolItemchứa tất cả các thuộc tính được chia sẻ giữa hai biến thể cũng như haityperiê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 ItemCollectioncủa chúng ta để bao gồm cácarrayriêng cho từngtypetrong haitypevì nếu không làm thế chúng ta sẽ phải liên tục nhập các giá trị choVideohoặcRecipe:
struct ItemCollection : Decodable {
    var videos: [Video]
    var recipes: [Recipe]
}
- Để có thể sử dụng lại các implement ItemvàItemCollectiontừ 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 để  Decodablenhư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 decodethực tế. Chúng ta sẽ bắt đầu bằng cáchdecodemột phiên bản của . Chúng ta sẽ chuyển đổi các mục củaItemCollectionthànhVideovàRecipenhư 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 instancelà các cách triển khai khai riêng biệt cóprotocolhãy coi chúng là cácinstancecủ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 protocolItemtrước đó thànhItemVariantvà bỏ thuộc tínhtypecủa nó:
protocol ItemVariant: Decodable {
    var title: String { get }
    var imageURL: URL { get }
}
- Chúng ta mô hình hóa loại itemthực tế của chúng ta dưới dạngenumnhư 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 decodediễn ra hoàn toàn trong chính loạiItemmới và chỉ cần kiểm tra từng giá trị loại của mụcJSONđể quyết định loạidecodenà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 Itemcủa chúng ta chịu trách nhiệmdecodecácinstancecủa chính nó và giờ chúng ta có thểItemCollectionđược trả lại trở lại đơn giản làarraycác giá trịItemcho 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 decodecủa chúng ta đơn giản và thứ tựitemtổ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ừngitemtrướ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 codenhư trên bất cứ khi nào chúng ta cần truy cậpdatacụ 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 trongprotocolItemVariantvà sử dụng tính năng tra cứusubscriptioncủaSwift.
- 
Việc thêm thuộc tính @dynamicMemberLookupvào khai báoitemchính của chúng ta:
@dynamicMemberLookup
enum Item {
    case video(Video)
    case recipe(Recipe)
}
- Từ Swift 5.1chúng ta có thêm cách hỗ trợ cho cácsubscriptiontheo 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 VideovàRecipe(thông quaprotocolItemVariant) 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ứcallTitlethành như sau :
extension ItemCollection  {
    func allTitles() -> [String] {
        items.map(\.title)
    }
}
All rights reserved
 
  
 