做阿里還是網(wǎng)站濰坊百度關(guān)鍵詞優(yōu)化
1.如何對(duì)接口鑒權(quán)這樣一個(gè)功能開(kāi)發(fā)做面向?qū)ο蠓治?/h2>
本章會(huì)結(jié)合一個(gè)真實(shí)的案例,從基礎(chǔ)的需求分析、職責(zé)劃分、類(lèi)的定義、交互、組裝運(yùn)行講起,將最基礎(chǔ)的面向?qū)ο蠓治?#xff08;00A
)、設(shè)計(jì)(00D
)、編程(00P
)的套路講清楚,為后面的設(shè)計(jì)原則和設(shè)計(jì)模型打好基礎(chǔ)。
1.1 案例介紹和難點(diǎn)剖析
假設(shè),你參與開(kāi)發(fā)一個(gè)微服務(wù)。微服務(wù)通過(guò) HTTP
暴露接口給其他系統(tǒng)調(diào)用。有一天,你的領(lǐng)導(dǎo)找到你說(shuō),“為了保證接口調(diào)用的安全,希望設(shè)計(jì)實(shí)現(xiàn)一個(gè)接口調(diào)用鑒權(quán)功能,只有經(jīng)過(guò)認(rèn)證的系統(tǒng)才能調(diào)用微服務(wù)接口,沒(méi)有認(rèn)證過(guò)的系統(tǒng)會(huì)被拒絕。希望由你來(lái)開(kāi)發(fā),爭(zhēng)取盡管上線”。
這個(gè)時(shí)候,你可能會(huì)有腦子里一團(tuán)漿糊,一時(shí)間無(wú)從下手的感覺(jué)? 有這種感覺(jué)的原因,個(gè)人覺(jué)得有以下兩點(diǎn)。
1.需求不明確
領(lǐng)導(dǎo)給的需求過(guò)于模糊、籠統(tǒng),離落地到設(shè)計(jì)、編碼還有一定的距離。而人的大腦不擅長(zhǎng)思考這種過(guò)于抽象的問(wèn)題。
前面講過(guò),面向?qū)ο蠓治鲋饕姆治鰧?duì)象是“需求”。因此,面向?qū)ο蠓治隹梢源致缘乜闯伞靶枨蠓治觥?。?shí)際上,不管是需求分析還是面向?qū)ο蠓治?#xff0c;首先要做的是將籠統(tǒng)的需求細(xì)化到足夠清晰、可執(zhí)行。需要通過(guò)溝通、挖掘、分析、假設(shè)、梳理,搞清楚具體的需求有哪些,哪些是現(xiàn)在要做,哪些是未來(lái)可能要做的,哪些是不用考慮的。
2.缺少鍛煉
相比單純的 CRUD
開(kāi)發(fā),鑒權(quán)這個(gè)開(kāi)發(fā)任務(wù)更有難度。鑒權(quán)作為一個(gè)根具體業(yè)務(wù)無(wú)關(guān)的功能,完全可以把它獨(dú)立開(kāi)發(fā)成一個(gè)獨(dú)立的框架,集成到很多業(yè)務(wù)系統(tǒng)中。而作為被很多系統(tǒng)復(fù)用的通用框架,比如普通的代碼,我們對(duì)框架的代碼質(zhì)量要求更高。
開(kāi)發(fā)這樣的通用框架,對(duì)工程師的需求分析能力、設(shè)計(jì)能力、編碼能力,甚至邏輯思維能力的要求,都是比較高的。如果你平時(shí)做的都是簡(jiǎn)單的 CRUD
業(yè)務(wù)開(kāi)發(fā),那這方面的鍛煉肯定不會(huì)很多,所以,一旦遇到這種開(kāi)發(fā)需求,很容易因缺少鍛煉,腦子放空,不知道從何入手,完全沒(méi)有思路。
1.2 對(duì)案例進(jìn)行需求分析
實(shí)際上,需求分析的工作很瑣碎,沒(méi)有固定的方法論。系統(tǒng)通過(guò)這個(gè)例子,給你展示下需求分析時(shí),完整的考慮思路是什么樣的。希望你自己體會(huì),舉一反三地應(yīng)用到其他項(xiàng)目的需求分析中。
針對(duì)鑒權(quán)這個(gè)功能的開(kāi)發(fā),該如何做需求分析?
實(shí)際上,這和做算法題類(lèi)似,先從最簡(jiǎn)單的法案想起,然后再優(yōu)化。所以,我把分析的過(guò)程分為了循序漸進(jìn)的四輪。
第一輪基礎(chǔ)分析
對(duì)于如何鑒權(quán)這樣的問(wèn)題,最簡(jiǎn)單的解決方案是,通過(guò)用戶名加密碼來(lái)做認(rèn)證。我們給每個(gè)允許訪問(wèn)服務(wù)的調(diào)用方,派發(fā)一個(gè) APPID
和一個(gè)對(duì)應(yīng)的密碼。調(diào)用方每次請(qǐng)求時(shí)都攜帶自己的 APPID
和密碼。微服務(wù)在接受到接口調(diào)用請(qǐng)求后,會(huì)解析出 APPID
和密碼,和存儲(chǔ)的 APPID
和密碼進(jìn)行對(duì)比。如果一致則允許調(diào)用請(qǐng)求;否則拒絕調(diào)用。
第二輪分析優(yōu)化
這樣的驗(yàn)證方式,每次都要傳輸明文密碼。密碼很容易被屏蔽,是不安全的。那如果借助加密算法(比如 SHA
),對(duì)密碼進(jìn)行加密后,再傳遞到微服務(wù)端驗(yàn)證,是不是就可以了?
實(shí)際上這樣也不安全,因?yàn)榧用苤蟮拿艽a及 APPID
,照樣可以被未認(rèn)證系統(tǒng)(或黑客)截獲,未認(rèn)證系統(tǒng)可以攜帶這個(gè)加密之后的面以及對(duì)應(yīng)的 APPID
,偽裝成已認(rèn)證系統(tǒng)來(lái)訪問(wèn)我們的接口。這就是典型的“重放攻擊”。
提出問(wèn)題,再解決問(wèn)題,是一個(gè)非常好的迭代方式。對(duì)于剛剛的問(wèn)題,可以借助 OAuth
的驗(yàn)證思路來(lái)解決。 調(diào)用方將請(qǐng)求的 URL
跟 APPID
、密碼拼接在一起,然后進(jìn)行加密,生成一個(gè) token
。調(diào)用方在接口請(qǐng)求的時(shí)候,將這個(gè) token
及 APPID
,跟著 URL
一塊傳遞給服務(wù)端。服務(wù)端接受到這些數(shù)據(jù)后,根據(jù) APPID
從數(shù)據(jù)庫(kù)中取出對(duì)應(yīng)的密碼,并通過(guò)同樣的 token
生成算法,生成另外一個(gè) token
。用這個(gè)新生成的 token
和調(diào)用方傳遞過(guò)來(lái)的 token
對(duì)比。如果一致,則允許接口調(diào)用請(qǐng)求;否則拒絕調(diào)用。
客戶端過(guò)程
1.生成token SHA(http://www.test.com/user?id=123&appid=abc&pwd=def)
2.生成新URLhttp://www.test.com/user?id=123&appid=abc&pwd=def&token=xxx
服務(wù)端過(guò)程
3.解析出 URL、Appid、token
4.從數(shù)據(jù)庫(kù)中根據(jù) Appid 取出 pwd
5.使用同樣的算法生成服務(wù)端 token_s
6. token == token_s,允許訪問(wèn);token != token_s,拒絕訪問(wèn)。
第三輪分析優(yōu)化
經(jīng)過(guò)第二輪優(yōu)化后,仍然存在重放攻擊的風(fēng)險(xiǎn)。因?yàn)槊總€(gè) URL
拼接上 Appid
、密碼生成的 token
都是固定的。
為解決這個(gè)問(wèn)題,可以進(jìn)一步優(yōu)化 token
生成算法,引入一個(gè)隨機(jī)變量,讓每次接口請(qǐng)求生成的 token
都不一樣??梢赃x擇時(shí)間戳作為隨機(jī)變量?,F(xiàn)在使用 URL
、Appid
、密碼、時(shí)間戳四種進(jìn)行加密生成 token
。調(diào)用方在進(jìn)行接口請(qǐng)求時(shí),將 token
、Appid
、時(shí)間戳,隨著 URL
一起傳給微服務(wù)端。
微服務(wù)端在接受到這些數(shù)據(jù)后,會(huì)驗(yàn)證當(dāng)前時(shí)間戳和傳遞過(guò)來(lái)的時(shí)間戳,是否在一定的時(shí)間窗口內(nèi)(如一分鐘)。如果超過(guò)時(shí)間窗口,則判定 token
過(guò)期,拒絕接口請(qǐng)求。如果沒(méi)有超過(guò)時(shí)間窗口,則說(shuō)明 token
沒(méi)有過(guò)期,就在通過(guò)同樣的 token
生成算法,在服務(wù)端生成新的 token
,和調(diào)用方的 token
對(duì)比。若一致,則允許接口調(diào)用請(qǐng)求;否則,拒絕調(diào)用。
優(yōu)化后的認(rèn)證流程如下
客戶端流程:
1.生成token SHA(http://www.test.com/user?id=123&appid=abc&pwd=def&ts=156152345)
2.生成新URLhttp://www.test.com/user?id=123&appid=abc&pwd=def&token=xxx&ts=156152345
服務(wù)端流程:
3.解析出 URL、Appid、token
4.驗(yàn)證token是否失效。失效就拒絕訪問(wèn),否則執(zhí)行5
5.從數(shù)據(jù)庫(kù)中根據(jù) Appid 取出 pwd
6.使用同樣的算法生成服務(wù)端 token_s
7. token == token_s,允許訪問(wèn);token != token_s,拒絕訪問(wèn)。
第四輪分析優(yōu)化
不過(guò),你可能會(huì)說(shuō),這樣還是不夠安全呀。未認(rèn)證系統(tǒng)還是可以在一分鐘的 token
失效窗口內(nèi),通過(guò)截取請(qǐng)求,來(lái)調(diào)用我們的借口OA。
你說(shuō)的不錯(cuò)。不過(guò)在攻與防之間,本來(lái)就沒(méi)有絕對(duì)的安全。我們能做的就是,盡量提高攻擊的成本。這個(gè)方案雖然還有漏洞,但是實(shí)現(xiàn)起來(lái)足夠簡(jiǎn)單,而且不會(huì)過(guò)度影響接口本身的性能(比如響應(yīng)時(shí)間)。
實(shí)際上,還有一個(gè)細(xì)節(jié)我們還沒(méi)有考慮到,那就是,如何在微服務(wù)端存儲(chǔ)每個(gè)授權(quán)調(diào)用方的 Appid
和密碼。當(dāng)然,這個(gè)問(wèn)題并不難。最容易想到的方案就是存儲(chǔ)到數(shù)據(jù)庫(kù)里,比如 MySQL
。不過(guò),像開(kāi)發(fā)這樣的非業(yè)務(wù)功能,最好不要與具體的第三方系統(tǒng)有過(guò)度的耦合。
針對(duì) Appid
和密碼的存儲(chǔ),最后可以靈活支持不同的存儲(chǔ)方式,比如 Zookeeper、本地配置文件、自研配置中心、MySQL
、Redis
等。我們不一定針對(duì)每種存儲(chǔ)都去做實(shí)現(xiàn),但起碼要留有擴(kuò)展點(diǎn),保證系統(tǒng)足夠的靈活性和擴(kuò)展性,能在我們切換存儲(chǔ)方式的時(shí)候,盡可能少的改動(dòng)代碼。
最終確定需求
- 調(diào)用方進(jìn)行接口請(qǐng)求的時(shí)候,將
URL
、Appid
、密碼、時(shí)間戳拼接在一起,通過(guò)加密算法生成token
,并將token
、Appid
、時(shí)間戳拼接在URL
中,一并發(fā)送到微服務(wù)端。 - 微服務(wù)端在接收到調(diào)用方的請(qǐng)求后,從請(qǐng)求中解析出
token
、Appid
、時(shí)間戳。 - 微服務(wù)端首先檢查傳遞過(guò)來(lái)的時(shí)間戳是否在
token
失效時(shí)間窗口內(nèi)。若已失效,則接口調(diào)用鑒權(quán)失敗,拒絕接口調(diào)用請(qǐng)求 - 如果
token
沒(méi)有過(guò)期失效,微服務(wù)再?gòu)淖约旱拇鎯?chǔ)中,取出Appid
對(duì)應(yīng)的密碼,通過(guò)同樣的token
生成算法,生成另一個(gè)token
,與調(diào)用方的token
進(jìn)行比對(duì)。如果一致,則鑒權(quán)成功,允許接口調(diào)用;否則就拒絕接口調(diào)用。
這就是我們的需求分析的整個(gè)過(guò)程,從最粗糙、最模型的需求開(kāi)始,通過(guò)“提出問(wèn)題 - 再解決問(wèn)題”的方式,循序漸進(jìn)的方式進(jìn)行優(yōu)化,最后得到一個(gè)足夠清晰、可落地的需求描述。
2.如何利用面向?qū)ο笤O(shè)計(jì)和編程開(kāi)發(fā)接口鑒權(quán)功能?
2.1如何進(jìn)行面向?qū)ο笤O(shè)計(jì)(OOD)
面向?qū)ο蠓治龅漠a(chǎn)出是詳細(xì)的需求描述,面向?qū)ο笤O(shè)計(jì)的產(chǎn)出是類(lèi)。在面向?qū)ο笤O(shè)計(jì)環(huán)節(jié),我們將需求描述轉(zhuǎn)化成具體的類(lèi)的設(shè)計(jì)。
設(shè)計(jì)這一環(huán)節(jié)拆解細(xì)化,主要包含以下幾個(gè)部分:
- 劃分職責(zé)而識(shí)別出有哪些類(lèi)
- 定義類(lèi)及其屬性和方法
- 定義類(lèi)之間的交互關(guān)系
- 將類(lèi)組裝起來(lái)并提供執(zhí)行入口
劃分職責(zé)而識(shí)別出有哪些類(lèi)
根據(jù)需求描述,把其中涉及的功能點(diǎn),一個(gè)個(gè)羅列出來(lái),然后再去看看哪些功能職責(zé)相近,操作同樣的屬性,是否應(yīng)該歸為同一個(gè)類(lèi)。
我們來(lái)看下,針對(duì)鑒權(quán)這個(gè)例子,具體如何來(lái)做。之前我們依據(jù)確定了最終需求,如下:
- 調(diào)用方進(jìn)行接口請(qǐng)求的時(shí)候,將
URL
、Appid
、密碼、時(shí)間戳拼接在一起,通過(guò)加密算法生成token
,并將token
、Appid
、時(shí)間戳拼接在URL
中,一并發(fā)送到微服務(wù)端。- 微服務(wù)端在接收到調(diào)用方的請(qǐng)求后,從請(qǐng)求中解析出
token
、Appid
、時(shí)間戳。- 微服務(wù)端首先檢查傳遞過(guò)來(lái)的時(shí)間戳是否在
token
失效時(shí)間窗口內(nèi)。若已失效,則接口調(diào)用鑒權(quán)失敗,拒絕接口調(diào)用請(qǐng)求- 如果
token
沒(méi)有過(guò)期失效,微服務(wù)再?gòu)淖约旱拇鎯?chǔ)中,取出Appid
對(duì)應(yīng)的密碼,通過(guò)同樣的
token
生成算法,生成另一個(gè)token
,與調(diào)用方的token
進(jìn)行比對(duì)。如果一致,則鑒權(quán)成功,允許接口調(diào)用;否則就拒絕接口調(diào)用。
首先是逐字逐句地閱讀上面的需求,拆解成一個(gè)個(gè)小的功能點(diǎn),一條條羅列下來(lái)。注意,拆解出來(lái)的每個(gè)功能點(diǎn)要盡可能小。每個(gè)功能點(diǎn)只負(fù)責(zé)一個(gè)很小的事情(專業(yè)叫法是“單一職責(zé)”)。下面是逐句拆解下來(lái)后,得到的功能點(diǎn)羅列:
- 將
URL
、Appid
、密碼、時(shí)間戳拼接為一個(gè)字符串 - 對(duì)字符串通過(guò)加密算法加密得到
token
- 將
token
、Appid
、時(shí)間戳拼接在URL
中,形成新的URL
- 解析得到
token
、Appid
、時(shí)間戳等信息 - 根據(jù)時(shí)間戳判斷
token
是否過(guò)期失效 - 從存儲(chǔ)中取出
Appid
對(duì)應(yīng)的密碼 - 驗(yàn)證兩個(gè)
token
是否匹配
從上面的功能列表中,我們發(fā)現(xiàn) 1、2、5、7 都是和 token
相關(guān),負(fù)責(zé) token
的生成、驗(yàn)證。3、4 都是在處理 URL
,負(fù)責(zé) URL
的拼接和解析;6 是操作 Appid
和密碼,負(fù)責(zé)從存儲(chǔ)中讀取 Appid
和密碼。所以,我們可以粗略地得到三個(gè)核心類(lèi):AuthToken
、URL
、CredentialStorage
。
AuthToken
負(fù)責(zé) 1、2、5、7 這四個(gè)操作。URL
負(fù)責(zé) 3、4 這兩個(gè)操作。CredentialStorage
負(fù)責(zé) 6 這個(gè)操作。
當(dāng)然,這是一個(gè)初步的類(lèi)劃分,其他一些不重要的類(lèi),我們可能暫時(shí)沒(méi)有辦法一下子想全,但這也沒(méi)關(guān)系,面向?qū)ο蠓治?、設(shè)計(jì)、編程本來(lái)就是一個(gè)循環(huán)迭代、不斷優(yōu)化的過(guò)程。根據(jù)需求,我們先給出一個(gè)粗糙版本的設(shè)計(jì)方案,然后基于這樣一個(gè)基礎(chǔ),再去迭代優(yōu)化,會(huì)更加容易些,思路也更加清晰一些。
需要強(qiáng)調(diào)一點(diǎn),接口調(diào)用鑒權(quán)這個(gè)需求比較簡(jiǎn)單,所以需求對(duì)應(yīng)的面向?qū)ο笤O(shè)計(jì)并不復(fù)雜,識(shí)別出來(lái)的類(lèi)也不多。如果是面向的更加大型的軟件開(kāi)發(fā)、更加復(fù)雜的需求,涉及的功能點(diǎn)可能會(huì)很多,對(duì)應(yīng)的類(lèi)也會(huì)比較多,像剛剛那樣根據(jù)需求逐句羅列功能點(diǎn)的方法,最后會(huì)得到一個(gè)很長(zhǎng)的列表,就會(huì)優(yōu)點(diǎn)凌亂、沒(méi)有規(guī)律。
針對(duì)這種復(fù)雜的需求開(kāi)發(fā),首先要做的是進(jìn)行模塊劃分,將需求先簡(jiǎn)單劃分成幾個(gè)小的、獨(dú)立的功能模塊,然后再在模塊內(nèi)部,應(yīng)用剛剛的方法,進(jìn)行面向?qū)ο笤O(shè)計(jì)。而模塊的劃分和識(shí)別,跟類(lèi)的劃分和識(shí)別,是類(lèi)似的套路。
定義類(lèi)及其屬性和方法
通過(guò)剛剛的需求分析,識(shí)別出了三個(gè)核心類(lèi):AuthToken
、URL
、CredentialStorage
?,F(xiàn)在再來(lái)看下,每個(gè)類(lèi)有哪些屬性和方法。我們還是從功能點(diǎn)列表中挖掘。
AuthToken 類(lèi)相關(guān)的功能點(diǎn)有四個(gè):
- 將
URL
、Appid
、密碼、時(shí)間戳拼接為一個(gè)字符串 - 對(duì)字符串通過(guò)加密算法加密得到
token
- 根據(jù)時(shí)間戳判斷
token
是否過(guò)期失效 - 驗(yàn)證兩個(gè)
token
是否匹配
對(duì)于方法的識(shí)別,一般都是識(shí)別需求描述中的動(dòng)詞,作為候選方法,再進(jìn)一步過(guò)濾篩選。類(lèi)比下方法的識(shí)別,可以把功能點(diǎn)中涉及的名詞,作為候選屬性,然后同樣進(jìn)行過(guò)濾篩選。
借用這個(gè)思路,識(shí)別出 AuthToken
類(lèi)的屬性和方法
/****** AuthToken ******/
// 屬性
private static final long DEFAULT_EXPIRED_TIME_INTERVAL = 60000;
private String token;
private long createTime;
private long expiredTimeInterval = DEFAULT_EXPIRED_TIME_INTERVAL;// 構(gòu)造函數(shù)
public AuthToken(String token, long createTime);
public AuthToken(String token, long createTime, long expiredTimeInterval);// 函數(shù)
public static AuthToken create(String baseUrl, long createTime, Map<String, String> params;)
public String getToken();
public boolean isExpired();
public boolean match(AuthToken authToken);
從上面的類(lèi)中,我們可以返現(xiàn)這樣三個(gè)小細(xì)節(jié):
- 第一個(gè)細(xì)節(jié): 并不是所有出現(xiàn)的名詞都被定義為類(lèi)的屬性,比如
URL
、Appid
、密碼、時(shí)間戳這幾個(gè)名詞,我們把它作為了方法的參數(shù)。 - 第二個(gè)細(xì)節(jié):我們還需要挖掘出一些沒(méi)有出現(xiàn)在功能點(diǎn)描述中的屬性,比如
createTime
、expiredTimeInterval
,它們用在isExpired()
函數(shù)中用來(lái)判斷token
是否過(guò)期。 - 第三個(gè)細(xì)節(jié):我們還給
AuthToken
類(lèi)添加了一個(gè)功能點(diǎn)描述中沒(méi)有提到的方法getToken()
。
第一個(gè)細(xì)節(jié)高速我們,從業(yè)務(wù)模型上來(lái)說(shuō),不應(yīng)該屬于這個(gè)類(lèi)的屬性和方法,不應(yīng)該被放到這個(gè)類(lèi)中。比如 URL
、Appid
這些信息,從業(yè)務(wù)模型上來(lái)說(shuō),不應(yīng)該屬于 AuthToken
,所以不應(yīng)該放到這個(gè)類(lèi)中。
第二、第三個(gè)細(xì)節(jié)高速我們,在設(shè)計(jì)類(lèi)具體有哪些屬性和方法的時(shí)候,不能單純地依賴當(dāng)下的需求,還要分析這個(gè)類(lèi)從業(yè)務(wù)模型上來(lái)講,應(yīng)該具有哪些屬性和方法。這樣一方面保證類(lèi)定義的完整性,另一方面不僅為當(dāng)下的需求,還為未來(lái)的需求做些準(zhǔn)備。
Url 類(lèi)相關(guān)的功能點(diǎn)有兩個(gè)
- 將
token
、Appid
、時(shí)間戳拼接在URL
中,形成新的URL
- 解析得到
token
、Appid
、時(shí)間戳等信息
雖然需求描述中,都是以 URL
來(lái)代指接口請(qǐng)求,但是,接口請(qǐng)求并不一定是 URL
的形式來(lái)表達(dá),還可能是 Dubbo
、RPC
等其他形式。為了讓這個(gè)類(lèi)設(shè)計(jì)的更加通用,命名更加貼切,我們接下來(lái)把它命名為 ApiRequest
。下面是根據(jù)功能點(diǎn)描述設(shè)計(jì)的 ApiRequest
。
/****** ApiRequest ******/// 屬性
private String baseUrl;
private String token;
private String appId;
private long timestamp;// 構(gòu)造函數(shù)
public ApiRequest(String baseUrl, String token,String appId, long timestamp);// 函數(shù)
public static ApiRequest createFromUrl(String url);public String getBaseUrl();
public String getToken();
public String getAppId();
public long getTimestamp();
CredentialStorage 類(lèi)相關(guān)的功能點(diǎn)有一個(gè)
- 從存儲(chǔ)中取出
Appid
對(duì)應(yīng)的密碼
CredentialStorage
類(lèi)很簡(jiǎn)單。為了做到抽象封裝具體的存儲(chǔ)方式,我們將CredentialStorage
設(shè)計(jì)成了接口,基于接口而非實(shí)現(xiàn)編程。
/****** CredentialStorage ******/// 接口函數(shù)
String getPasswordByAppId(String appId);
定義類(lèi)之間的交互關(guān)系
類(lèi)與類(lèi)之間的關(guān)系有哪些? UML
統(tǒng)一建模語(yǔ)言定義了 6 種類(lèi)之間的關(guān)系。分別是:泛化、實(shí)現(xiàn)、關(guān)聯(lián)、聚合、組合、依賴。
泛化可以簡(jiǎn)單理解為繼承關(guān)系。
public class A {...}
public class B extends A {...}
實(shí)現(xiàn)一般是指接口和實(shí)現(xiàn)類(lèi)之間的關(guān)系。
public interface A {...}
public class B implements A {...}
聚合 是一種包含關(guān)系,A 類(lèi)對(duì)象包含 B 類(lèi)對(duì)象,B 類(lèi)對(duì)象的生命周期可以不依賴 A 類(lèi)對(duì)的生命周期,也就是說(shuō)可以單獨(dú)銷(xiāo)毀 A 類(lèi)對(duì)象而不影響 B 類(lèi)對(duì)象,比如課程與學(xué)生的關(guān)系。
public class A {private B b;public A(B b) {this.b = b;}
}
組合也是一種包含的關(guān)系。A 類(lèi)對(duì)象包含 B 類(lèi)對(duì)象,B 類(lèi)對(duì)象的生命周期依賴 A 類(lèi)對(duì)的生命周期,B 類(lèi)對(duì)象不可以單獨(dú)存在,比如鳥(niǎo)與翅膀的關(guān)系。
public class A {private B b;public A() {this.b = new B();}
}
關(guān)聯(lián) 是一種比較弱的關(guān)系,包含組合和聚合。如果 B 類(lèi)對(duì)象是 A 類(lèi)的成員變量,那 B 類(lèi)和 A 類(lèi)就是關(guān)聯(lián)關(guān)系。
public class A {private B b;public A(B b) {this.b = b;}
}或者public class A {private B b;public A() {this.b = new B();}
}
依賴是一種比關(guān)聯(lián)關(guān)系更加弱的關(guān)系,包含關(guān)聯(lián)關(guān)系。不管 B 類(lèi)對(duì)象是 A 類(lèi)的成員變量,還是 A 類(lèi)的方法使用 B 類(lèi)對(duì)象作為入?yún)ⅰ⒎祷刂?、局部變?#xff0c;只要 B 類(lèi)對(duì)象和 A 類(lèi)對(duì)象有任何使用關(guān)系,都稱它們具有依賴關(guān)系。
public class A {private B b;public A(B b) {this.b = b;}
}或者public class A {private B b;public A() {this.b = new B();}
}或者public class A {public void func(B b) {...}
}
個(gè)人覺(jué)得這樣拆分的太細(xì),增加了學(xué)習(xí)的成本,對(duì)指導(dǎo)編程沒(méi)有太大的意義。所以,我只保留了四個(gè)關(guān)系:泛化、實(shí)現(xiàn)、組合、依賴。其中泛化、實(shí)現(xiàn)、依賴的定義不變,組合關(guān)系替代 UML
中的組合、聚合、關(guān)聯(lián)這三個(gè)概念。
相當(dāng)于重命名關(guān)聯(lián)關(guān)系為組合關(guān)系,且不在區(qū)分組合和聚合這兩個(gè)概念。
只要 B 類(lèi)對(duì)象,是 A 類(lèi)的成員變量,那就成 A 類(lèi)和 B 類(lèi)具有組合關(guān)系。
在看下我們定義的類(lèi)之間有哪些關(guān)系?因?yàn)槟壳爸挥腥齻€(gè)核心類(lèi),所以只用到了實(shí)現(xiàn)關(guān)系,即 CredentialStorage
和 MySqlCredentialStorage
之間是實(shí)現(xiàn)關(guān)系。接下來(lái)講到組裝類(lèi)的時(shí)候,還會(huì)用到依賴關(guān)系、組合關(guān)系,但是泛化關(guān)系暫時(shí)沒(méi)有用到。
將類(lèi)組裝起來(lái)并提供執(zhí)行入口
類(lèi)定義好了,類(lèi)之間的泛化關(guān)系也設(shè)計(jì)好了,接下來(lái)我們要將所有的類(lèi)組裝在一起,提供一個(gè)執(zhí)行入口。這個(gè)入口可能是 main()
函數(shù),也可能是一組給外部用的 API
接口。通過(guò)這個(gè)入口,我們能觸發(fā)整個(gè)代碼跑起來(lái)。
接口鑒權(quán)并不是一個(gè)獨(dú)立運(yùn)行的系統(tǒng),而是一個(gè)集成在系統(tǒng)上運(yùn)行的組件,所以,我們封裝所有的實(shí)現(xiàn)細(xì)節(jié),設(shè)計(jì)一個(gè)最頂層的 ApiAuthenticator
接口類(lèi),暴露一組給外部調(diào)用者或者 API
接口,作為觸發(fā)執(zhí)行鑒權(quán)邏輯的入口。
/****** ApiAuthenticator ******/
// 接口函數(shù)
void auth(String url);
void auth(ApiRequest apiRequest);
實(shí)現(xiàn)類(lèi)
/****** DefaultApiAuthenticatorImpl ******/
// 屬性
private CredentialStorage credentialStorage;
// 構(gòu)造函數(shù)
public DefaultApiAuthenticatorImpl();
public DefaultApiAuthenticatorImpl(CredentialStorage credentialStorage);
// 函數(shù)
void auth(String url);
void auth(ApiRequest apiRequest);
2.2 如何進(jìn)行面向?qū)ο缶幊?#xff08;OOP)
面向?qū)ο笤O(shè)計(jì)完成之后,已經(jīng)定義了清晰的類(lèi)、屬性、方法、類(lèi)之間的交互,并將所有的類(lèi)組裝起來(lái),提供了統(tǒng)一的執(zhí)行入口。接下來(lái),面向?qū)ο缶幊痰墓ぷ?#xff0c;就是將這些設(shè)計(jì)思路翻譯成代碼實(shí)現(xiàn)。有了前面分析,這部分工作相對(duì)來(lái)說(shuō)就比較簡(jiǎn)單了。所以,這里,只給出比較復(fù)雜的 ApiAuthenticator
的實(shí)現(xiàn)。
對(duì)于 AuthToken
、ApiRequest
、CredentialStorage
這三個(gè)類(lèi),就不給出具體代碼實(shí)現(xiàn)了。你可以自己試著把整個(gè)鑒權(quán)框架自己實(shí)現(xiàn)一遍。
public interface ApiAuthenticator {void auth(String url);void auth(ApiRequest apiRequest);
}public class DefaultApiAuthenticatorImpl implements ApiAuthenticator {private CredentialStorage credentialStorage;public DefaultApiAuthenticatorImpl() {this.credentialStorage = new MysqlCredentialStorage();}public DefaultApiAuthenticatorImpl(CredentialStorage credentialStorage) {this.credentialStorage = credentialStorage;}@Overridevoid auth(String url) {ApiRequest apiRequest = ApiRequest.createFromUrl(url);auth(apiRequest);}@Overridevoid auth(ApiRequest apiRequest) {String appId = apiRequest.getAppId();String token = apiRequest.getToken();long timestamp = apiRequest.getTimestamp();String baseUrl = apiRequest.getBaseUrl();AuthToken clientAuthToken = new AuthToken(token, timestamp);if(clientAuthToken.isExpired()) {throw new RuntimeException("Token is exipred.");}String password = credentialStorage.getPasswordByAppId(appId);AuthToken serverAuthToken = AuthToken.generator(baseUrl, appId, password, timestamp);if(!serverAuthToken.match(clientAuthToken)) {throw new RuntimeException("Token verfication failed.");}}
}
2.3 辯證思考與靈活應(yīng)用
之前講解過(guò),面向?qū)ο蠓治?、設(shè)計(jì)、編程,每個(gè)環(huán)節(jié)的界限劃分都比較清楚。而且,設(shè)計(jì)和實(shí)現(xiàn)基本上是按照功能點(diǎn)的描述,逐句照著翻譯過(guò)來(lái)的。這樣做的好處是先做什么,后做什么,都非常清晰、明確。
不過(guò)在平時(shí)的工作中,大部分程序員往往都是在腦子里或者草紙上完成面向?qū)ο蠓治龊驮O(shè)計(jì)后,然后就開(kāi)始寫(xiě)了,邊寫(xiě)邊思考重構(gòu),并不會(huì)嚴(yán)格地按照剛剛的流程來(lái)執(zhí)行。而且,說(shuō)實(shí)話,即使在寫(xiě)代碼之前,花很多時(shí)間做分析和設(shè)計(jì),繪制出完美的類(lèi)圖、UML 圖,也不可能把每個(gè)細(xì)節(jié)、交互都想的很清楚。在落實(shí)到代碼的時(shí)候,還是要反復(fù)迭代、重構(gòu)、打破重寫(xiě)。
畢竟,整個(gè)軟件開(kāi)發(fā)本來(lái)就是一個(gè)迭代、修修補(bǔ)補(bǔ)、遇到問(wèn)題解決問(wèn)題的過(guò)程,是一個(gè)不斷重構(gòu)的過(guò)程。我們沒(méi)法嚴(yán)格地按照順序執(zhí)行各個(gè)步驟。
2.4 總結(jié)回顧
面向?qū)ο蠓治龅漠a(chǎn)出是詳細(xì)的需求描述。面向?qū)ο笤O(shè)計(jì)的產(chǎn)出是類(lèi)。在面向?qū)ο笤O(shè)計(jì)這一環(huán)節(jié),我們將需求描述轉(zhuǎn)化為具體的類(lèi)的設(shè)計(jì)。這個(gè)環(huán)節(jié)的工作可以分為四步:
- 劃分職責(zé)進(jìn)而識(shí)別出有哪些類(lèi)
根據(jù)需求描述,把其中涉及的功能點(diǎn),一個(gè)個(gè)羅列出來(lái),然后再去看哪些功能點(diǎn)職責(zé)相近,操作同樣的屬性,可否歸為一個(gè)類(lèi)。 - 定義類(lèi)的屬性和方法
識(shí)別出功需求中的動(dòng)詞,作為候選方法,再進(jìn)一步過(guò)濾篩選出真正的方法;把功能點(diǎn)中涉及的名詞,作為候選屬性,然后再同樣進(jìn)行過(guò)濾篩選。 - 定義類(lèi)與類(lèi)之間的關(guān)系
UML
統(tǒng)一建模語(yǔ)言定義了六種類(lèi)之間的關(guān)系。分別是:泛化、實(shí)現(xiàn)、關(guān)聯(lián)、組合、聚合、依賴。從貼近編程的角度,我們對(duì)類(lèi)之間的關(guān)系做了調(diào)整,保留四個(gè)關(guān)系:泛化、實(shí)現(xiàn)、組合、依賴。 - 將類(lèi)封裝起來(lái)并提供執(zhí)行入口
將所有類(lèi)組裝在一起,提供一個(gè)執(zhí)行入口。這個(gè)入口可能是main()
函數(shù),也可能是一組給外部調(diào)用的API
接口。通過(guò)這個(gè)接口,我們能觸發(fā)整個(gè)代碼跑起來(lái)。