天天看點

使用子產品化擴充iOS應用介紹總覽初始方案解決方案什麼是子產品?通路另一個子產品子產品公共接口實作子產品依賴管理器解決依賴結論示例項目

介紹

本文我将主要說明如何将大型和舊代碼庫 子產品化 為小型、獨立且可測試的元件。在此過程中,我們還将分享我們采用的一些解決方案,以解決 依賴關系管理 并在持續傳遞配置中優化 建構性能 。

總覽

這是我們應用程式代碼庫在其初始形狀時的樣子。

使用子產品化擴充iOS應用介紹總覽初始方案解決方案什麼是子產品?通路另一個子產品子產品公共接口實作子產品依賴管理器解決依賴結論示例項目

Pods是CocoaPods項目,其中包含整個應用程式依賴項。

在我們開始的時候,這很簡單,可以建構所需的大多數功能。但是,您可能會想像到,通過這種方法,任何新功能都會與數百個其他功能一起添加到混合中,并且 在關鍵邊界 (例如UI,API,資料等) 之間沒有明确的分隔 。

結果,錯誤、崩潰和令人頭痛的事數量增加了,這主要是由于工程師團隊的不斷增長以及對建構新功能的需求不斷增加所帶來的不可預測的副作用。

初始方案

随着時間的推移,事情變得越來越糟,我們決定集中精力在代碼庫的主要宏大元件之間建立 清晰的邊界 。是以,我們決定采用傳統的 分層體系結構 。

使用子產品化擴充iOS應用介紹總覽初始方案解決方案什麼是子產品?通路另一個子產品子產品公共接口實作子產品依賴管理器解決依賴結論示例項目

每個框代表添加到工作區中的Xcode項目,其中每個框都包含一個dynamic framework。主APP的target仍将位于“舊功能(Old Features)”項目中,并将嵌入所有其他frameworks。

  • 藍色層:包含用于所有新功能(New Features主要是視圖控制器)和樣式邏輯的UI元件,并且所有功能(包括舊功能)都使用這些元件。
  • 綠色層:包含非UI邏輯,例如DB和API用戶端。所有上層都可以直接通路。
  • 黃色層:包含utility方法和函數,它們完全獨立于所有其他功能的邏輯,是以在整個代碼庫中共享。

我們開始看到直接的好處,尤其是對所有新功能的好處,實際上使之更加穩定和強大。

不幸的是,我們後來意識到這種方案僅節省了我們一些時間,實際上,随着我們不斷增加地 慢的建構時間 展現,這些層開始像原始的一樣逐漸發展成大型整體!

解決方案

使用子產品化擴充iOS應用介紹總覽初始方案解決方案什麼是子產品?通路另一個子產品子產品公共接口實作子產品依賴管理器解決依賴結論示例項目
  • 大的UI層現在分為多個子產品,根據其定義域分開。
  • 核心服務和核心UI包含每個子產品都需要共享的低級元件(例如API用戶端,字型,顔色等)。
  • 該 的CocoaPods 項目已經一去不複返了!現在,每個子產品都僅連結所需的依賴,而這些依賴會預先建構成動态庫(dynamic frameworks)。

每當我們添加新功能時,這都給我們帶來了更多的穩定性,還使我們能夠安全地嘗試新的設計模式。

我們還大大降低了pull請求合并時産生沖突的風險,因為每個開發人員很可能将在單個定義域中工作。

那建構時間呢?我們采用的方案非常簡單:我們隻建構已更改的内容;我們僅對所需内容進行測試。這要歸功于 Carthage ,它為第三方項目提供了輕松的緩存。

下面将展示了以上所有内容在實踐中的方式:我們的第一個 子產品 Chat。我們還将說明我們如何大規模地向這個新世界遷移。

什麼是子產品?

這是來自維基百科的 子產品化程式設計 的定義:

子產品化程式設計 是一種軟體設計技術,它強調将程式的功能分成獨立的,可互換的 子產品 ,以使每個 子產品 都包含僅執行所需功能的一個方面所必需的所有内容。

對我們來說,子產品包含屬于同一功能定義域的代碼。看起來像這樣:

使用子產品化擴充iOS應用介紹總覽初始方案解決方案什麼是子產品?通路另一個子產品子產品公共接口實作子產品依賴管理器解決依賴結論示例項目

在專用Xcode項目中定義為動态庫(dynamic framework)的子產品的視圖。

此庫(包括其單元測試)中定義了每種功能所需的代碼和資源。

這些實作細節中的大多數都将隔離在此子產品(或庫)的範圍内。通過庫公共接口僅公開對外部使用者有用的接口。

當一個子產品需要通路另一個子產品時,這個起着重要的作用。讓我們解釋一下。

通路另一個子產品

使用子產品化擴充iOS應用介紹總覽初始方案解決方案什麼是子產品?通路另一個子產品子產品公共接口實作子產品依賴管理器解決依賴結論示例項目

子產品A無法直接通路子產品B

我們希望每個子產品都完全獨立。這樣,如果修改子產品A,則不會對其任何使用者産生影響。這也意味着我們隻需要重新編譯修改後的子產品,而不是整個項目。

何時需要通路另一個子產品呢?

示例:我在子產品A中,我想導航到子產品B中定義的螢幕。代碼如下所示:

func navigateToB() {
    //從依賴項管理器擷取ModuleB的執行個體
    let module = Dependencies.shared.moduleB()
    //從ModuleB擷取screenB的視圖控制器的執行個體
    let vc = module.screenB(input)
    // Push視圖控制器
    navigationController.pushViewController(vc, animated: true)
}
           

