協議中的私有屬性

SwiftGG翻譯組發表於2019-02-18

作者:Olivier Halligon,原文連結,原文日期:2018-09-02 譯者:灰s;校對:numbbbbb小鐵匠Linus;定稿:Forelax

在 Swift 中,協議中宣告的屬性沒有訪問控制的能力。如果協議中列出了某個屬性,則必須使遵守協議的型別顯式宣告這些屬性。

不過有些時候,儘管你會在協議中宣告一些屬性,但你是要利用這些屬性來提供你的實現,並不希望這些屬性在型別的外部被使用。讓我們看看如何解決這個問題。

一個簡單的例子

假設你需要建立一個專門的物件來管理你的檢視控制器(ViewControllers)導航,比如一個協調器(Coordinator)。

每個協調器都有一個根控制器 UINavigationController,並共享一些通用的功能,比如在它上面推進(push)和彈出(pop)其他 ViewController。所以最初它看起來可能是這樣 [1]

// Coordinator.swift

protocol Coordinator {
  var navigationController: UINavigationController { get }
  var childCoordinator: Coordinator? { get set }

  func push(viewController: UIViewController, animated: Bool)
  func present(childViewController: UIViewController, animated: Bool)
  func pop(animated: Bool)
}

extension Coordinator {
  func push(viewController: UIViewController, animated: Bool = true) {
    self.navigationController.pushViewController(viewController, animated: animated)
  }
  func present(childCoordinator: Coordinator, animated: Bool) {
    self.navigationController.present(childCoordinator.navigationController, animated: animated) { [weak self] in
      self?.childCoordinator = childCoordinator
    }
  }
  func pop(animated: Bool = true) {
    if let childCoordinator = self.childCoordinator {
      self.dismissViewController(animated: animated) { [weak self] in
        self?.childCoordinator = nil
      }
    } else {
      self.navigationController.popViewController(animated: animated)
    }
  }
}
複製程式碼

當我們想要宣告一個新的 Coordinator 物件時,會像這樣做:

// MainCoordinator.swift

class MainCoordinator: Coordinator {
  let navigationController: UINavigationController = UINavigationController()
  var childCoordinator: Coordinator?

  func showTutorialPage1() {
    let vc = makeTutorialPage(1, coordinator: self)
    self.push(viewController: vc)
  }
  func showTutorialPage2() {
    let vc = makeTutorialPage(2, coordinator: self)
    self.push(viewController: vc)
  }

  private func makeTutorialPage(_ num: Int, coordinator: Coordinator) -> UIViewController { … }
}
複製程式碼

問題:洩漏實現細節

這個解決方案在 protocol 的可見性上有兩個問題:

  • 每當我們想要宣告一個新的 Coordinator 物件,都必須顯式的宣告一個 let navigationController: UINavigationController 屬性和一個 var childCoordinator: Coordinator? 屬性。雖然,在遵守協議的型別現實中,我們並沒有顯式的使用他們 - 但它們就在那裡,因為我們需要它們作為預設的實現來供 protocol Coordinator 正常工作。
  • 我們必須宣告的這兩個屬性具有與 MainCoordinator 相同的可見性(在本例中為隱式 internal(內部) 訪問控制級別),因為這是 protocol Coordinator 的必備條件。這使得它們對外部可見,就像在編碼時可以使用 MainCoordinator

所以問題是我們每次都要宣告一些屬性——即使它只是一些實現細節,而且這些實現細節會通過外部介面被洩漏,從而允許類的訪問者做一些本不應該被允許的事,例如:

let mainCoord = MainCoordinator()
// 訪問者不應該被允許直接訪問 navigationController ,但是他們可以
mainCoord.navigationController.dismissViewController(animated: true)
// 他們也不應該被允許做這樣的事情
mainCoord.childCoordinator = mainCoord
複製程式碼

也許你會認為,既然我們不希望它們是可見的,那麼可以直接在第一段程式碼的 protocol 中不宣告這兩個屬性。但是如果我們這樣做,將無法通過 extension Coordinator 來提供預設的實現,因為預設的實現需要這兩個屬性存在以便它們的程式碼被編譯。

你可能希望 Swift 允許在協議中申明這些屬性為 fileprivate,但是在 Swift 4 中,你不能在 protocols 中使用任何訪問控制的關鍵字。

