使用协议在 Swift 中构建可组合对象

Building composable objects in Swift with protocols

我正在尝试创建一种在 Swift 中构建可比较对象的方法。我觉得我几乎已经掌握了我所拥有的,但它仍然不是 100% 正确。

我的目标是拥有一个 FlowController 对象,它可以创建我们的 UIViewControllers,然后为他们提供他们需要的任何依赖项。

我还想做的是让这项工作尽可能松散。

我这里有一个小例子可以工作但并不理想。我会解释...

这里有两个对象可以用作组件...WalletUser

class Wallet {
    func topUp(amount: Int) {
        print("Top up wallet with £\(amount)")
    }
}

class User {
    func sayHello() {
        Print("Hello, world!")
    }
}

然后我们定义一个 Component 枚举,其中包含每个案例...

enum Component {
    case Wallet
    case User
}

... 以及定义方法 requiresComponents 的协议,该方法 return 是 Components.

的数组

问题就出在这里。为了让“工厂对象”将组件放入 Composable 对象中,我们还需要在协议中定义 userwallet 属性。

protocol Composable {
    var user: User? {get set}
    var wallet: Wallet? {get set}
    
    func requiresComponents() -> [Component]
}

为了使这些属性成为“可选”(非可选),我定义了 Composable 协议的扩展,将这些变量定义为 nil。

extension Composable {
    var user: User? {
        get {return nil}
        set {}
    }
    var wallet: Wallet? {
        get {return nil}
        set {}
    }
}

现在我声明我想制作 Composable 的 class。如您所见,它需要 User 组件并声明变量。

class SomeComposableClass: Composable {
    var user: User?
    
    func requiresComponents() -> [Component] {
        return [.User]
    }
}

现在 FlowController 将创建这些并向其中添加组件。您可以在这里看到,我必须获取该对象,创建它的本地 var 版本,然后 return 更新的对象。我认为这是因为它不知道将符合协议的对象类型,因此无法更改参数。

class FlowController {
    func addComponents<T: Composable>(toComposableObject object: T) -> T {
        var localObject = object
        
        for component in object.requiresComponents() {
            switch component {
            case .Wallet:
                localObject.wallet = Wallet()
                print("Wallet")
            case .User:
                localObject.user = User()
                print("User")
            }
        }
        
        return localObject
    }
}

我在这里创建对象。

let flowController = FlowController()
let composable = SomeComposableClass()

我在这里添加组件。在生产中,这将全部在 FlowController.

内完成
flowController.addComponents(toComposableObject: composable)  // prints "User" when adding the user component
compassable.user?.sayHello()  // prints "Hello, world!"

如您所见,它在这里有效。添加了用户对象。

不过,正如你也看到的那样。因为我已经在协议中声明了变量,所以 composable 对象也有一个对 wallet 组件的引用(尽管它总是 nil)。

composable.wallet  // nil

我觉得我已经完成了大约 95%,但我希望能够做的是改进属性的声明方式。我想要的是最后一行...... composable.wallet 是一个编译错误。

我可以通过将属性声明移出协议来做到这一点,但我遇到了无法将属性添加到任何符合 Composable 协议的对象的问题。

如果工厂对象能够在不依赖声明的情况下添加属性,那就太棒了。或者甚至有某种守卫说“如果这个对象有一个 属性 调用用户,然后将用户组件添加到它”。或者类似的东西。

如果有人知道我如何让其他 5% 的工作正常进行,那就太棒了。就像我说的,这行得通,只是不是以一种理想的方式。

谢谢 :D

Hacky 编辑

嗯......作为一个快速俗气,可怕的,“没有人应该这样做”的编辑。我已经将我的协议扩展更改为这样...

extension Composable {
    var user: User? {
        get {fatalError("Access user")}
        set {fatalError("Set user")}
    }
    var wallet: Wallet? {
        get {fatalError("Access wallet")}
        set {fatalError("Set waller")}
    }
}

现在,如果我试图访问一个我没有定义的变量,至少程序会崩溃。但还是不够理想。

阅读 Daniel 的博客后编辑

好的,我想我已经完成了我想要的。只是不确定它是不是 Swifty。虽然,我也觉得有可能。寻找第二意见:)

所以,我的组件和协议变成了这个...

