How can I implement polymorphic decoding of JSON data in Swift 4?

前端 未结 3 1967
被撕碎了的回忆
被撕碎了的回忆 2021-02-08 19:46

I am attempting to render a view from data returned from an API endpoint. My JSON looks (roughly) like this:

{
  \"sections\": [
    {
      \"title\": \"Feature         


        
3条回答
  •  自闭症患者
    2021-02-08 19:48

    Polymorphic design is a good thing: many design patterns exhibit polymorphism to make the overall system more flexible and extensible.

    Unfortunately, Codable doesn't have "built in" support for polymorphism, at least not yet.... there's also discussion about whether this is actually a feature or a bug.

    Fortunately, you can pretty easily create polymorphic objects using an enum as an intermediate "wrapper."

    First, I'd recommend declaring itemType as a static property, instead of an instance property, to make switching on it easier later. Thereby, your protocol and polymorphic types would look like this:

    import Foundation
    
    public protocol ViewLayoutSectionItemable: Decodable {
      static var itemType: String { get }
    
      var id: Int { get }
      var title: String { get set }
      var imageURL: URL { get set }
    }
    
    public struct Foo: ViewLayoutSectionItemable {
      
      // ViewLayoutSectionItemable Properties
      public static var itemType: String { return "foo" }
      
      public let id: Int
      public var title: String
      public var imageURL: URL
      
      // Foo Properties
      public var audioURL: URL
    }
    
    public struct Bar: ViewLayoutSectionItemable {
      
      // ViewLayoutSectionItemable Properties
      public static var itemType: String { return "bar" }
      
      public let id: Int
      public var title: String
      public var imageURL: URL
      
      // Bar Properties
      public var director: String
      public var videoURL: URL
    }
    

    Next, create an enum for the "wrapper":

    public enum ItemableWrapper: Decodable {
      
      // 1. Keys
      fileprivate enum Keys: String, CodingKey {
        case itemType = "item_type"
        case sections
        case sectionItems = "section_items"
      }
      
      // 2. Cases
      case foo(Foo)
      case bar(Bar)
      
      // 3. Computed Properties
      public var item: ViewLayoutSectionItemable {
        switch self {
        case .foo(let item): return item
        case .bar(let item): return item
        }
      }
      
      // 4. Static Methods
      public static func items(from decoder: Decoder) -> [ViewLayoutSectionItemable] {
        guard let container = try? decoder.container(keyedBy: Keys.self),
          var sectionItems = try? container.nestedUnkeyedContainer(forKey: .sectionItems) else {
            return []
        }
        var items: [ViewLayoutSectionItemable] = []
        while !sectionItems.isAtEnd {
          guard let wrapper = try? sectionItems.decode(ItemableWrapper.self) else { continue }
          items.append(wrapper.item)
        }
        return items
      }
      
      // 5. Decodable
      public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Keys.self)
        let itemType = try container.decode(String.self, forKey: Keys.itemType)
        switch itemType {
        case Foo.itemType:  self = .foo(try Foo(from: decoder))
        case Bar.itemType:  self = .bar(try Bar(from: decoder))
        default:
          throw DecodingError.dataCorruptedError(forKey: .itemType,
                                                 in: container,
                                                 debugDescription: "Unhandled item type: \(itemType)")
        }
      }
    }
    

    Here's what the above does:

    1. You declare Keys that are relevant to the response's structure. In your given API, you're interested in sections and sectionItems. You also need to know which key represents the type, which you declare here as itemType.

    2. You then explicitly list every possible case: this violates the Open Closed Principle, but this is "okay" to do as it's acting as a "factory" for creating items....

      Essentially, you'll only have this ONCE throughout your entire app, just right here.

    3. You declare a computed property for item: this way, you can unwrap the underlying ViewLayoutSectionItemable without needing to care about the actual case.

    4. This is the heart of the "wrapper" factory: you declare items(from:) as a static method that's capable of returning [ViewLayoutSectionItemable], which is exactly what you want to do: pass in a Decoder and get back an array containing polymorphic types! This is the method you'll actually use instead of decoding Foo, Bar or any other polymorphic arrays of these types directly.

    5. Lastly, you must make ItemableWrapper implement the Decodable method. The trick here is that ItemWrapper always decodes an ItemWrapper: thereby, this works how Decodable is expecting.

    As it's an enum, however, it's allowed to have associated types, which is exactly what you do for each case. Hence, you can indirectly create polymorphic types!

    Since you've done all the heavy lifting within ItemWrapper, it's very easy to now go from a Decoder to an `[ViewLayoutSectionItemable], which you'd do simply like this:

    let decoder = ... // however you created it
    let items = ItemableWrapper.items(from: decoder)
    

提交回复
热议问题