Custom class clusters in Swift
Asked Answered
Q

7

16

This is a relatively common design pattern:

https://mcmap.net/q/750748/-is-init-a-bad-place-to-implement-a-factory-in-objective-c

It allows you to return a subclass from your init calls.

I'm trying to figure out the best method of achieving the same thing using Swift.

I do know that it is very likely that there is a better method of achieving the same thing with Swift. However, my class is going to be initialized by an existing Obj-C library which I don't have control over. So it does need to work this way and be callable from Obj-C.

Any pointers would be very much appreciated.

Quechua answered 3/6, 2014 at 11:33 Comment(0)
S
10

I don't believe that this pattern can be directly supported in Swift, because initialisers do not return a value as they do in Objective C - so you do not get an opportunity to return an alternate object instance.

You can use a type method as an object factory - a fairly contrived example is -

class Vehicle
{
    var wheels: Int? {
      get {
        return nil
      }
    }

    class func vehicleFactory(wheels:Int) -> Vehicle
    {
        var retVal:Vehicle

        if (wheels == 4) {
            retVal=Car()
        }
        else if (wheels == 18) {
            retVal=Truck()
        }
        else {
            retVal=Vehicle()
        }

        return retVal
    }

}

class Car:Vehicle
{
    override var wheels: Int {
      get {
       return 4
      }
    }
}

class Truck:Vehicle
{
    override var wheels: Int {
      get {
          return 18
       }
     }
}

main.swift

let c=Vehicle.vehicleFactory(4)     // c is a Car

println(c.wheels)                   // outputs 4

let t=Vehicle.vehicleFactory(18)    // t is a truck

println(t.wheels)                   // outputs 18
Spermatium answered 3/6, 2014 at 12:27 Comment(4)
Ah okay. Unfortunately, the library I'm using directly calls initWithDictionary:, so I don't think I'll be able to do anything about it. I'll try doing the subclasses with it at least. Thank you for the help :)Quechua
About the only thing I can think of in that case is an ObjectiveC adapter class that implements the initWithDictionary and calls the Swift factory method.Spermatium
Ah yes true. I'm not specifically super needing to use Swift - only if convenient.Quechua
Man, way to take something that worked beautifully in Obj-C and completely handcuff it in Swift. Fewer simpler and reusable concepts are always better than a whole bunch of specialized features. The idea that initializers in Obj-C are just methods that return an object is brilliant, and now we're back to the same limited crap in other languages. I'm starting to wonder what the point of Swift even is now: it's more complicated, less intuitive and less capable than Objective-C, yet it's somehow supposed to be the future?Exurb
T
4

The "swifty" way of creating class clusters would actually be to expose a protocol instead of a base class.

Apparently the compiler forbids static functions on protocols or protocol extensions.

Until e.g. https://github.com/apple/swift-evolution/pull/247 (factory initializers) is accepted and implemented, the only way I could find to do this is the following:

import Foundation

protocol Building {
    func numberOfFloors() -> Int
}

func createBuilding(numberOfFloors numFloors: Int) -> Building? {
    switch numFloors {
    case 1...4:
        return SmallBuilding(numberOfFloors: numFloors)
    case 5...20:
        return BigBuilding(numberOfFloors: numFloors)
    case 21...200:
        return SkyScraper(numberOfFloors: numFloors)
    default:
        return nil
    }
}

private class BaseBuilding: Building {
    let numFloors: Int

    init(numberOfFloors:Int) {
        self.numFloors = numberOfFloors
    }

    func numberOfFloors() -> Int {
        return self.numFloors
    }
}

private class SmallBuilding: BaseBuilding {
}

private class BigBuilding: BaseBuilding {
}

private class SkyScraper: BaseBuilding {
}

.

// this sadly does not work as static functions are not allowed on protocols.
//let skyscraper = Building.create(numberOfFloors: 200)
//let bigBuilding = Building.create(numberOfFloors: 15)
//let smallBuilding = Building.create(numberOfFloors: 2)

// Workaround:
let skyscraper = createBuilding(numberOfFloors: 200)
let bigBuilding = createBuilding(numberOfFloors: 15)
let smallBuilding = createBuilding(numberOfFloors: 2)
Temple answered 20/6, 2015 at 15:18 Comment(0)
C
1

Since init() doesn't return values like -init does in Objective C, using a factory method seems like the easiest option.

One trick is to mark your initializers as private, like this:

class Person : CustomStringConvertible {
    static func person(age: UInt) -> Person {
        if age < 18 {
            return ChildPerson(age)
        }
        else {
            return AdultPerson(age)
        }
    }

    let age: UInt
    var description: String { return "" }

    private init(_ age: UInt) {
        self.age = age
    }
}

extension Person {
    class ChildPerson : Person {
        let toyCount: UInt

        private override init(_ age: UInt) {
            self.toyCount = 5

            super.init(age)
        }