所以我們如何才能解決這個“既要提供用到這個屬性的預設實現,有不讓這些屬性對外暴露”的問題呢?

一個解決方案

實現這一點的一個技巧是將這些屬性隱藏在中間物件中,並在該物件中將對應的屬性宣告為 fileprivate

通過這種方式,儘管我們依舊在對應型別的公共介面中宣告瞭屬性,但是介面的訪問者卻不能訪問該物件的內部屬性。而我們對於協議的預設實現卻能夠訪問它們 —— 只要它們在同一個檔案中被宣告就行了(因為它們是 fileprivate )。

看起來就像這樣:

// Coordinator.swift

class CoordinatorComponents {
  fileprivate let navigationController: UINavigationController = UINavigationController()
  fileprivate var childCoordinator: Coordinator? = nil
}

protocol Coordinator: AnyObject {
  var coordinatorComponents: CoordinatorComponents { get }

  func push(viewController: UIViewController, animated: Bool)
  func present(childCoordinator: Coordinator, animated: Bool)
  func pop(animated: Bool)
}

extension Coordinator {
  func push(viewController: UIViewController, animated: Bool = true) {
    self.coordinatorComponents.navigationController.pushViewController(viewController, animated: animated)
  }
  func present(childCoordinator: Coordinator, animated: Bool = true) {
    let childVC = childCoordinator.coordinatorComponents.navigationController
    self.coordinatorComponents.navigationController.present(childVC, animated: animated) { [weak self] in
      self?.coordinatorComponents.childCoordinator = childCoordinator // retain the child strongly
    }
  }
  func pop(animated: Bool = true) {
    let privateAPI = self.coordinatorComponents
    if privateAPI.childCoordinator != nil {
      privateAPI.navigationController.dismiss(animated: animated) { [weak privateAPI] in
        privateAPI?.childCoordinator = nil
      }
    } else {
      privateAPI.navigationController.popViewController(animated: animated)
    }
  }
}
複製程式碼

現在,遵守協議的 MainCoordinator 型別:

  • 僅需要宣告一個 let coordinatorComponents = CoordinatorComponents() 屬性,並不用知道 CoordinatorComponents 型別的內部有些什麼(隱藏了實現細節)。
  • MainCoordinator.swift 檔案中,不能訪問 coordinatorComponents 的任何屬性,因為它們被宣告為 fileprivate
public class MainCoordinator: Coordinator {
  let coordinatorComponents = CoordinatorComponents()

  func showTutorialPage1() {
    let vc = makeTutorialPage(1, coordinator: self)
    self.push(viewController: vc)
  }
  func showTutorialPage2() {
    let vc = makeTutorialPage(2, coordinator: self)
    self.push(viewController: vc)
  }

  private func makeTutorialPage(_ num: Int, coordinator: Coordinator) -> UIViewController { … }
}
複製程式碼

當然,你仍然需要在遵守協議的型別中宣告 let coordinatorComponents 來提供儲存,這個宣告必須是可見的(不能是 private),因為這是遵守 protocol Coordinator 所要求的一部分。但是:

  • 只需要宣告 1 個屬性,取代之前的 2 個(在更復雜的情況下會有更多)。
  • 更重要的是,即使它可以從遵守協議的型別的實現中訪問,也可以從外部介面訪問,你卻不能對它做任何事情。

當然,你仍然可以訪問 myMainCoordinator.coordinatorComponents,但是不能使用它做任何事情,因為它所有的屬性都是 fileprivate

結論

Swift 可能無法提供你想要的所有功能。你可能希望有朝一日 protocols 允許對它宣告需要的屬性和方法使用訪問控制關鍵字,或者通過某種方式將它們在公共 API 中隱藏。

但與此同時,掌握這些技巧和變通方法可以使你的公共 API 更好、更安全,避免洩露實現細節或者訪問在實現之外不應該被修改的屬性,同時仍然使用 Mixin pattern 並提供預設實現。


[1].這是一個簡化的例子;不要將注意力集中在 Coordinator 的實現 - 它不是這個例子的重點,更應該關注的是需要在協議中宣告公開可訪問的屬性。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg

相關文章