Maklaus
Maklaus

Reputation: 688

How to properly use subclasses in Swift 5

I'm trying to have a generic Plugin class and then subclasses such as PluginOne and PluginTwo that would extend the class by adding a function run() and output property so each plugin can execute a custom command and save the output.

Something like this:

class Plugin: Decodable {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class PluginOne: Plugin {
  var output: String?

  init(name: String, output: String) {
    self.output = output

    super.init(name: name)
  }

  func run() {
    // do something
    self.output = "Some output"
  }  
}

class PluginTwo: Plugin {
  var output: String?

  init(name: String, output: String) {
    self.output = output

    super.init(name: name)
  }

  func run() {
    // do something
    self.output = "Some other output"
  }  
}

Now I'm getting the list of available plugins from a json:

let json = """
[
  { "type": "PluginOne", "name": "First plugin of type one" },
  { "type": "PluginOne", "name": "Second plugin of type one" },
  { "type": "PluginTwo", "name": "abcd" }
]
"""

And I'm decoding the file into [Plugin]:

let decoder = JSONDecoder()
let jsonData = Data(json.utf8)
let plugins = try decoder.decode([Plugin].self, from: jsonData)

Now the question is how to properly create subclasses PluginOne and PluginTwo from each Plugin in order to run() each of them?

Also I understand I'm doing something wrong and should maybe decode into a subclass right away (how?) and/or use protocols instead of subclasses.

Please advise

Result of executing the first answer:

import Foundation

let json = """
[
  { "type": "PluginOne", "name": "First plugin of type one" },
  { "type": "PluginOne", "name": "Second plugin of type one" },
  { "type": "PluginTwo", "name": "abcd" }
]
"""


class Plugin: Decodable {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class PluginOne: Plugin {
  var output: String?

  init(name: String, output: String) {
    self.output = output

    super.init(name: name)
  }

  required init(from decoder: Decoder) throws {
    fatalError("init(from:) has not been implemented")
  }

  func run() {
    // do something
    self.output = "Some output"
  }
}

class PluginTwo: Plugin {
  var output: String?

  init(name: String, output: String) {
    self.output = output

    super.init(name: name)
  }

  required init(from decoder: Decoder) throws {
      fatalError("init(from:) has not been implemented")
  }

  func run() {
    // do something
    self.output = "Some other output"
  }
}

let decoder = JSONDecoder()
let jsonData = Data(json.utf8)
let plugins = try decoder.decode([Plugin].self, from: jsonData)

for plugin in plugins {
    if let pluginOne = plugin as? PluginOne {
        pluginOne.run()
        print(pluginOne.output ?? "empty one")
    }
    else if let pluginTwo = plugin as? PluginTwo {
        pluginTwo.run()
        print(pluginTwo.output ?? "empty two")
    } else {
        print("error")
    }
}
// Result: error error error

Upvotes: 2

Views: 283

Answers (3)

Rob
Rob

Reputation: 437532

In answer to the question “how to properly use subclasses” is often “don’t”. Swift offers a different paradigm: Rather than object oriented programming, we employ protocol oriented programming as outlined in WWDC 2016 video Protocol and Value Oriented Programming in UIKit Apps.

protocol Plugin: Decodable {
    var name: String { get }

    func run()
}

struct PluginOne: Plugin {
    let name: String

    func run() { ... }
}

struct PluginTwo: Plugin {
    let name: String

    func run() { ... }
}

The question is then “how to I parse the JSON”, and we would employ the techniques outlined in the “Encode and Decode Manually” section of the Encoding and Decoding Custom Types document:

struct Plugins: Decodable {
    let plugins: [Plugin]

