Custom class clusters in Swift

前端 未结 7 2022
灰色年华
灰色年华 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: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.

提交回复
热议问题