通過這種方式,子產品可以解除耦合:我們不需要知道要導航到的視圖控制器的具體類型,也不需要知道如何配置它。我們隻需将任何輸入傳遞給factory方法即可獲得準備好呈現的泛型執行個體。

我們将解釋Dependencies類是什麼,但首先讓我們定義子產品公共接口。

子產品公共接口

每個子產品将其公共接口定義為Swift協定,描述将在外部公開的行為。所有這些接口都定義在公共的“Dependencies”層中。

public protocol ChatModuleProtocol {
    //傳回一個新的視圖控制器,用于顯示對話清單
    func conversationsScreen() -> UIViewController
 
    //傳回一個新的視圖控制器,用于與使用者就産品進行聊天(可選)
    func messagesScreen(user: User, product: Product?) -> UIViewController
 
    //傳回一個新對象,該對象可用于在背景發送消息。有關更多資訊,請參見ChatMessageSender。
    func messageSender(to receiver: User, about product: Product?) -> ChatMessageSender
}
 
//可用于發送有關特定對話的消息的對象。
public protocol ChatMessageSender {
    //發送帶有正文的新消息。完成處理程式将在完成時被調用。如果成功,則為建立的新消息提供有效的ID。
    func sendNewMessage(with body: String, completion: @escaping (_ messageId: String?) -> Void)
}
           

該子產品可以公開視圖控制器和用于執行任何類型背景任務的普通對象。無論哪種情況,這些對象的真實類型都不會透露給使用者。

現在讓我們看一個具體的實作。

實作子產品

每個子產品都必須符合其公共接口。這是通過在子產品庫内定義的此類來完成的:

import Dependencies
 
public class ChatModule: ChatModuleProtcol {
    public init() {}
    
    public func messageSender(to receiver: User, about product: Product?) -> ChatMessageSender { 
        //配置并傳回一個對象,用于在背景發送msg
    }
    public func conversationsScreen() -> UIViewController {
        //配置并傳回視圖控制器
    }
    public func messagesScreen(user: User, product: Product?) -> UIViewController {
        //配置并傳回視圖控制器
    }
}
           

現在,我們需要一種途徑來請求該子產品的執行個體并使用它。這是由依賴管理器完成的。

依賴管理器

public final class Dependencies: DependencyManager {
    //我們通過單例将其公開給每個子產品
    public static let shared = Dependencies()
}
 
extension Dependencies {
    //現在我們可以獲得一個ChatModule
    public var chatModule: ChatModuleProtocol {
        return resolve(ChatModuleProtocol.self)!
    }
}
           

注意:此類繼承自DependencyManager,這是用于注冊和解析依賴項的簡單類。如果您想了解更多詳細資訊,請在部落格文章末尾檢視我們的示例項目。

現在,我們有一種途徑來請求子產品的執行個體。我們隻需要最後一件事就可以使用它,告訴依賴管理器如何解決依賴。

解決依賴

主APP的target是為這些依賴關系提供具體實作的好地方,因為它“位于”任何子產品之上。

import Dependencies
import Chat

//在應用程式啟動完成後調用此函數。
func registerDependencies() {
    let dependencies = Dependencies.shared
    
    dependencies.register(ChatModuleProtocol.self) {
        return ChatModule()
    }
}
           

注意,這是整個項目中導入Chat子產品的唯一位置。

每次任何子產品發生更改時,都會重新編譯主APP的target。是以,它應該非常輕巧且編譯迅速。

而已!現在,我們可以從APP中的任何位置使用新子產品。

結論

以下是我們所做工作的簡要概述:

使用子產品化擴充iOS應用介紹總覽初始方案解決方案什麼是子產品?通路另一個子產品子產品公共接口實作子產品依賴管理器解決依賴結論示例項目
使用子產品化擴充iOS應用介紹總覽初始方案解決方案什麼是子產品?通路另一個子產品子產品公共接口實作子產品依賴管理器解決依賴結論示例項目

遷移之前(左)和遷移後(右)的項目。 Utils已被省略,因為沒有實質性的變化。

這個過程可能會變得非常複雜,是以您需要一次 遞增地處理 一個子產品。

大緻了解在開始遷移之前需要投入多少精力是很重要的。 評估工作量的 一種簡單方法是:

  1. 确定屬于您的定義域的核心檔案。
  2. 建立一個全新的Xcode項目(在工作區外部),然後将那些檔案複制到那裡。
  3. 嘗試編譯。這将顯示所有隐式依賴關系。
  4. 列出這些依賴項并相應地計劃遷移。
  5. 從“較低”層(核心服務/核心UI)開始,然後向上移動,因為這将有助于達到具有有效綠色建構的中間階段。

不幸的是,建立新的Xcode項目,将其添加到您的工作區并将其與正确的frameworks連結時涉及很多 樣闆檔案 。記住重複使完美。專注于您的目标,稍後您将找到優化此目标的方法。

確定整個團隊做出貢獻,實作協作并促進回報共享。在我們的案例中,我們發現 記錄整個過程 非常有價值。

建構性能仍然是我們正在研究的問題。但是,我們剛剛描述的工作使我們能夠引入 Carthage 來管理第三方依賴并緩存動态庫。展望未來,我們也希望将這種方案也應用于我們的内部frameworks,以便我們僅建構和測試所需的東西。

我們期待與您盡快分享更多更新,并聽聽您的回報!

示例項目

從我們的倉庫中下載下傳示例項目:

https://gitee.com/zhi_lei/my-first-module.git