詳解Dagger2(註解框架)
為什麼使用依賴注入
首先我們需要知道,人們在很長的一段時間裡都是利用控制反轉原則規定:應用程式的流程取決於在程式執行時物件圖的建立。通過抽象定義的物件互動可以實現這樣的動態流程。而使用依賴注入技術或者服務定位器便可以完成執行時繫結。
使用依賴注入可以帶來以下好處:
-
依賴的注入和配置獨立於元件之外。
-
因為物件是在一個獨立、不耦合的地方初始化,所以當注入抽象方法的時候,我們只需要修改物件的實現方法,而不用大改程式碼庫。
-
依賴可以注入到一個元件中:我們可以注入這些依賴的模擬實現,這樣使得測試更加簡單。
可以看到,能夠管理建立例項的範圍是一件非常棒的事情。按我的觀點,你app中的所有物件或者協作者都不應該知道有關例項建立和生命週期的任何事情,這些都應該由我們的依賴注入框架管理的。
什麼是JSR-330?
為了最大程度的提高程式碼的複用性、測試性和維護性,java的依賴注入為注入類中的使用定義了一整套註解(和介面)標準。Dagger1和Dagger2(還有Guice)都是基於這套標準,給程式帶來了穩定性和標準的依賴注入方法。
Dagger1
這個版本不是這篇文章的重點,所以我只是簡略地說一下。不管怎樣,Dagger1還是做了很多的貢獻,可以說是如今Android上最流行的依賴注入框架。它是由Square公司受到Guice啟發建立的。
基本特點:
-
多個注入點:依賴,通過injected
-
多種繫結方法:依賴,通過provided
-
多個modules:實現某種功能的繫結集合
-
多個物件圖: 實現一個範圍的modules集合
Dagger1是在編譯的時候實行繫結,不過也用到了反射機制。但這個反射不是用來例項化物件的,而是用於圖的構成。Dagger會在執行的時候去檢測是否一切都正常工作,所以使用的時候會付出一些代價:偶爾會無效和除錯困難。
Dagger2
Dagger2是Dagger1的分支,由谷歌公司接手開發,目前的版本是2.0。Dagger2是受到AutoValue專案的啟發。 剛開始,Dagger2解決問題的基本思想是:利用生成和寫的程式碼混合達到看似所有的產生和提供依賴的程式碼都是手寫的樣子。
如果我們將Dagger2和1比較,他們兩個在很多方面都非常相似,但也有很重要的區別,如下:
-
再也沒有使用反射:圖的驗證、配置和預先設定都在編譯的時候執行。
-
容易除錯和可跟蹤:完全具體地呼叫提供和建立的堆疊
-
更好的效能:谷歌聲稱他們提高了13%的處理效能
-
程式碼混淆:使用派遣方法,就如同自己寫的程式碼一樣
當然所有這些很棒的特點都需要付出一個代價,那就是缺乏靈活性,例如:Dagger2沒用反射所以沒有動態機制。
深入研究
想要了解Dagger2,就必須要知道依賴注入的基礎和這其中的每一個概念:
-
@Inject: 通常在需要依賴的地方使用這個註解。換句話說,你用它告訴Dagger這個類或者欄位需要依賴注入。這樣,Dagger就會構造一個這個類的例項並滿足他們的依賴。
-
@Module: Modules類裡面的方法專門提供依賴,所以我們定義一個類,用@Module註解,這樣Dagger在構造類的例項的時候,就知道從哪裡去找到需要的 依賴。modules的一個重要特徵是它們設計為分割槽並組合在一起(比如說,在我們的app中可以有多個組成在一起的modules)。
-
@Provide: 在modules中,我們定義的方法是用這個註解,以此來告訴Dagger我們想要構造物件並提供這些依賴。
-
@Component: Components從根本上來說就是一個注入器,也可以說是@Inject和@Module的橋樑,它的主要作用就是連線這兩個部分。 Components可以提供所有定義了的型別的例項,比如:我們必須用@Component註解一個介面然後列出所有的@Modules組成該元件,如 果缺失了任何一塊都會在編譯的時候報錯。所有的元件都可以通過它的modules知道依賴的範圍。
-
@Scope: Scopes可是非常的有用,Dagger2可以通過自定義註解限定註解作用域。後面會演示一個例子,這是一個非常強大的特點,因為就如前面說的一樣,沒 必要讓每個物件都去了解如何管理他們的例項。在scope的例子中,我們用自定義的@PerActivity註解一個類,所以這個物件存活時間就和 activity的一樣。簡單來說就是我們可以定義所有範圍的粒度(@PerFragment, @PerUser, 等等)。
-
Qualifier: 當類的型別不足以鑑別一個依賴的時候,我們就可以使用這個註解標示。例如:在Android中,我們會需要不同型別的context,所以我們就可以定義 qualifier註解“@ForApplication”和“@ForActivity”,這樣當注入一個context的時候,我們就可以告訴 Dagger我們想要哪種型別的context。
不廢話上程式碼
前面已經講了很多理論了,所以接下來讓我們看看如何使用Dagger2。首先還是要在我們的build.gradle檔案中如下配置:
apply plugin: 'com.neenbedankt.android-apt' buildscript { repositories { jcenter() } dependencies { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' } } android { ... } dependencies { apt 'com.google.dagger:dagger-compiler:2.0' compile 'com.google.dagger:dagger:2.0' ... }
如上所示,我們添加了編譯和執行庫,還有必不可少的apt外掛,沒有這外掛,dagger可能不會正常工作,特別是在Android studio中。
例子
幾個月前,我寫了一篇關於如何在Android上實現bob叔叔的清晰架構的文章,強烈建議大家去看一下,看完之後,你將會對我們現在做的事情有更好的理解。言歸正傳,在我以前的方案中,構造和提供大多數物件的依賴的時候,會遇到問題,具體如下(見評註):
@Override void initializePresenter() { // All this dependency initialization could have been avoided by using a // dependency injection framework. But in this case this is used this way for // LEARNING EXAMPLE PURPOSE. ThreadExecutor threadExecutor = JobExecutor.getInstance(); PostExecutionThread postExecutionThread = UIThread.getInstance(); JsonSerializer userCacheSerializer = new JsonSerializer(); UserCache userCache = UserCacheImpl.getInstance(getActivity(), userCacheSerializer, FileManager.getInstance(), threadExecutor); UserDataStoreFactory userDataStoreFactory = new UserDataStoreFactory(this.getContext(), userCache); UserEntityDataMapper userEntityDataMapper = new UserEntityDataMapper(); UserRepository userRepository = UserDataRepository.getInstance(userDataStoreFactory, userEntityDataMapper); GetUserDetailsUseCase getUserDetailsUseCase = new GetUserDetailsUseCaseImpl(userRepository, threadExecutor, postExecutionThread); UserModelDataMapper userModelDataMapper = new UserModelDataMapper(); this.userDetailsPresenter = new UserDetailsPresenter(this, getUserDetailsUseCase, userModelDataMapper); }
可以看出,解決這個問題的辦法是使用依賴注入框架。我們要避免像上面這樣引用程式碼:這個類不能涉及物件的建立和依賴的提供。 那我們該怎麼做呢,當然是使用Dagger2,我們先看看結構圖:
接下來我們會分解這張圖,並解釋各個部分還有程式碼。
Application Component: 生命週期跟Application一樣的元件。可注入到AndroidApplication和BaseActivity中類中。
@Singleton // Constraints this component to one-per-application or unscoped bindings. @Component(modules = ApplicationModule.class) public interface ApplicationComponent { void inject(BaseActivity baseActivity); //Exposed to sub-graphs. Context context(); ThreadExecutor threadExecutor(); PostExecutionThread postExecutionThread(); UserRepository userRepository(); }
我為這個元件使用了@Singleton註解,使其保證唯一性。也許你會問為什麼我要將context和其他成員暴露出去。這正是Dagger中 components工作的重要性質:如果你不想把modules的型別暴露出來,那麼你就只能顯示地使用它們。在這個例子中,我把這些元素暴露給子圖, 如果你把他們刪掉,編譯的時候就會報錯。
Application Module: module的作用是提供在應用的生命週期中存活的物件。這也是為什麼@Provide註解的方法要用@Singleton限定。
@Module public class ApplicationModule { private final AndroidApplication application; public ApplicationModule(AndroidApplication application) { this.application = application; } @Provides @Singleton Context provideApplicationContext() { return this.application; } @Provides @Singleton Navigator provideNavigator() { return new Navigator(); } @Provides @Singleton ThreadExecutor provideThreadExecutor(JobExecutor jobExecutor) { return jobExecutor; } @Provides @Singleton PostExecutionThread providePostExecutionThread(UIThread uiThread) { return uiThread; } @Provides @Singleton UserCache provideUserCache(UserCacheImpl userCache) { return userCache; } @Provides @Singleton UserRepository provideUserRepository(UserDataRepository userDataRepository) { return userDataRepository; } }
Activity Component: 生命週期跟Activity一樣的元件。
@PerActivity @Component(dependencies = ApplicationComponent.class, modules = ActivityModule.class) public interface ActivityComponent { //Exposed to sub-graphs. Activity activity(); }
@PerActivity是一個自定義的範圍註解,作用是允許物件被記錄在正確的元件中,當然這些物件的生命週期應該遵循activity的生命週期。這是一個很好的練習,我建議你們都做一下,有以下好處:
-
注入物件到構造方法需要的activity。
-
在一個per-activity基礎上的單例使用。
-
只能在activity中使用使得全域性的物件圖保持清晰。
看下程式碼:
@Scope @Retention(RUNTIME) public @interface PerActivity {}
Activity Module: 在物件圖中,這個module把activity暴露給相關聯的類。比如在fragment中使用activity的context。
@Module public class ActivityModule { private final Activity activity; public ActivityModule(Activity activity) { this.activity = activity; } @Provides @PerActivity Activity activity() { return this.activity; } }
User Component: 繼承於ActivityComponent的元件,並用@PerActivity註解。我通常會在注入使用者相關的fragment中使用。因為 ActivityModule把activity暴露給圖了,所以在任何需要一個activity的context的時候,Dagger都可以提供注入, 沒必要再在子modules中定義了。
@PerActivity @Component(dependencies = ApplicationComponent.class, modules = {ActivityModule.class, UserModule.class}) public interface UserComponent extends ActivityComponent { void inject(UserListFragment userListFragment); void inject(UserDetailsFragment userDetailsFragment); }
User Module: 提供跟使用者相關的例項。基於我們的例子,它可以提供使用者用例。
@Module public class UserModule { @Provides @PerActivity GetUserListUseCase provideGetUserListUseCase(GetUserListUseCaseImpl getUserListUseCase) { return getUserListUseCase; } @Provides @PerActivity GetUserDetailsUseCase provideGetUserDetailsUseCase(GetUserDetailsUseCaseImpl getUserDetailsUseCase) { return getUserDetailsUseCase; } }
整合到一起
現在我們已經實現了依賴注入圖,但是我該如何注入?我們需要知道,Dagger給了我們一堆選擇用來注入依賴:
-
構造方法注入:在類的構造方法前面註釋@Inject
-
成員變數注入:在類的成員變數(非私有)前面註釋@Inject
-
函式方法注入:在函式前面註釋@Inject
這個順序是Dagger建議使用的,因為在執行的過程中,總會有一些奇怪的問題甚至是空指標,這也意味著你的依賴在物件建立的時候可能還沒有初始化 完成。這在Android的activity或者fragment中使用成員變數注入會經常遇到,因為我們沒有在它們的構造方法中使用。
回到我們的例子中,看一下我們是如何在BaseActivity中注入一個成員變數。在這個例子中,我們注入了一個叫Navigator的類,它是我們應用中負責管理導航的類。
public abstract class BaseActivity extends Activity { @Inject Navigator navigator; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.getApplicationComponent().inject(this); } protected ApplicationComponent getApplicationComponent() { return ((AndroidApplication)getApplication()).getApplicationComponent(); } protected ActivityModule getActivityModule() { return new ActivityModule(this); } }
Navigator類是成員變數注入的,由ApplicationModule裡面@Provide註解顯示提供的。最終我們初始化 component然後呼叫inject()方法注入成員變數。我們通過在Activity的onCreate()方法中呼叫 getApplicationComponent(),完成這些操作。getApplicationComponent()方法放在這兒是為了複用性,它 的主要作用是為了獲取例項化的ApplicationComponent物件。
在Fragment的presenter中我們也做了同樣的事情,這兒的獲取方法有一點不一樣,因為問我們使用的是per-activity範圍限 定的component。所以我們注入到UserDetailsFragment中的UserComponent其實是駐留在 UserDetailsActivity中的。
private UserComponent userComponent;
我們必須在activity的onCreate()方法中用下面的方式初始化。
private void initializeInjector() { this.userComponent = DaggerUserComponent.builder() .applicationComponent(getApplicationComponent()) .activityModule(getActivityModule()) .build(); }
Dagger會處理我們的註解,為components生成實現並重命名加上“Dagger”字首。因為這個是一個組合的component,所以在構建 的時候,我們必須把所有的依賴的傳進去(components和modules)。現在我們的component已經準備好了,接著為了可以滿足 fragment的依賴需求,我們寫一個獲取方法:
@Override public UserComponent getComponent() { return userComponent; }
我們現在可以利用get方法獲取建立的component,然後呼叫inject()方法將Fragment作為引數傳進去,這樣就完成了繫結UserDetailsFragment依賴。
@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); this.getComponent.inject(this); }
想要檢視完整的例子,可以去我的github.這裡面有一些地方重構了的,我可以告訴你一個重要的思想(來自官方的例子)是:
public interface HasComponent<C> { C getComponent(); }
因此,客戶端(例如fragment)可以獲取並且使用component(來自activity):
@SuppressWarnings("unchecked") protected <C> C getComponent(Class<C> componentType) { return componentType.cast(((HasComponent<C>)getActivity()).getComponent()); }
這兒使用了強制轉換,不論這個客戶端不能獲取到能用的component,但是至少很快就會失敗。如果你有任何想法能夠更好地解決這個問題,請告訴我。
Dagger2生成的程式碼
在瞭解Dagger的主要特徵之後,我們再來看看內部構造。為了舉例說明,我們還是用Navigator類,看看它是如何建立和注入的。首先我們看一下我們的DaggerApplicationComponent。
@Generated("dagger.internal.codegen.ComponentProcessor") public final class DaggerApplicationComponent implements ApplicationComponent { private Provider<Navigator> provideNavigatorProvider; private MembersInjector<BaseActivity> baseActivityMembersInjector; private DaggerApplicationComponent(Builder builder) { assert builder != null; initialize(builder); } public static Builder builder() { return new Builder(); } private void initialize(final Builder builder) { this.provideNavigatorProvider = ScopedProvider.create(ApplicationModule_ProvideNavigatorFactory.create(builder.applicationModule)); this.baseActivityMembersInjector = BaseActivity_MembersInjector.create((MembersInjector) MembersInjectors.noOp(), provideNavigatorProvider); } @Override public void inject(BaseActivity baseActivity) { baseActivityMembersInjector.injectMembers(baseActivity); } public static final class Builder { private ApplicationModule applicationModule; private Builder() { } public ApplicationComponent build() { if (applicationModule == null) { throw new IllegalStateException("applicationModule must be set"); } return new DaggerApplicationComponent(this); } public Builder applicationModule(ApplicationModule applicationModule) { if (applicationModule == null) { throw new NullPointerException("applicationModule"); } this.applicationModule = applicationModule; return this; } } }
有兩個重點需要注意。第一個:由於我們要將依賴注入到activity中,所以會得到一個注入這個比成員的注入器(由Dagger生成的BaseActivity_MembersInjector):
@Generated("dagger.internal.codegen.ComponentProcessor") public final class BaseActivity_MembersInjector implements MembersInjector<BaseActivity> { private final MembersInjector<Activity> supertypeInjector; private final Provider<Navigator> navigatorProvider; public BaseActivity_MembersInjector(MembersInjector<Activity> supertypeInjector, Provider<Navigator> navigatorProvider) { assert supertypeInjector != null; this.supertypeInjector = supertypeInjector; assert navigatorProvider != null; this.navigatorProvider = navigatorProvider; } @Override public void injectMembers(BaseActivity instance) { if (instance == null) { throw new NullPointerException("Cannot inject members into a null reference"); } supertypeInjector.injectMembers(instance); instance.navigator = navigatorProvider.get(); } public static MembersInjector<BaseActivity> create(MembersInjector<Activity> supertypeInjector, Provider<Navigator> navigatorProvider) { return new BaseActivity_MembersInjector(supertypeInjector, navigatorProvider); } }
這個注入器一般都會為所有activity的注入成員提供依賴,只要我們一呼叫inject()方法,就可以獲取需要的欄位和依賴。
第二個重點:關於我們的DaggerApplicationComponent類,我們有一個Provider,它不僅僅是一個提供例項的介面,它還是被ScopedProvider構造出來的,可以記錄建立例項的範圍。
Dagger還會為我們的Navigator類生成一個名叫ApplicationModule_ProvideNavigatorFactory的工廠,這個工廠可以傳遞上面提到的範圍引數然後得到這個範圍內的類的例項。
@Generated("dagger.internal.codegen.ComponentProcessor") public final class ApplicationModule_ProvideNavigatorFactory implements Factory<Navigator> { private final ApplicationModule module; public ApplicationModule_ProvideNavigatorFactory(ApplicationModule module) { assert module != null; this.module = module; } @Override public Navigator get() { Navigator provided = module.provideNavigator(); if (provided == null) { throw new NullPointerException("Cannot return null from a [email protected] @Provides method"); } return provided; } public static Factory<Navigator> create(ApplicationModule module) { return new ApplicationModule_ProvideNavigatorFactory(module); } }
這個類非常簡單,它代表我們的ApplicationModule(包含@Provide方法)建立了Navigator類。
總之,上面的程式碼看起來就像是手敲出來的,而且非常好理解,便於除錯。其餘還有很多可以去探索,你們可以通過除錯去看看Dagger如何完成依賴繫結的。
原始碼:
相關推薦
詳解Dagger2(註解框架)
為什麼使用依賴注入 首先我們需要知道,人們在很長的一段時間裡都是利用控制反轉原則規定:應用程式的流程取決於在程式執行時物件圖的建立。通過抽象定義的物件互動可以實現這樣的動態流程。而使用依賴注入技術或者服務定位器便可以完成執行時繫結。 使用依賴注入可以帶來以下好處:
Java自動化測試框架-12 - TestNG之xml檔案詳解篇 (詳細教程)
1.簡介 現在這篇,我們來學習TestNG.xml檔案,前面我們已經知道,TestNG就是執行這個檔案來執行測試用例的。通過本篇,你可以進一步瞭解到:這個檔案是配置測試用例,測試套件。簡單來說,利用這個檔案,我們可以跑同一個類或者多個不同類裡面的測試用例。 TestNG通過設定testng.xml檔案能做以下
angular路由詳解四(子路由)
str edr bsp 絕對路徑 pat outer menu one const 子路由是相對路由 路由配置部分: 主要是children const routes: Routes = [ {path:‘home‘, component: HomeComponent,
Scala筆記整理(四):Scala面向對象—類詳解2(繼承相關)
大數據 Scala [TOC] 單例 希望某個類只存在一個使用的對象,而不管有多少個調用者在使用它,就是單例的概念。 Java中的單例 package cn.xpleaf.single; /** * 單例模式-餓漢式(在屬性中先創建好對象,不管是否調用getInstance方法) * @auth
wifi詳解-1(基本常識)
先看一下wifi開啟流程圖: 圖中注意三個重要模組:1.WifiService(framework層) 2.netd(處於Framework與kernel之間library層,system/netd目錄下) 3.wpa_supplicant與hostapd
java程式設計師面試真題及詳解2017(純手動)
這是本人第一次面試真題及以後的每一次面試都會整理。這一次的面試題倒是挺簡單的。 注:每個題基本不會深究,知道如何解答即可。 填空題 1.Java之所以可以實現跨平臺,是因為Java程式在執行時使用了()。 JVM JVM是Ja
主席樹入門詳解一(學習筆記)(例題POJ-2104 求區間第k小)
學習主席樹,在網上搜了很多教程(都好簡短啊,直接就是幾行字就上程式碼,看不懂啊有木有~~),最後才很艱難的學會了最基礎的部分。下面就是我在學習的過程中的產生的疑惑和解決的辦法。 學習主席樹需要的前置技能:線段樹。 參考資料 1. B站上的視訊講解(話說B站真的啥都有啊)
一文詳解蒙特卡洛(Monte Carlo)法及其應用
概述 蒙特卡羅方法是一種計算方法。原理是通過大量隨機樣本,去了解一個系統,進而得到所要計算的值。 它非常強大和靈活,又相當簡單易懂,很容易實現。對於許多問題來說,它往往是最簡單的計算方法,有時甚至是唯一可行的方法。它誕生於上個世紀40年代美國的"曼哈頓計劃",名字
主席樹入門詳解二(學習筆記)(例題SPOJ
主席樹入門詳解一連結 Start~ 看了前一篇部落格,應該已經對最基礎的主席樹有了一個大概的掌握。主席樹的本質就是一堆線段樹的集合(也就是包含歷史版本的線段樹),所以需要用一堆線段樹來解決的問題,就可以用主席樹來解決。主席樹與線段樹最大的區別就是主席樹的左右兒子的節點
Python unittest詳解二(測試夾具)
split unittest 添加 per ima 函數 python .com 分享圖片 關於測試夾具,我們知道,以類為對象的話,在python裏對應的方法分別是test_isupper、test_upper, 那麽以測試case為單位呢? 這時候,就要提到我們的
SSM框架開發專案--用到的註解記錄,及詳解。(先記錄,後研究,再總結---未完)
@JsonSerialize(include = JsonSerialize.Inclusion.NON_NULL) //保證序列化json的時候,如果是null的物件,key也會消失 public c
python+requests介面自動化測試框架例項詳解教程(米兔888)
前段時間由於公司測試方向的轉型,由原來的web頁面功能測試轉變成介面測試,之前大多都是手工進行,利用postman和jmeter進行的介面測試,後來,組內有人講原先web自動化的測試框架移駕成介面的自動化框架,使用的是java語言,但對於一個學java,卻在學python的我
ggplot2作圖詳解7(完):主題(theme)設置
clas base 參數 由於 end 文字標簽 cut evaluate minor 凡是和數據無關的圖形設置內容理論上都可以歸為主題類,但考慮到一些內容(如坐標軸)的特殊性,可以允許例外的情況。主題的設置相當繁瑣,很容易就占用了 大量的作圖時間,應盡量把這些東西簡化,把
搬家行業開發小程序系統(App.Config詳解及讀寫操作)
設置 反饋 流程 選擇 文件的 搬家 功能 操作 服務項目 應用程序配置文件是標準的 XML 文件,XML 標記和屬性是區分大小寫的。它是可以按需要更改的,開發人員可以使用配置文件來更改設置,而不必重編譯應用程序。配置文件的根節點是configuration。我們經常訪問的
Python列表list詳解篇(七)
python list 介紹:列表是最常用的python數據類型,它可以作為一個方括號內的逗號用分隔值出現。列表的數據項不需要具有相同的類型。創建一個列表,只要逗號分隔的不同數據項用方括號括起來即可。name=[‘’beijing,’shenzhen’,’nanjing’] 訪問列表的值:(列表的下標
tomcat中server.xml配置詳解(轉載)(一)
重要 lis 結構 更多 tle 處理請求 服務器端 sta 設置 轉載自:https://www.cnblogs.com/starhu/p/5599773.html tomcat中server.xml配置詳解 Tomcat Server的結構圖如下:(該文件描述了如何
tomcat中server.xml配置詳解(轉載)(二)
lin power servlet容器 secure redirect tar 屬性 限制 man 轉載自:https://www.cnblogs.com/starhu/p/5599773.html 一:<Connector>元素 由Connector接口定義.
JDBC詳解系列(二)之加載驅動
red mar mys ons try path 替換 host man ---[來自我的CSDN博客](http://blog.csdn.net/weixin_37139197/article/details/78838091)--- ??在JDBC詳解系列(一)之流程中
Zookeeper詳解-概述(一)
可靠的 配置 應用程序 們的 共享 主機 專註 會有 減少 ZooKeeper是一種分布式協調服務,用於管理大型主機。在分布式環境中協調和管理服務是一個復雜的過程。ZooKeeper通過其簡單的架構和API解決了這個問題。ZooKeeper允許開發人員專註於核心應用程序邏輯
Zookeeper詳解-基礎(二)
ima con 下一個序列 新的 簡單的 pos 列表 之前 集中 在深入了解ZooKeeper的運作之前,讓我們來看看ZooKeeper的基本概念。我們將在本章中討論以下主題: Architecture(架構) Hierarchical namespace(層次命名空間