Custom class clusters in Swift

前端 未结 7 1923
灰色年华
灰色年华 2020-12-02 00:04

This is a relatively common design pattern:

https://stackoverflow.com/a/17015041/743957

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

相关标签:
7条回答
  • 2020-12-02 00:12

    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

    0 讨论(0)
  • 2020-12-02 00:20

    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)
    
    0 讨论(0)
  • 2020-12-02 00:22

    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
    
    0 讨论(0)
  • 2020-12-02 00:23

    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)
    
    0 讨论(0)
  • 2020-12-02 00:27
    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.

    0 讨论(0)
  • 2020-12-02 00:28

    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.

    0 讨论(0)
提交回复
热议问题