Reputation: 688
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
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
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
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