        override var description: String {
            return "\(self.dynamicType): I'm \(age). I have \(toyCount) toys!"
        }
    }

    class AdultPerson : Person {
        let beerCount: UInt

        private override init(_ age: UInt) {
            self.beerCount = 99

            super.init(age)
        }

        override var description: String {
            return "\(self.dynamicType): I'm \(age). I have \(beerCount) beers!"
        }
    }
}

This results in the following behavior:

Person.person(10) // "ChildPerson: I'm 10. I have 5 toys!"
Person.person(35) // "AdultPerson: I'm 35. I have 99 beers!"
Person(35) // 'Person' cannot be constructed because it has no accessible initializers
Person.ChildPerson(35) // 'Person.ChildPerson' cannot be constructed because it has no accessible initializers

It's not quite as nice as Objective C, since private means all the subclasses need to be implemented in the same source file, and there's that the minor syntax difference Person.person(x) (or Person.create(x) or whatever) instead of simply Person(x), but practically speaking, it works the same.

To be able to instantiate literally as Person(x), you could turn Person into a proxy class which contains a private instance of the actual base class and forwards everything to it. Without message forwarding, this works for simple interfaces with few properties/methods but it gets unwieldy for anything more complex :P

Clinkscales answered 13/9, 2015 at 14:5 Comment(0)
S
0

I think actually the Cluster pattern can be implemented in Swift using runtime functions. The main point is to replace the class of your new object with a subclass when initializing. The code below works fine though I think more attention should be paid to subclass' initialization.

class MyClass
{
    var name: String?

    convenience init(type: Int)
    {
        self.init()

        var subclass: AnyClass?
        if type == 1
        {
            subclass = MySubclass1.self
        }
        else if type == 2
        {
            subclass = MySubclass2.self
        }

        object_setClass(self, subclass)
        self.customInit()
    }

    func customInit()
    {
        // to be overridden
    }
}

class MySubclass1 : MyClass
{
    override func customInit()
    {
        self.name = "instance of MySubclass1"
    }
}

class MySubclass2 : MyClass
{
    override func customInit()
    {
        self.name = "instance of MySubclass2"
    }
}

let myObject1 = MyClass(type: 1)
let myObject2 = MyClass(type: 2)
println(myObject1.name)
println(myObject2.name)
Sucre answered 7/6, 2014 at 10:36 Comment(4)
I'm not super convinced that would work. Reason being - what if there are additional fields in MySubclass1? The memory allocations made to initialize MyClass wouldn't be correct. Just overriding the class in the runtime won't deal with the memory will it?Quechua
@JamesBillingham Agree. But there are two correct solutions to that: store data in associated objects (also available through the runtime) or add more fields to the base class (or even a dictionary in which subclasses could store their data). This of course doesn’t add beauty to your implementation details but keeps the class interface nice (subjectively) and free from factory methods. Another disadvantage of a cluster implementation in Swift is that it doesn't let you return nil. So if you're ok with having class-level creation methods then definitely object factory is the most safe solution.Sucre
Yeah, that definitely isn't a possibility for us. We need to use actual properties for the library we rely on. Also, I think there is still a decent possibility of allocation issues even if you are very careful due to optimization done by Swift.Quechua
object_setClass might give undefined behaviour if the memory layout of the child classes differs than the one of the base class. For example new properties need extra storage, which doesn't exist because the initial allocation was for the base class, which doesn't require this extra storage.Prevision
S
0
protocol SomeProtocol {
   init(someData: Int)
   func doSomething()
}

class SomeClass: SomeProtocol {

   var instance: SomeProtocol

   init(someData: Int) {
      if someData == 0 {
         instance = SomeOtherClass()
      } else {
         instance = SomethingElseClass()
      }
   }

   func doSomething() {
      instance.doSomething()
   }
}

class SomeOtherClass: SomeProtocol {
   func doSomething() {
      print("something")
   }
}

class SomethingElseClass: SomeProtocol {
   func doSomething() {
     print("something else")
   }
}

Basically you create a protocol that your class cluster inherits from. You then wrap around an instance variable of the same type and choose which implementation to use.

For example, if you were writing an array class that switched between a LinkedList or a raw array then SomeOtherClass and SomethingElseClass might be named LinkedListImplementation or PlainArrayImplementation and you could decide which one to instantiate or switch to based on whatever is more efficient.

Siva answered 7/8, 2020 at 15:34 Comment(0)
B
0

There is a way to achieve this. Whether it is good or bad practice is for another discussion.

I have personally used it to allow for extension of a component in plugins without exposing the rest of the code to knowledge of the extensions. This follows the aims of the Factory and AbstractFactory patterns in decoupling code from the details of instantiation and concrete implementation classes.

In the example case the switching is done on a typed constant to which you would add in extensions. This kinda contradicts the above aims a little technically - although not in terms of foreknowledge. But in your case the switch might be anything - the number of wheels for example.