    init(from decoder: Decoder) throws {
        enum AdditionalInfoKeys: String, CodingKey {
            case type
            case name
        }

        var plugins: [Plugin] = []

        var array = try decoder.unkeyedContainer()

        while !array.isAtEnd {
            let container = try array.nestedContainer(keyedBy: AdditionalInfoKeys.self)

            let type = try container.decode(PluginType.self, forKey: .type)
            let name = try container.decode(String.self, forKey: .name)

            switch type {
            case .pluginOne: plugins.append(PluginOne(name: name))
            case .pluginTwo: plugins.append(PluginTwo(name: name))
            }
        }

        self.plugins = plugins
    }
}

With

enum PluginType: String, Decodable {
    case pluginOne = "PluginOne"
    case pluginTwo = "PluginTwo"
}

You can then do things like:

do {
    let plugins = try JSONDecoder().decode(Plugins.self, from: data)
    print(plugins.plugins)
} catch {
    print(error)
}

That gives you your array of objects that conform to the Plugin protocol.

Upvotes: 2

Joakim Danielson
Joakim Danielson

Reputation: 51910

I think you need to separate between the plugins and management of the plugins since the json contains a list of plugins to load, create or run and not the actual plugins. So for this solution I created a separate PluginManager to hold the plugins and also a protocol and enum to use by the manager

protocol Plugin { //Protocol each plugin must conform to
    func run() -> ()
}

enum PluginType: String { // All supported plugins. To simplify the code but not necessary
    case pluginOne = "PluginOne"
    case pluginTwo = "PluginTwo"
}

The manager class itself, it has an add method for adding plugins from json data and as an example a runAll method that runs all plugins

struct PluginManager {
    var plugins: [String: Plugin]

    init() {
        plugins = [:]
    }

    mutating func add(_ type: String, name: String) {
        var plugin: Plugin?
        switch PluginType.init(rawValue: type) {
        case .pluginOne:
            plugin = PluginOne()
        case .pluginTwo:
            plugin = PluginTwo()
        default:
            print("warning unknow plugin type: \(type)")
        }
        if let plugin = plugin {
            plugins[name] = plugin
        }
    }

    func runAll() {
        for (name, plugin) in plugins {
            print("Executing \(name)")
            plugin.run()
        }
    }
}

The json decoding has been simplified to decode into a dictionary and then use that dictionary to add plugins to the manager

var pluginManager = PluginManager()
do {
    let plugins = try JSONDecoder().decode([[String: String]].self, from: json)
    for plugin in plugins {
        if let type = plugin["type"], let name = plugin["name"] {
            pluginManager.add(type, name: name)
        }
    }
    pluginManager.runAll()

} catch {
    print(error)
}

Upvotes: 2

John Farkerson
John Farkerson

Reputation: 2632

The best method is definitely protocols. However, if you want to do this, you could make use of Swift's nice optional casting functionality:

for plugin in plugins {
    if let pluginOne = plugin as? PluginOne {
        pluginOne.foo = 0// If you need to set some subclass-specific variable
        pluginOne.run()
    }
    else if let pluginTwo = plugin as? PluginTwo {
        pluginTwo.bar = 0
        pluginTwo.run()
    }
}

If you wanted to use protocols instead:

protocol Runnable {//Our new protocol, only containing one method
    func run()
}

class Plugin: Decodable {
    name: String

    init(name: String) {
        self.name = name
    }
}

class PluginOne: Plugin, Runnable { //implements Runnable protocol
    output: String?

    init(name: String) {
        self.output = output

        super.init(name: name)
    }

    func run() {
        // do something
        self.output = "Some output"
    }  
}

class PluginTwo: Plugin, Runnable { //Also implements runnable protocol
    output: String?

    init(name: String) {
        self.output = output

        super.init(name: name)
    }

    func run() {
        // do something
        self.output = "Some other output"
    }  
}

//.....

let plugins: [Runnable] = load("plugins.json")//Now we have an array of Runnables!
for plugin in plugins {
    plugin.run()//We know that plugin will implement the Runnable protocol,
                //so we know it contains the run() method!
}

Upvotes: 1

Related Questions