python做大型网站_Python中的大型Web应用:一个好的架构
如果你著手使用關(guān)系型數(shù)據(jù)庫(kù)在Python中編寫大型應(yīng)用程序,這篇長(zhǎng)文正好滿足你的需求。這里我分享下在一個(gè)大型團(tuán)隊(duì)中使用SQLAlchemy(Python語(yǔ)言中提供最先進(jìn)ORM工具的軟件)編寫超過(guò)6個(gè)月時(shí)間大型應(yīng)用的經(jīng)驗(yàn)。
誠(chéng)然,我認(rèn)為這篇文章可能太復(fù)雜,嘗試一次性教太多的東西。但真的很想展示出多個(gè)方面銜接是如何導(dǎo)致失敗的。
隱藏的危險(xiǎn)
如果要解釋我所遇到的所有糟糕軟件的原因是不太現(xiàn)實(shí)的,但是可以肯定地說(shuō)它們是由于某些因素的相互作用所導(dǎo)致的:
草率
次優(yōu)技術(shù)選擇
SQLAlchemy需要開發(fā)人員進(jìn)行數(shù)周研究才能明智地使用它的客觀事實(shí)
相比MVC缺乏更好的整體架構(gòu)
開發(fā)人員對(duì)于MCV的理解不一致
開發(fā)人員還沒(méi)意識(shí)到自動(dòng)化測(cè)試應(yīng)該告知主代碼被分解的方式
需要實(shí)踐TDD(測(cè)試驅(qū)動(dòng)開發(fā)),即先編寫單元測(cè)試
關(guān)系型數(shù)據(jù)庫(kù)對(duì)于測(cè)試組件速度上的有害影響
需要學(xué)習(xí)什么是真正的單元測(cè)試:一些開發(fā)者認(rèn)為集成測(cè)試就是單元測(cè)試
我將討論所有這些方面,包括它們的關(guān)系以及一些解決方案。
草率是不好的…不要著急…MKay?
除非一個(gè)軟件的生命周期真的很短,否則匆忙地編寫永遠(yuǎn)是得不償失的。編寫一個(gè)軟件需要時(shí)間、調(diào)研、學(xué)習(xí)、實(shí)驗(yàn)、重構(gòu)和測(cè)試。每一個(gè)讓步都會(huì)讓你的草率產(chǎn)生糟糕的影響。但是不要相信我;每個(gè)人都需要親自地去嘗試這種痛苦。
謹(jǐn)慎地選擇你的框架
本節(jié)中提到的建議僅針對(duì)目前(2014年4月)而言。
當(dāng)編寫大型應(yīng)用時(shí),每個(gè)人都應(yīng)該謹(jǐn)慎地選擇所使用的工具。不要只是去走捷徑。舉例來(lái)說(shuō),選擇一個(gè)更加復(fù)雜的web框架是值得的,比如功能齊全、設(shè)計(jì)精美以及豐富文檔的Pyramid,它比起其他像Flask等一些框架具有更好的定義范圍以及更完善的解耦。雖然Flask之類的框架可以在完成小型工作時(shí)更加迅速,但是卻要承受著無(wú)處不豐的線程局部變量,不完整的文檔(好像只是給已經(jīng)了解它的人去閱讀),以及瘋長(zhǎng)的插件(Flask愛(ài)好者希望系統(tǒng)中所有東西都成為一個(gè)Flask插件)。
可以肯定地說(shuō),在Python的領(lǐng)域里SQLAlchemy相比于其他的ORMs軟件是非常先進(jìn)的。如果你通過(guò)其他的方式來(lái)訪問(wèn)關(guān)系型數(shù)據(jù)庫(kù),那么你將錯(cuò)過(guò)它。這也是你不應(yīng)該去選擇web2py框架的原因,而且,它并不包含ORM,僅僅是通過(guò)簡(jiǎn)單的DAL來(lái)生成SQL。
既然我已經(jīng)推薦了Pyramid和SQLAlchemy,為什么只字不提表單驗(yàn)證工具?從古老的FormEncode至今,許多庫(kù)都為此而創(chuàng)建,比如說(shuō)WTForms和ToscaWidgets。我只會(huì)說(shuō)你可以親自嘗試下將Deform和Colander結(jié)合在一起——它們具有不同的功能,比如說(shuō)將數(shù)據(jù)轉(zhuǎn)換成Python結(jié)構(gòu),以及部件(Deform)的模式分離(Colander),它真的能正確地解決問(wèn)題。這種架構(gòu)清晰性導(dǎo)致它比競(jìng)爭(zhēng)對(duì)手而言更加有力。同樣的是,你需要花費(fèi)稍微多些的時(shí)間去學(xué)習(xí)如何使用這些工具。使用其他工具的學(xué)習(xí)曲線可能會(huì)比較平緩,但是你將在今后的工作中深受其擾。
MVC不足以應(yīng)對(duì)大型項(xiàng)目
在Web應(yīng)用開發(fā)中,你可能已經(jīng)知道了MVC架構(gòu)(model,view,controller)。如果你不清楚,你可能并沒(méi)有準(zhǔn)備好去開發(fā)大型項(xiàng)目;可以先使用一個(gè)小型的MVC框架,然后在數(shù)月或者幾年之后再來(lái)看這篇文章吧。
嚴(yán)格來(lái)說(shuō),MVC是一個(gè)古老的概念,從早期的Smalltalk開始已經(jīng)不再適合于web應(yīng)用開發(fā)。Django的開發(fā)人員已經(jīng)正確意識(shí)到在Python中我們實(shí)際使用的是MTV(model,template,view):
模板包含了HTML的內(nèi)容以及頁(yè)面顯示邏輯。它是使用模板語(yǔ)言如Kajiki來(lái)編寫,從視圖(view)中獲取數(shù)據(jù),然后展示在頁(yè)面中。
視圖(有時(shí)也稱為“控制器”),僅僅是使用Python語(yǔ)言編寫的中間代碼。它借助于web框架將所有的內(nèi)容放在一起。它可以看到其他的所有層,并且定義了URLs,將它們映射到web框架中用于接收數(shù)據(jù)的函數(shù),然后利用其他層以最終發(fā)送響應(yīng)給web框架。它應(yīng)該盡可能得小,因?yàn)樗拇a是不能重復(fù)利用的,即使你盡量地縮減它,web表單也會(huì)促使其逐漸地變得復(fù)雜。
模型層本質(zhì)上是一個(gè)持久層:它最重要的依賴就是SQLAlchemy。模型知道如何去保存數(shù)據(jù),構(gòu)成整個(gè)項(xiàng)目中最可重用的代碼。當(dāng)然它并不清楚HTTP相關(guān)的內(nèi)容和你所使用的框架。它代表了排除用戶界面細(xì)節(jié)的系統(tǒng)本質(zhì)內(nèi)容。
但是稍等下,哪里?在視圖還是模型中?你應(yīng)該在哪里放置程序的靈魂:業(yè)務(wù)規(guī)則?模板層已經(jīng)自動(dòng)被排除掉,因?yàn)樗⒉皇荘ython編寫的。所以剩下3個(gè)可能的答案:
視圖層,這是最糟糕的選擇。視圖層應(yīng)該僅僅包含中間代碼,將代碼數(shù)量保持盡可能得小,并且同系統(tǒng)中的其他部分隔離開,所以系統(tǒng)應(yīng)該能在web框架中、使用中以及單元測(cè)試中獨(dú)立訪問(wèn)。另外,業(yè)務(wù)邏輯應(yīng)該存在于更加可重用的地方。視圖層被視為展示邏輯的一部分,所以業(yè)務(wù)邏輯被排除在外。實(shí)際上,除了Web UI之外,在創(chuàng)建desktop UI時(shí),開發(fā)者應(yīng)該忽略視圖和HTTP相關(guān)的內(nèi)容,需要業(yè)務(wù)邏輯盡可能地被重用,因此,我們應(yīng)該排除掉視圖層。
模型層,這個(gè)是可能的選擇,因?yàn)槟P蛯又辽偈强芍赜玫摹5悄P蛯又饕P(guān)注于持久化,它應(yīng)該更少地依賴于SQLAlchemy(它已經(jīng)是一個(gè)非常復(fù)雜的東西)。
新的層,這才是正確的答案。下面將舉例來(lái)更好地理解這部分內(nèi)容。
如果你要?jiǎng)?chuàng)建一個(gè)博客,那么MTV正好滿足你的需求。但是對(duì)于更加復(fù)雜的項(xiàng)目來(lái)說(shuō),其實(shí)還是至少缺了一層。你應(yīng)該將業(yè)務(wù)邏輯旋轉(zhuǎn)到一個(gè)新的、可重用的層次中,大多數(shù)人稱之為“Service”層,但是我更喜歡稱之為”Action“層。
為什么你需要這層?
在大型應(yīng)用中,單個(gè)用戶的操作引起多項(xiàng)活動(dòng)是非常常見(jiàn)的。比如,用戶成功地注冊(cè)了你的服務(wù),那么你的業(yè)務(wù)邏輯中可能會(huì)觸發(fā)非常多的后臺(tái)處理:
在關(guān)系數(shù)據(jù)庫(kù)多張表中新增數(shù)據(jù),使用了模型層。
將發(fā)送郵件給用戶的任務(wù)放置到隊(duì)列中。
將發(fā)送短信給用戶的任務(wù)放置到隊(duì)列中。
將創(chuàng)建實(shí)際使用服務(wù)時(shí)必需的空間和其他準(zhǔn)備性資源的任務(wù)放置到隊(duì)列中。
將更新用戶數(shù)據(jù)的任務(wù)放置到隊(duì)列中。
….
這是一個(gè)理解“業(yè)務(wù)邏輯“的好例子:給定一個(gè)用戶操作(比如注冊(cè)),系統(tǒng)就要做一些必需的操作。這種業(yè)務(wù)邏輯被單個(gè)函數(shù)捕獲會(huì)更好;這個(gè)函數(shù)應(yīng)該在哪一層呢?
如果所有的這些都實(shí)現(xiàn)在模型層,你能想象到它將變得多復(fù)雜嗎?模型層在只面對(duì)持久化時(shí)已經(jīng)很艱難了。現(xiàn)在想象一下模型層處理所有這些事務(wù),它要使用多少外部服務(wù)?文件頭部應(yīng)該包括多少imports?反過(guò)來(lái)看,有多少模塊愿意引入這個(gè)模型,可能在系統(tǒng)啟動(dòng)前就因?yàn)閯?chuàng)建的循環(huán)依賴而導(dǎo)致系統(tǒng)崩潰。
循環(huán)依賴其實(shí)就是你沒(méi)有正確認(rèn)清系統(tǒng)架構(gòu)的明顯的標(biāo)識(shí)。
對(duì)于依賴于Celery的模型來(lái)說(shuō),了解如何去發(fā)送郵件、短信以及使用外部服務(wù)等是不應(yīng)該的做法。持久化對(duì)于模型層來(lái)說(shuō)已經(jīng)是非常復(fù)雜的主題了。你應(yīng)該在模型層之外去處理這些業(yè)務(wù)邏輯——在模型層和視圖層之間的一層。所以稱之為”Action“層。
另外,模型層在關(guān)系型數(shù)據(jù)庫(kù)中經(jīng)常被映射到一張單獨(dú)的表。如果你在用戶表和訂閱表中插入一條記錄,哪一個(gè)模型應(yīng)該包含上述邏輯呢?這幾乎是不能確定的。因?yàn)閷?shí)際執(zhí)行的操作遠(yuǎn)超過(guò)了用戶表和訂閱表的范圍。因此,業(yè)務(wù)邏輯應(yīng)該被定義在任何模型之外。
當(dāng)開發(fā)人員執(zhí)行維護(hù)時(shí),有時(shí)她想按步驟地執(zhí)行每一步,而有時(shí)她想一次性執(zhí)行完所有操作。分別地實(shí)現(xiàn)這些操作并在單個(gè)Action層函數(shù)中調(diào)用是有幫助的。
你可能會(huì)懷疑我提出的方法難道不是反面模式域模型的一種嗎?沒(méi)有動(dòng)作的模型恰恰與面向?qū)ο笤O(shè)計(jì)相反!我并沒(méi)有說(shuō)“將所有的方法從模型中移除”。我的意思是指“將需要外部調(diào)用的方法移除”。模型中的方法僅僅用來(lái)使用它所需要的數(shù)據(jù),并且屬于模型中的那些數(shù)據(jù)。一個(gè)面向世界的,調(diào)用外部服務(wù)的并且很少使用自身數(shù)據(jù)的方法不應(yīng)該被放置在模型中。
另一個(gè)使得這種架構(gòu)成功的原因就是測(cè)試。TDD教會(huì)了程序員去讓程序變得解耦,這樣做通常會(huì)使得軟件更加健壯。如果你想要?jiǎng)?chuàng)建一個(gè)Celery的應(yīng)用,并且在你測(cè)試之前已經(jīng)知道了其他的外部服務(wù),那么你將經(jīng)常陷入頭痛中。
還有將業(yè)務(wù)邏輯放置在視圖層之外的最后一個(gè)原因。在未來(lái),當(dāng)你最終決定從Flask過(guò)渡到Pyramid時(shí),你很樂(lè)意將視圖層保持簡(jiǎn)潔。如果所有的視圖都是在與web框架間交互,并且動(dòng)作層會(huì)執(zhí)行所有的函數(shù),那么你的代碼就做到了非常好的隔離。Web框架通常都很貪婪,不要讓你的系統(tǒng)跟隨他們的腳步。
所以下面就是我所提議的在Python中構(gòu)建大型應(yīng)用的層次結(jié)構(gòu):
模型層是最底層的,最可重用并且可見(jiàn)的層。它僅專注于持久化,模型層是可以包含動(dòng)作的,只不過(guò)這個(gè)動(dòng)作僅僅屬于這個(gè)模型。模型可以被其他層所返回,以各種方式在請(qǐng)求的結(jié)尾返回給模板。
外部服務(wù)。對(duì)每個(gè)服務(wù)都創(chuàng)建一個(gè)比如說(shuō)發(fā)送郵件。
動(dòng)作層。這是系統(tǒng)中的核心層,它包含了業(yè)務(wù)邏輯以及工作流。它使用外部服務(wù)去實(shí)現(xiàn)特定的目標(biāo)并且借助模型層來(lái)持久化數(shù)據(jù)。通過(guò)以上這些層,它支撐起了整個(gè)系統(tǒng),包括配置,除了用戶界面。
模板層僅包括了頁(yè)面展示邏輯比如說(shuō)從列表中循環(huán)輸出構(gòu)成一個(gè)HTML表格。
視圖層,這是最高層的,最不可重用的層。它依賴于(與系統(tǒng)中其他層隔離)web框架。并且依賴于表單驗(yàn)證庫(kù)。它可以看到模板層以及動(dòng)作層,但是不能直接調(diào)用模型層——它必須通過(guò)動(dòng)作層。但是當(dāng)一個(gè)動(dòng)作層返回了模型數(shù)據(jù),那么它可以被傳遞給模板中(一個(gè)Celery任務(wù)可以類比于一個(gè)web視圖)。
這種體系結(jié)構(gòu)有助于避免了在會(huì)話層中進(jìn)行調(diào)試因?yàn)樗宄囟x了各自的職責(zé)。同時(shí)它也是明顯經(jīng)得起檢測(cè)的,因?yàn)樗龅搅撕芎玫慕怦?#xff0c;因此可以減少測(cè)試的并且減少了模擬的次數(shù)。
好的架構(gòu)總是解耦性非常好的。如果你曾經(jīng)陷入到一個(gè)循環(huán)依賴中,你可以想一下是否真的定義好了每一層的職責(zé)。當(dāng)你放棄了并且從一個(gè)函數(shù)中引入內(nèi)容,那么你的架構(gòu)已經(jīng)失敗了。
這并不是說(shuō)你的web應(yīng)用必須與Celery應(yīng)用隔離開。在他們之間可以重用代碼——特別是模型——但是對(duì)于Celery應(yīng)用來(lái)說(shuō),不應(yīng)該引入web框架!獲取配置也不例外,因?yàn)樵赑ython中讀取配置是非常簡(jiǎn)單的。
自動(dòng)化測(cè)試也是個(gè)巨大的挑戰(zhàn)
Python是一個(gè)非常靈活,富有表現(xiàn)力,反射型語(yǔ)言。隱藏在動(dòng)態(tài)機(jī)制后的不利之處在于“編譯期”內(nèi)很少能發(fā)現(xiàn)錯(cuò)誤。如今使用Java語(yǔ)言構(gòu)建大型系統(tǒng)時(shí)已經(jīng)離不開自動(dòng)化測(cè)試了;Python中更是如此。
一旦你意識(shí)到它對(duì)你軟件健壯性的重要性時(shí)你就會(huì)開始編寫測(cè)試用例。你理解了它并開始編寫用例,你編寫的第一個(gè)用例具有極大的價(jià)值,它會(huì)給你增加對(duì)系統(tǒng)的不可思議的信心。這是非常有趣的。
然而,你很快就會(huì)感覺(jué)測(cè)試對(duì)你來(lái)說(shuō)更像是一種負(fù)擔(dān)。你會(huì)擁有數(shù)百個(gè)的測(cè)試用例去運(yùn)行,而且通常要運(yùn)行很久。在這種情況下,每一個(gè)編寫的新測(cè)試用例都會(huì)讓你的生活更加糟糕。這個(gè)時(shí)候,一些人可能會(huì)覺(jué)得幻想破滅并得出測(cè)試是不值得去做的。這個(gè)結(jié)論往往言之過(guò)早。
你認(rèn)為你已經(jīng)知道了如何去編寫測(cè)試代碼,其實(shí)不然,你其實(shí)是在編寫一個(gè)綜合測(cè)試。雖然你稱之為“單元測(cè)試”,但是實(shí)際并非如此。每一個(gè)測(cè)試用例都貫穿了系統(tǒng)整個(gè)的運(yùn)行過(guò)程。你把你的模擬覆蓋到了盡可能遠(yuǎn)的位置,你認(rèn)為這是好的(通過(guò)這種方式可以測(cè)試更多)。但是你將發(fā)現(xiàn)這其實(shí)并不好。
單元測(cè)試其實(shí)是完全相反的,真正的單元測(cè)試會(huì)像激光一樣,它僅僅執(zhí)行某一層的某個(gè)函數(shù),并且使用模擬來(lái)避免陷入到其他層中,它永遠(yuǎn)不會(huì)到達(dá)其他的外部資源,它斷言只有一個(gè)條件而且運(yùn)行速度像光一樣快。
雪上加霜的是當(dāng)測(cè)試用例開始工作時(shí),顯示的結(jié)果會(huì)讓你陷入混亂。不同于針對(duì)單個(gè)錯(cuò)誤的測(cè)試用例可以準(zhǔn)確地告知你哪里出現(xiàn)了問(wèn)題,數(shù)十個(gè)測(cè)試用例執(zhí)行失敗(所有的失敗可能都是因?yàn)橐粋€(gè)原因,因?yàn)樗鼈冐灤┝苏麄€(gè)系統(tǒng)),但是你需要花費(fèi)很長(zhǎng)的時(shí)間去找出bug的真正所在。因而你需要編寫出更好的測(cè)試用例。
專家推薦你編寫99%的真實(shí)、專注、模擬性的單元測(cè)試,而僅1%的貫穿所有層次的綜合測(cè)試。如果你從開始就這樣做了,那么你的測(cè)試集會(huì)在數(shù)秒鐘內(nèi)就運(yùn)行完畢,這樣才能使得TDD是可行的。如果某個(gè)單元測(cè)試運(yùn)行時(shí)間較長(zhǎng)(超過(guò)了10毫秒),那么它可能是其他類型的測(cè)試而非單元測(cè)試。
如果你要編寫的僅僅是個(gè)小的應(yīng)用,那么使用綜合測(cè)試的方法或許也可行。但是我們要討論的是大型應(yīng)用,因此,在這個(gè)層面上來(lái)說(shuō),要么你去優(yōu)化測(cè)試用例的性能,要么你根本應(yīng)用不了TDD。
另外,你要記得有一些測(cè)試用例是比較難編寫的,它們需要大量的工作才能完成。有的人解釋說(shuō)這是因?yàn)槟愕臏y(cè)試用例并非真正的單元測(cè)試,而且你并沒(méi)有以測(cè)試為先——你在給現(xiàn)有的未充分解耦的代碼編寫測(cè)試用例時(shí)會(huì)很難。現(xiàn)在你知道TDD其實(shí)并非只改變了測(cè)試相關(guān)的東西,還有將你實(shí)際的系統(tǒng)變得更好。
關(guān)于測(cè)試用例的編寫可以參考以下資料:
快速測(cè)試,慢速測(cè)試
綜合測(cè)試是一個(gè)騙局
停止模擬,開始測(cè)試(實(shí)際上并不是在攻擊模擬實(shí)踐,只是提出要重用模擬和存根)
如果想發(fā)現(xiàn)最慢的兩個(gè)測(cè)試,可以使用如下命令:
py.test -s --tb=native -x --durations 2
SQLAlchemy和測(cè)試
但是系統(tǒng)使用的是SQLAlchemy!數(shù)據(jù)要以模型實(shí)例的方式在不同的層之間流動(dòng)。執(zhí)行一次數(shù)據(jù)庫(kù)查詢,可能已經(jīng)使用了超過(guò)10毫秒的時(shí)間。這迫使你意識(shí)到,如果這次查詢命中了數(shù)據(jù)庫(kù),那么它就不再是單元測(cè)試。(想要實(shí)例化SQLAlchemy模型是非常迅速的,但是與SQLite的交互卻很耗時(shí),即使它是保存在內(nèi)存中)。TDD迫使你將查詢,session.flush()和session.commit()等放到適合于單元測(cè)試的函數(shù)之外。
即使如此,你仍然需要編寫一些綜合測(cè)試。它可以測(cè)試出不同層次之間的聯(lián)系,并且捕獲到單元測(cè)試不能查出的BUG。對(duì)于綜合測(cè)試,John Andreson有一個(gè)很好的使用方法:使用SQLAlchemy,但是永遠(yuǎn)不要允許提交事務(wù)。在每一個(gè)測(cè)試結(jié)尾處,加上session.rollback()以使得下一次的測(cè)試可以在數(shù)據(jù)庫(kù)未被改變的情況下執(zhí)行。這種方法可以讓你不用每次測(cè)試都重新建立數(shù)據(jù)表。
為了實(shí)現(xiàn)這一點(diǎn),你不能到處提交會(huì)話,最好的是制定一個(gè)規(guī)則:系統(tǒng)只能在最外面的層中通過(guò)調(diào)用session.commit()實(shí)現(xiàn)提交,即web視圖層或者Celery任務(wù)中。不要在模型層提交,也不要在動(dòng)作層提交!
這會(huì)導(dǎo)致最后一個(gè)問(wèn)題:如果一個(gè)任務(wù)是用來(lái)提交事務(wù)的,怎么為該任務(wù)編寫單元測(cè)試?我需要一種測(cè)試調(diào)用任務(wù)的說(shuō)法:異常,就這一次(因?yàn)樗菧y(cè)試),其他的請(qǐng)不要提交。否則單元測(cè)試就會(huì)命中服務(wù)器,并且執(zhí)行超過(guò)10ms的時(shí)間限制。
最終我會(huì)給出一個(gè)外部的函數(shù)(比如測(cè)試用例)去控制是否提交事務(wù)。使用這種方案,默認(rèn)情況下會(huì)提交事務(wù),但是允許測(cè)試用例告知可以不提交。下面就是這段示例代碼:
歡迎討論,希望對(duì)你有幫助
與50位技術(shù)專家面對(duì)面20年技術(shù)見(jiàn)證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的python做大型网站_Python中的大型Web应用:一个好的架构的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 电脑硬盘坏了怎么修复电脑硬盘坏了怎么修复
- 下一篇: 怎样把python源程序发给别人_如何把