I don’t remember if this approach was available in 2014 - but it is now.

import Foundation

struct InterfaceType {
    let impl: Interface.Type
}

class Interface {

    let someAttribute: String

    convenience init(_ attribute: String, type: InterfaceType = .concrete) {
        self.init(impl: type.impl, attribute: attribute)
    }

    // need to disambiguate here so you aren't calling the above in a loop
    init(attribute: String) {
        someAttribute = attribute
    }

    func someMethod() {}

}

protocol _Factory {}

extension Interface: _Factory {}

fileprivate extension _Factory {

    // Protocol extension initializer - has the ability to assign to self, unlike class initializers.
    init(impl: Interface.Type, attribute: String) {
        self = impl.init(attribute: attribute) as! Self;
    }

}

Then in a concrete implementation file ...

import Foundation

class Concrete: Interface {

    override func someMethod() {
        // concrete version of some method
    }

}

extension InterfaceType {
    static let concrete = InterfaceType(impl: Concrete.self)
}

For this example Concrete is the "factory" supplied default implementation.

I have used this, for example, to abstract the details of how modal dialogs were presented in an app where initially UIAlertController was being used and migrated to a custom presentation. None of the call sites needed changing.

Here is a simplified version that does not determine the implementation class at runtime. You can paste the following into a Playground to verify its operation ...

import Foundation

class Interface {
        
    required init() {}
    
    convenience init(_ discriminator: Int) {
        let impl: Interface.Type
        switch discriminator {
            case 3:
                impl = Concrete3.self
            case 2:
                impl = Concrete2.self
            default:
                impl = Concrete1.self
        }
        self.init(impl: impl)
    }
    
    func someMethod() {
        print(NSStringFromClass(Self.self))
    }
    
}

protocol _Factory {}

extension Interface: _Factory {}

fileprivate extension _Factory {
    
    // Protocol extension initializer - has the ability to assign to self, unlike class initializers.
    init(impl: Interface.Type) {
        self = impl.init() as! Self;
    }
    
}

class Concrete1: Interface {}

class Concrete2: Interface {}

class Concrete3: Interface {
    override func someMethod() {
        print("I do what I want")
    }
}

Interface(2).someMethod()
Interface(1).someMethod()
Interface(3).someMethod()
Interface(0).someMethod()

Note that Interface must actually be a class - you can't collapse this down to a protocol avoiding the abstract class even if it had no need for member storage. This is because you cant invoke init on a protocol metatype and static member functions cannot be invoked on protocol metatypes. This is too bad as that solution would look a lot cleaner.

Benefice answered 29/8, 2020 at 1:42 Comment(0)
P
0

We can take advantage of a compiler quirk - self is allowed to be assigned in protocol extensions - https://forums.swift.org/t/assigning-to-self-in-protocol-extensions/4942.

Thus, we can have in place something like this:

/// The sole purpose of this protocol is to allow reassigning `self`
fileprivate protocol ClusterClassProtocol { }

extension ClusterClassProtocol {
    init(reassigningSelfTo other: Self) {
        self = other
    }
}

/// This is the base class, the one that gets circulated in the public space
class ClusterClass: ClusterClassProtocol {
    
    convenience init(_ intVal: Int) {
        self.init(reassigningSelfTo: IntChild(intVal) as! Self)
    }
    
    convenience init(_ stringVal: String) {
        self.init(reassigningSelfTo: StringChild(stringVal) as! Self)
    }
}

/// Some private subclass part of the same cluster
fileprivate class IntChild: ClusterClass {
    init(_ intVal: Int) { }
}

/// Another private subclass, part of the same cluster
fileprivate class StringChild: ClusterClass {
    init(_ stringVal: String) { }
}

Now, let's give this a try:

print(ClusterClass(10))    // IntChild
print(ClusterClass("abc")) // StringChild

This works the same as in Objective-C, where some classes (e.g. NSString, NSArray, NSDictionary) return different subclasses based on the values given at initialization time.

Prevision answered 3/11, 2020 at 15:55 Comment(4)
Is the above still true? I'm trying this on Swift 5.9 but it's failing with the error cannot convert value of type 'IntChild' to expected argument type 'Self' and the same for StringChild. Maybe this 'quirk' was actually a bug that they addressed. Any thoughts?Andrel
@MarkA.Donohoe yes, it works in Swift 5.9 too, though an as! Self cast is now needed (the compiler suggests this as a fix-it). The cast seems to now be needed due to the recent changes around the existentials. I've updated the code to reflect thisPrevision
🙏🏻😁Thanks! Happy this is still an option. It helps with decoding heterogeneous collections via JSON.Andrel
Finally got a chance to check this. Yup! Works like a charm! This is gonna make deserializing my heterogeneous collections so much cleaner! Thanks!!Andrel

© 2022 - 2024 — McMap. All rights reserved.