《我用段子講.NET之依賴注入其二》
”隨著我們將業務程式碼抽象化成介面和實現兩部分,這也使得物件生命週期的統一管理成為可能。這就引發了第二個問題,.NET Core中的依賴注入框架。”
1
聽到董哥這麼說,作為一位僅有3年左右經驗的開發者小木同學一臉錯愕,雖然這句話的每個詞他都認識,但連在一起卻猶如聽天書。
”董哥,你說的第一句話,我倒是剛剛已經聽你說明白了。但這第二句話卻完全搞不懂。主要有兩個問題,第一,什麼叫做物件生命週期,其次,怎麼實現統一管理的?”
董哥莞爾一笑:”那你覺得.NET物件是如何建立和銷燬的?”
小木同學對這個話題並不陌生,他說:”物件的建立,一般用new關鍵詞來建立,物件的銷燬則一般使用物件繼承自IDisposable,並在解構函式中釋放。不過解構函式這種用法都是我在網上看到的,實際上沒用過,這就屬於物件生命週期管理吧?”
董哥說:”是的。”
小木繼續說:”那按這個說法,我感覺.NET中不用刻意維護物件生命週期,垃圾回收機制會回收超出生命週期的物件吧。 ”
“事實上雖然.NET雖然會通Gc管理物件生命週期,實際上依然會面臨這樣的問題。這有點像我們可能看不到地球的存在,但我們存在,那地球也是客觀存在的,只是平時我們沒注意觀察而已。
那你們的程式碼裡面,估計沒刻意控制過物件的建立過程,而且估計也經常使用靜態變數吧。”
“額,是啊,我司生產程式碼裡面出現得最多的,估計就是new 物件的語句,以及定義為public static的靜態變數了,有一次我認真的觀察了一下,幾乎每個類都會定義幾個靜態變數。”
聽到小木這麼說,董哥被逗樂了:“聽起來有點搞笑,不過我也知道,你們這種HIS系統,本來就是業務非常複雜,如何快速的完成專案才最重要。“
2
董哥看到小木的杯子空了,又去給他加了一點水。
這時董哥窗外的湖面也掩映著一片夕陽的燦爛,微風吹過,粼粼波光,風景倒是挺雅緻,不知不覺,已經臨近6點半,他們圍繞這個話題已經聊了快個把小時了,此時董哥他們辦公室也迎來了下班高峰期,同事們漸次的離開辦公室,但對技術愛好者來說,總是感覺不到時間流逝。
董哥回來後,把杯子遞給小木,同時,也拿起自己的杯子喝了一口,他慢條斯理的說:
“在許多業務系統開發裡面,記憶體彷彿不要錢,大不了就給醫生們多加幾根記憶體條,反正都是地方財政買單,也不會要開發人員出錢。再者,估計買記憶體條的錢,顯然比優化程式碼付出的成本低,花那麼多時間優化效能,有時確實反而付出了更高的時間成本。
而對於我們物聯網嵌入式系統開發者來說,硬體基礎設施都金貴著,有時稍微注意點,配置可能看起來差不多,但總價可以省出好幾十塊錢一臺出來。如果生產量比較多,還能多賺不少錢。當然,並不是說.NET就不行,我們有的應用其實也在用.NET Core來開發,效能還挺不錯的。“
小木也端起杯子喝了一口,回答道:
“也許只是我們公司是這樣吧,估計那些大一點的公司會好許多。也有可能是.NET入門容易,沒.NET Core那麼多套路,所以很容易就放飛了。
那在.NET Core裡面依賴注入框架是如何統一管理物件生命週期的?”
3
董哥繼續拿起了那張畫了一棵樹的紙,他說:”不僅物件間的依賴關係本身像一棵樹,物件的生命週期管理,其實也像這一棵樹。首先是主程式不斷的new物件,然後物件再不斷的new子物件。就像主幹長出枝幹,枝幹長出葉子,結構越來越複雜。但物件如果不斷的堆積在記憶體中,就會引發記憶體洩漏(oom)問題,導致程式崩潰。你聽過值物件和引用物件麼?“
小木點了點頭:“聽說值物件放在棧上,引用物件放在堆上。”
“是的,當然,值物件在業務開發中可能用得沒那麼多,所以造成記憶體佔用的,主要還是放在堆上的物件。垃圾回收策略倒是隻有一句話,引用計數,分代回收,物件壓縮。0代,1代,2代,大物件堆,分別有不同的回收策略。不過這些可能對你來說理解暫時有點困難,有機會再慢慢說。“
”比較容易理解的可能是,物件回收並非一直在發生,它只有在系統實體記憶體不足,達到記憶體回收閾值,手動呼叫GC.Collect方法等情況下發生。而且垃圾回收過程並非對程式毫無副作用,在某些效能要求特別高的場景,例如高併發場景下,發生計劃外的垃圾回收,可能會導致程式偶發性變得卡頓,可能會造成使用者體驗度的下降。這也是痛點,也就是依賴注入框架所能解決的第二個問題。統一的生命週期管理“
董哥突然變得嚴肅起來。
“在.NET Core中,ASP.NET Core內建了依賴注入容器IServiceCollection和服務提供者IServiceProvider。對開發者來說,通過建立抽象,提取出介面或基類,再將該物件註冊到依賴注入容器中。
在服務呼叫時,上層應用不再通過new的形式使用下層介面,而是在建構函式中,通過引入依賴注入框架例項化的下層實現,並由該框架來統一管理物件的生命週期。
這就是依賴注入框架所採取的手段。”
4
“聽起來還是挺不錯的,就是對開發者的操作習慣造成了一些影響。”木哥嘆息道。
董哥點了點頭,“對許多開發者來說,改變習慣如同斷人財路。我也曾經見過很多開發者特別喜歡使用全域性靜態變數,彷彿不用靜態變數就不會寫程式碼了。事實上對專業開發者來說,出現的每一個靜態變數都會讓其產生很痛苦的感覺。
首先,全域性靜態變數由root根來進行管理的物件,除非應用程式退出,這些物件幾乎不會釋放。如果開發者再玩一些騷操作,例如用全域性靜態變數來存放集合,有時會成為gc的一場災難片。
其次,全域性靜態變數就是單例物件,全域性只會出現一次例項,併發訪問時,很容易就會引發執行緒安全問題。執行緒安全問題看起來簡單,實則是軟體開發中最難處理的一類問題了。為了一勞永逸,有的開發者習慣於用lock
語句來強制使之同步化,這又使得應用程式很難充分利用硬體效能帶來的優勢。
所以,開發者有時改變一些思維定勢,例如儘量不要出現全域性靜態變數,如果一定要出現,也推薦使用依賴注入框架所提供的單例生命週期來管理,這樣使得整個應用系統的生命週期管理都處於可控狀態。當然,全域性靜態方法是允許存在的,類似於 public static string xxx
這樣的擴充套件方法,可以使開發者的程式碼變得非常優雅。
在我看來,定義介面,通過依賴注入框架註冊物件,通過建構函式建立物件,看起來非常簡單的三個步驟,卻有可能使應用系統開發變得更加的高可控,僅就這麼一個小小的變化,就已經顯得.NET Core開發比傳統.NET 開發更加的充滿魅力和神奇。“
5
董哥一時激動,講了許多在木哥看來有點完全搞不懂的概念。這也使得小木突然間一臉懵逼,不知道該如何接下這個話題,半響之後,他才逐漸吐出一個問題:”董哥,你說的單例生命週期是啥意思?我在.NET Core開發中好像隱約看到過,但有點不太理解它的含義,你能跟我簡單講一下麼。“
董哥也意識到自己突然間聊嗨了,兀自的笑了一笑,然後說:“
在.NET Core提供的依賴注入框架中,提供了三種不同的生命週期。
第一種就是SingleTon單例生命週期,這種模式有點像設計模式中的單例模式,通俗而言,就是在軟體的整個生命週期只會出現唯一一次例項,並隨著應用程式的結束而銷燬。他有點像全域性靜態變數,只是整個過程都由依賴注入框架管理。許多全域性變數都可以通過該生命週期所提供的服務層維護。
第二種就是Scoped生命週期,用中文翻譯有點像作用域,這個作用域如何理解呢,我們都知道,http請求實際上是以為會話的形式進行的,每個會話往往會有IIS宿主開闢獨立的執行緒來進行支援,那麼這個作用域大概就像是給每個會話開闢的獨立執行緒。在這個執行緒的生命週期內,開發者都能從依賴注入框架中,便捷的引入適當的服務層來提供業務支援。
第三種則是Transient生命週期,瞬時生命週期。一瞬間就過去了,說明時間很快。當然,如果用一句佛經來形容,那就是
一剎那者為一念,二十念為一瞬,二十瞬為一彈指,二十彈指為一羅預,二十羅預為一須臾,一日夜有三十須臾。
這個一瞬間,大概是0.36秒,實際上程式執行可能比這快很多。程式語言目前還無法跟佛經對應起來,我們只能把它理解為,一次方法呼叫過程為瞬時。一般是那種需要隨用隨取的物件,主要是相對於Scoped而言的,在控制檯應用中使用非常廣泛。
.NET Core提供的這三種生命週期,看起來很簡單,但有時候也比較容易就踩坑了,這就需要我們平時注意自己的使用習慣了。“
6
一晃圍繞這個話題,他們已經聊了一個半小時,隨著窗外夕陽的逐漸消逝,窗外初上的華燈也使兩位大佬意識到時間不早了,於是,他們也離開了辦公室。
--end