// these are unchanged
class Wallet {
    func topUp(amount: Int) {
        print("Top up wallet with £\(amount)")
    }
}

// each component gets a protocol    
protocol WalletComposing {
    var wallet: Wallet? {get set}
}

class User {
    func sayHello() {
        print("Hello, world!")
    }
}

protocol UserComposing {
    var user: User? {get set}
}

现在工厂方法变了...

// this is the bit I'm unsure about.
// I now have to check for conformance to each protocol
// and add the components accordingly.
// does this look OK?
func addComponents(toComposableObject object: AnyObject) {
    if var localObject = object as? UserComposing {
        localObject.user = User()
        print("User")
    }
    
    if var localObject = object as? WalletComposing {
        localObject.wallet = Wallet()
        print("Wallet")
    }
}

这允许我这样做...

class SomeComposableClass: UserComposing {
    var user: User?
}

class OtherClass: UserComposing, WalletComposing {
    var user: User?
    var wallet: Wallet?
}

let flowController = FlowController()

let composable = SomeComposableClass()
flowController.addComponents(toComposableObject: composable)
composable.user?.sayHello()
composable.wallet?.topUp(amount: 20) // this is now a compile time error which is what I wanted :D

let other = OtherClass()
flowController.addComponents(toComposableObject: other)
other.user?.sayHello()
other.wallet?.topUp(amount: 10)

这似乎是应用 Interface Segregation Principle

的好案例

具体来说,不是拥有一个主 Composable 协议,而是拥有许多较小的协议,例如 UserComposingWalletComposing。然后你的具体类型希望组成这些不同的特征,只需将它们的 "requiredComponents" 列为它们符合的协议,即:

class FlowController : UserComposing, WalletComposing 

我居然写了一个blog post that talks about this more extensively and gives more detailed examples at http://www.danielhall.io/a-swift-y-approach-to-dependency-injection


更新:

查看更新后的问题和示例代码,我只建议进行以下改进:

回到您的原始设计,定义一个基础 Composing 协议可能是有意义的,该协议需要任何符合 class 的内容来为组合特征创建存储作为字典。像这样:

protocol Composing : class {
    var traitDictionary:[String:Any] { get, set }
}

然后,使用协议扩展将实际的可组合特征添加为计算 属性,这减少了必须在每个符合的 class 中创建这些属性的样板文件。这样 any class 可以符合任意数量的特征协议,而不必为每个协议声明一个特定的 var。这是一个更完整的示例实现:

class FlowController {
    static func userFor(instance:UserComposing) -> User {
        return User()
    }
    static func walletFor(instance:WalletComposing) -> Wallet {
        return Wallet()
    }
}

protocol Composing : class {
    var traitDictionary:[String:Any] { get, set }
}

protocol UserComposing : Composing {}
extension UserComposing {
    var user:User {
        get {
            if let user = traitDictionary["user"] as? User {
                return user
            }
            else {
                let user = FlowController.userFor(self)
                traitDictionary["user"] = user
                return user
            }
        }
    }
}

protocol WalletComposing {}
extension WalletComposing {
    var wallet:Wallet {
        get {
            if let wallet = traitDictionary["wallet"] as? Wallet {
                return wallet
            }
            else {
                let wallet = FlowController.walletFor(self)
                traitDictionary["wallet"] = wallet
                return wallet
            }
        }
    }
}

class AbstractComposing {
    var traitDictionary = [String:Any]()
}

这不仅摆脱了那些你必须在任何地方解包的讨厌的选项,而且它使用户和钱包的注入隐式和自动。这意味着您的 classes 即使在它们自己的初始化器中也已经具有这些特征的正确值,无需每次都显式地将每个新实例传递给 FlowController 的实例。

例如,您的最后一个代码片段现在将变为:

class SomeComposableClass: AbstractComposing, UserComposing {} // no need to declare var anymore

class OtherClass: AbstractComposing, UserComposing, WalletComposing {} //no vars here either!

let composable = SomeComposableClass() // No need to instantiate FlowController and pass in this instance
composable.user.sayHello() // No unwrapping the optional, this is guaranteed
composable.wallet.topUp(amount: 20) // this is still a compile time error which is what you wanted :D

let other = OtherClass() // No need to instantiate FlowController and pass in this instance
other.user.sayHello()
other.wallet.topUp(amount: 10) // It all "just works"  ;)