作者: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。