Swift-将第三方类型与我自己的协议相一致,但要求相互冲突

时间:2018-06-21 08:06:40

标签: swift protocols conflict requirements dependency-inversion

这是水落石出的情况:

假设由Alice Allman编写的第三方框架提供了一个非常有用的类:

public class AATrackpad {
  public var cursorLocation: AAPoint = .zero
}

和鲍勃·贝尔(Bob Bell)编写的另一个框架提供了不同的类:

public class BBMouse {
  public var where_is_the_mouse: BBPoint = .zero
}

在运行时,可能需要这些类之一,具体取决于用户决定使用哪种硬件。因此,为了与Dependency Inversion Principle保持一致,我不希望自己的类型直接依赖于AATrackpadBBMouse。相反,我想定义一个描述我所需行为的协议:

protocol CursorInput {
  var cursorLocation: CGPoint { get }
}

然后让我自己的类型改用该协议:

class MyCursorDescriber {
  var cursorInput: CursorInput?

  func descriptionOfCursor () -> String {
    return "Cursor Location: \(cursorInput?.cursorLocation.description ?? "nil")"
  }
}

我希望能够使用BBMouse的实例作为光标输入,如下所示:

let myCursorDescriber = MyCursorDescriber()
myCursorDescriber.cursorInput = BBMouse()

但要对此进行编译,我必须追溯BBMouse以符合我的协议:

extension BBMouse: CursorInput {
  var cursorLocation: CGPoint {
    return CGPoint(x: self.where_is_the_mouse.x, y: self.where_is_the_mouse.y)
  }
}

现在我已经使BBMouse符合我的CursorInput协议,我的代码可以编译,而我的体系结构就是我想要的方式。我在这里没有问题的原因是,我认为where_is_the_mouse是该属性的糟糕名称,并且我很高兴不再使用该名称。但是,AATrackpad的故事则不同。我碰巧认为Alice完美地命名了她的cursorLocation属性,并且如您所见,我希望能够为我的协议要求使用相同的名称。我的问题是AATrackpad没有使用CGPoint作为该属性的类型,而是使用了称为AAPoint的专有点类型。我的协议要求(cursorLocation)与AATrackpad的现有属性同名,但类型不同,这一事实表明我无法追溯地符合CursorInput

extension AATrackpad: CursorInput {
  var cursorLocation: CGPoint { // -- Invalid redeclaration
    return CGPoint(x: self.cursorLocation.x, y: self.cursorLocation.y) // -- Infinite recursion
  }
}

正如该代码段中的注释所述,该代码无法编译,即使确实存在,我也将在运行时面临无限递归,因为我无法具体引用{{ 1}}。像这样的方法AATrackpad会很好,但是我认为在这种情况下这没有道理。再说一次,协议一致性甚至都不会首先编译,因此解决无限递归的歧义是次要的。

考虑到所有这些上下文,我的问题是:

如果我使用协议来构建我的应用程序(出于充分的理由,广泛推荐使用该协议),那么我使用某种第三方具体类型的能力是否取决于该第三方开发人员不会分享的希望确实如此我喜欢命名约定吗?


注意:答案“仅选择一个与您要使用的类型不冲突的名称” 将不令人满意。也许一开始我只有cursorLocation,没有冲突,然后一年后,我决定也想增加对(self as? AATrackpad)?.cursorLocation的支持。我最初选择了一个好名字,现在在我的应用程序中普遍使用它-我是否应该为了一种新的具体类型而在任何地方都对其进行更改?以后要添加对BBMouse的支持(现在与我选择的任何新名称冲突)时会发生什么?我是否必须再次更改协议要求的名称​​ ?我希望您能明白为什么我要寻找比这更合理的答案。

1 个答案:

答案 0 :(得分:2)

受到乔纳斯·迈耶(Jonas Maier)的评论的启发,我发现我认为这是在架构上足以解决此问题的解决方案。如乔纳斯所说,函数重载表现出我正在寻找的行为。我开始认为也许协议要求应该永远只是功能而不是属性。按照这种思路,我的协议现在将是:

protocol CursorInput {
  func getCursorLocation () -> CGPoint
  func setCursorLocation (_ newValue: CGPoint)
}

(请注意,在这个答案中,我也将其设置为可设置的,这与原始文章不同。)

我现在可以追溯AATrackpad遵守该协议,而不会发生冲突:

extension AATrackpad: CursorInput {
  func getCursorLocation () -> CGPoint {
    return CGPoint(x: self.cursorLocation.x, y: self.cursorLocation.y)
  }
  func setCursorLocation (_ newValue: CGPoint) {
    self.cursorLocation = AAPoint(newValue)
  }
}

重要-即使AATrackpad已经具有名称相同但类型不同的函数func getCursorLocation () -> AAPoint,它仍将编译。这种行为正是我在原始帖子中的财产所要的。因此:

在协议中包含属性的主要问题是,由于名称空间冲突,它可能使某些具体类型实际上无法遵循该协议。

以这种方式解决此问题后,我要解决一个新问题:出于某种原因,我希望cursorLocation是一个属性而不是一个函数。我绝对不想被迫在我的应用程序中全部使用getPropertyName()语法。幸运的是,这可以解决,就像这样:

extension CursorInput {
  var cursorLocation: CGPoint {
    get { return self.getCursorLocation() }
    set { self.setCursorLocation(newValue) }
  }
}

这是协议扩展的很棒之处。协议扩展中声明的任何内容的行为都类似于函数的默认参数-仅在没有其他优先事项的情况下使用。由于这种不同的行为方式,当我使AATrackpad符合CursorInput时,此属性不会引起冲突。现在,我可以使用我最初想要的属性语义,而不必担心名称空间冲突。我很满意。


“请稍等-现在 AATrackpad 符合 CursorInput ,它是否有两个版本 cursorLocation ?如果我要使用 trackpad.cursorLocation ,它是 CGPoint 还是 AAPoint

工作方式是这样的-如果在该范围内该对象被称为AATrackpad,则使用Alice的原始属性:

let trackpad = AATrackpad()
type(of: trackpad.cursorLocation) // This is AAPoint

但是,如果只知道该类型为CursorInput,则使用我定义的默认属性:

let cursorInput: CursorInput = AATrackpad()
type(of: cursorInput.cursorLocation) // This is CGPoint

这意味着,如果我确实知道类型为AATrackpad,那么我可以访问以下任一版本的属性:

let trackpad = AATrackpad()
type(of: trackpad.cursorLocation) // This is AAPoint
type(of: (trackpad as CursorInput).cursorLocation) // This is CGPoint

这也意味着我的用例已完全解决,因为我特别想 not 不知道我的cursorInput是恰好是AATrackpad还是{{1 }}-只是它是某种BBMouse。因此,无论我在何处使用CursorInput,它的属性都将是我在协议扩展中定义的类型,而不是该类中定义的原始类型。


仅具有功能要求的协议就有可能引起名称空间冲突-乔纳斯(Jonas)在其评论中指出了这一点。如果协议要求之一是不带参数的函数,并且符合类型已经具有使用该名称的属性,则该类型将无法符合该协议。这就是为什么我确保命名包括动词而不是名词(cursorInput: CursorInput?)的函数的原因-如果任何第三方类型在属性名称中使用了动词,那么我可能还是不想使用它:)