1

Let's start with the problem I'm trying to solve. I'm parsing an XML document into a hierarchy of model objects. All of the model objects have a common base class with a set of common properties. Then each specific model class has a few additional properties.

Here's a simplified example of a few model classes:

class Base {
    var id: String?
    var name: String?
    var children = [Base]()
}

class General: Base {
    var thing: String?
}

class Specific: General {
    var boring: String?
}

class Other: Base {
    var something: String?
    var another: String?
}

The part I'm having issue with is implementing a clean way to write the XML parser classes to deal with this model hierarchy. I'm attempting to write a parser hierarchy that matches the model hierarchy. Here's my attempt:

protocol ObjectParser {
    associatedtype ObjectType

    func createObject() -> ObjectType
    func parseAttributes(element: XMLElement, object: ObjectType)
    func parseElement(_ element: XMLElement) -> ObjectType
}

class BaseParser: ObjectParser {
    typealias ObjectType = Base

    var shouldParseChildren: Bool {
        return true
    }

    func createObject() -> Base {
        return Base()
    }

    func parseAttributes(element: XMLElement, object: Base) {
        object.id = element.attribute(forName: "id")?.stringValue
        object.name = element.attribute(forName: "name")?.stringValue
    }

    func parseChildren(_ element: XMLElement, parent: Base) {
        if let children = element.children {
            for child in children {
                if let elem = child as? XMLElement, let name = elem.name {
                    var parser: BaseParser? = nil

                    switch name {
                    case "general":
                        parser = GeneralParser()
                    case "specific":
                        parser = SpecificParser()
                    case "other":
                        parser = OtherParser()
                    default:
                        break
                    }

                    if let parser = parser {
                        let res = parser.parseElement(elem)
                        parent.children.append(res)
                    }
                }
            }
        }
    }

    func parseElement(_ element: XMLElement) -> Base {
        let res = createObject()

        parseAttributes(element: element, object: res)

        if shouldParseChildren {
            parseChildren(element, parent: res)
        }

        return res
    }
}

class GeneralParser: BaseParser {
    typealias ObjectType = General

    override func createObject() -> General {
        return General()
    }

    func parseAttributes(element: XMLElement, object: General) {
        super.parseAttributes(element: element, object: object)

        object.thing = element.attribute(forName: "thing")?.stringValue
    }
}

class SpecificParser: GeneralParser {
    typealias ObjectType = Specific

    override func createObject() -> Specific {
        return Specific()
    }

    func parseAttributes(element: XMLElement, object: Specific) {
        super.parseAttributes(element: element, object: object)

        object.boring = element.attribute(forName: "boring")?.stringValue
    }
}

And there is OtherParser which is the same as GeneralParser except replace General with Other. Of course there are many more model objects and associated parsers in my hierarchy.

This version of the code almost works. You'll notice there is no override for the parseAttributes methods in the GeneralParser and SpecificParser classes. I think this is due to the different type for the object argument. The result of this is that the parser specific parseAttributes methods are not being called from the parseElement method of BaseParser. I got around this problem by updating all of the parseAttributes signatures to:

func parseAttributes(element: XMLElement, object: Base)

