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>
source to share
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)
source to share