使用协议在 Swift 中构建可组合对象
Building composable objects in Swift with protocols
我正在尝试创建一种在 Swift 中构建可比较对象的方法。我觉得我几乎已经掌握了我所拥有的,但它仍然不是 100% 正确。
我的目标是拥有一个 FlowController
对象,它可以创建我们的 UIViewControllers
,然后为他们提供他们需要的任何依赖项。
我还想做的是让这项工作尽可能松散。
我这里有一个小例子可以工作但并不理想。我会解释...
这里有两个对象可以用作组件...Wallet
和 User
。
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
对象中,我们还需要在协议中定义 user
和 wallet
属性。
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
协议,而是拥有许多较小的协议,例如 UserComposing
和 WalletComposing
。然后你的具体类型希望组成这些不同的特征,只需将它们的 "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" ;)
我正在尝试创建一种在 Swift 中构建可比较对象的方法。我觉得我几乎已经掌握了我所拥有的,但它仍然不是 100% 正确。
我的目标是拥有一个 FlowController
对象,它可以创建我们的 UIViewControllers
,然后为他们提供他们需要的任何依赖项。
我还想做的是让这项工作尽可能松散。
我这里有一个小例子可以工作但并不理想。我会解释...
这里有两个对象可以用作组件...Wallet
和 User
。
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
对象中,我们还需要在协议中定义 user
和 wallet
属性。
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
协议,而是拥有许多较小的协议,例如 UserComposing
和 WalletComposing
。然后你的具体类型希望组成这些不同的特征,只需将它们的 "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" ;)