協議中的私有屬性

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

在 Swift 中,協議中聲明的屬性沒有訪問控制的能力。若是協議中列出了某個屬性,則必須使遵照協議的類型顯式聲明這些屬性。github

不過有些時候,儘管你會在協議中聲明一些屬性,但你是要利用這些屬性來提供你的實現,並不但願這些屬性在類型的外部被使用。讓咱們看看如何解決這個問題。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 對象時,會像這樣作:spa

// 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

因此問題是咱們每次都要聲明一些屬性——即便它只是一些實現細節,並且這些實現細節會經過外部接口被泄漏,從而容許類的訪問者作一些本不該該被容許的事,例如:code

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

相關文章
相關標籤/搜索