How do I apply Swift generics / protocols in a class hierarchy?

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

Here's a simplified example of several 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 came across implements a clean way to write XML parser classes to work with this model hierarchy. I am trying 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

what is the same as GeneralParser

, except for the replacement General

with Other

. Of course, there are many more model objects and related parsers in my hierarchy.

This version of the code almost works. You will notice that for parseAttributes

methods in classes GeneralParser

and SpecificParser

not override

. I think it has to do with a different type for the argument object

. The result of this is that the parser-specific methods are parseAttributes

not called from the method parseElement

BaseParser

. I got around this problem by updating all signatures parseAttributes

to:

func parseAttributes(element: XMLElement, object: Base)

      

Then, in parsers other than Base, I had to force-cast (and add override

, for example, the following to 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 is:

How do I get rid of the need for enforcement in the method hierarchy parseAttributes

and use a protocol-related type? And more generally, is this the correct approach to this problem? Is there a faster way to fix this problem?

Here are some compiled 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>

      

+3


source to share


1 answer


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 the need for a mirrored inheritance hierarchy of Parser classes. I first tried to make functions initialize

constructors in extensions, but you cannot override extension methods. Of course, you could just make them init

methods of the classes themselves, but I am assuming that you want the separate XML code to be separated from your model code.

- ADDITION -



I would still like to know if there is a more general solution to the general question of call overloading (not overridden) (like parseAttributes) from a base class in Swift.

You do it the same way you would in any other language. You throw the object (if necessary) and then call the method. There is nothing magical or special about 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)

      

0


source







All Articles