Then in the non-Base parsers, I had to use a force-cast (and add override such as the following in the GeneralParser:

override func parseAttributes(element: XMLElement, object: Base) {
    super.parseAttributes(element: element, object: object)

    let general = object as! General
    general.thing = element.attribute(forName: "thing")?.stringValue
}

Finally, the question:

How do I eliminate the need for the force-cast in the parseAttributes method hierarchy and make use of the protocol's associated type? And more general, is this the correct approach to this problem? Is there a more "Swift" way to solve this problem?

Here's some made up XML based on this simplified object model if needed:

<other id="top-level" name="Hi">
    <general thing="whatever">
        <specific boring="yes"/>
        <specific boring="probably"/>
        <other id="mid-level">
            <specific/>
        </other>
    </general>
</other>
rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • 1
    Give us an `XML` to toy with – Alexander May 22 '17 at 16:38
  • @Alexander I added some XML at the end of the question but you really shouldn't need it to provide an answer. – rmaddy May 22 '17 at 16:47
  • When you say that _"parser specific parseAttributes methods are not being called"_, where are they not being called from? – 0x416e746f6e May 22 '17 at 17:49
  • @courteouselk From the `parseElement` method of `BaseParser`. – rmaddy May 22 '17 at 17:53
  • You could replace `func createObject()` by a `func createObject(_ element: XMLElement)` which creates the object *and* calls `parseAttributes()`. There are probably more elegant solutions but that should solve the problem of parsing the specific attributes. – Just an idea... – Martin R May 22 '17 at 18:53
  • @MartinR Now why didn't I think of that? OK, that would solve the specific issue posted here. But I'd still love to know if there is a more general solution to the overall question of dealing with calling overloaded (not overridden) methods (like `parseAttributes`) from a base class in Swift. I just ran into a similar issue trying to implement pure Swift copy constructors for this same model hierarchy. – rmaddy May 22 '17 at 19:01
  • @rmaddy: With respect to making copies: Did you see this https://stackoverflow.com/questions/25645090/protocol-func-returning-self ? – Martin R May 22 '17 at 19:05
  • @MartinR I did actually, thanks. I couldn't get it to compile. I ran into the issue mentioned by "fluidsonic". I don't want to get off-topic here. I may post another question on the copy constructor issue. – rmaddy May 22 '17 at 19:10
  • 1
    Is there a reason you're writing your parsers as a separate hierarchy? I would just have, for example, a `required init(from: XMLDecoder)` in your model classes, and let each class grab their property values from a given `XMLDecoder` instance, which would just a simple type that can vend property values for given attribute names of an XML element. You would then just pass this decoder instance up the model hierarchy. – Hamish May 22 '17 at 20:45
  • @Hamish The model should not have any knowledge of how it was persisted. The model should know nothing about XML or any other format. It's a model. A persistence layer knows about models but models should not know about persistence layers. – rmaddy May 22 '17 at 21:10
  • @rmaddy I agree – but there shouldn't be anything 'persistance-specific' in the implementation of the `required init` I proposed, it would be just be along the lines of `self.someProperty = decoder.decodeIfPresent(String.self, forKey: "someKey")` (basically, something along the lines of https://github.com/apple/swift-evolution/blob/master/proposals/0166-swift-archival-serialization.md :) ). It would just be the `XMLDecoder` that deals with the specifics of XML. In fact, you could probably have a `Decoder` protocol (again from the SE proposal), and then have the init as `init(from: Decoder)`. – Hamish May 22 '17 at 21:13
  • I answered the question in your comments. – Daniel T. May 29 '17 at 11:52

1 Answers1

0

Here's how I would solve this problem:

func createObject(from element: XMLElement) -> Base {
    switch element.name {
    case "base":
        let base = Base()
        initialize(base: base, from: element)
        return base
    case "general":
        let general = General()
        initialize(general: general, from: element)
        return general
    case "specific":
        let specific = Specific()
        initialize(specific: specific, from: element)
        return specific
    case "other":
        let other = Other()
        initialize(other: other, from: element)
        return other
    default:
        fatalError()
    }
}

func initialize(base: Base, from element: XMLElement) {
    base.id = element.attribute(forName: "id")?.stringValue
    base.name = element.attribute(forName: "name")?.stringValue
    base.children = element.children.map { createObject(from: $0) }
}

func initialize(general: General, from element: XMLElement) {
    general.thing = element.attribute(forName: "thing")?.stringValue
    initialize(base: general, from: element)
}

func initialize(specific: Specific, from element: XMLElement) {
    specific.boring = element.attribute(forName: "boring")?.stringValue
    initialize(general: specific, from: element)
}

func initialize(other: Other, from element: XMLElement) {
    other.something = element.attribute(forName: "something")?.stringValue
    other.another = element.attribute(forName: "another")?.stringValue
    initialize(base: other, from: element)
}

I really don't see a need of a mirrored inheritance hierarchy of Parser classes. I initially tried to make the initialize functions as constructors in extensions, but you can't override extension methods. Of course you could just make them init methods of the classes themselves, but I'm assuming you want to keep the XML specific code separate from your model code.

-- ADDITION --

I'd still love to know if there is a more general solution to the overall question of dealing with calling overloaded (not overridden) methods (like parseAttributes) from a base class in Swift.

You do it the same way you would do it in any other language. You cast the object (if necessary) and then call the method. There's nothing magical or special to Swift in this regard.

class Foo {
    func bar(with: Int) {
        print("bar with int called")
    }
}

class SubFoo: Foo {
    func bar(with: String) {
        print("bar with string called")
    }
}


let foo: Foo = SubFoo()

foo.bar(with: 12) // can't access bar(with: Double) here because foo is of type Foo
(foo as? SubFoo)?.bar(with: "hello") // (foo as? SubFoo)? will allow you to call the overload if foo is a SubFoo

let subFoo = SubFoo()

// can call either here
subFoo.bar(with: "hello")
subFoo.bar(with: 12)
Daniel T.
  • 32,821
  • 6
  • 50
  • 72