為什么您的代碼需要抽象層?
譯文【51CTO.com快譯】抽象是編寫設(shè)計(jì)良好的軟件最重要的方面之一。
了解這個(gè)基本概念將為您提供可遵循的系統(tǒng)和清晰的思維模型,以了解如何創(chuàng)建好的抽象。
好的抽象降低了復(fù)雜性,并允許開發(fā)人員更輕松地更改代碼并減少錯(cuò)誤。但是創(chuàng)建抽象并非易事。那么您究竟如何做到這一點(diǎn),需要采取哪些步驟?
什么是抽象?
談?wù)摯a中的抽象層之前,不妨簡要地談?wù)劤橄笫鞘裁础?/p>
抽象可以定義為通過以下方式簡化實(shí)體的過程:
1. 省略不重要的細(xì)節(jié)。
2. 暴露接口。
所有抽象在這方面都大同小異。
自動駕駛汽車是抽象的實(shí)際例子。在這種情況下,離合器是抽象的,駕駛員可以更輕松地?fù)Q檔。
抽象也有不足。比如說,雖然駕駛員可以更輕松地?fù)Q檔,但現(xiàn)在對汽車的控制也較少,因此為賽車駕駛員抽象離合器可能是壞主意。
作者John Ousterhout在《軟件設(shè)計(jì)理念》一書中談到了抽象可能出錯(cuò)的兩種方式:
1. 包含不重要的細(xì)節(jié):由于包含不重要的細(xì)節(jié),抽象變得過于復(fù)雜,導(dǎo)致開發(fā)人員的認(rèn)知負(fù)擔(dān)加大。
2. 省略重要細(xì)節(jié):Ousterhout將這種抽象稱為“虛假抽象”,因?yàn)椴榭闯橄蟮拈_發(fā)人員不會擁有他們需要的所有信息。
所以,好的抽象需要兼顧和權(quán)衡。
代碼中的抽象
我們已知道了抽象,但它如何應(yīng)用于代碼?
所有代碼可以歸類為策略或細(xì)節(jié)。
- 策略:這些是實(shí)體和業(yè)務(wù)邏輯。
- 細(xì)節(jié):這是策略的實(shí)現(xiàn)。細(xì)節(jié)執(zhí)行策略。
假設(shè)您有一個(gè) User 實(shí)體。用戶有某個(gè)接口以及某個(gè)業(yè)務(wù)邏輯。這個(gè)User實(shí)體還有組,您被指派編寫獲取所有用戶組的代碼。
在這里,策略是用戶本身,因?yàn)樗且粋€(gè)實(shí)體,但它也是getUserGroups函數(shù),因?yàn)樗桥c該實(shí)體相關(guān)的業(yè)務(wù)邏輯。
它如何實(shí)現(xiàn)、使用哪個(gè)數(shù)據(jù)庫、使用哪個(gè)ORM(對象關(guān)系映射)、使用哪些庫、如何編寫代碼以及所有不同的實(shí)現(xiàn),這些都是代碼的細(xì)節(jié)部分。
在您的代碼中,您希望在隱藏細(xì)節(jié)的同時(shí)暴露策略。策略和細(xì)節(jié)之間的這種分離讓您可以切換和輕松重構(gòu)實(shí)現(xiàn)。
如果您的策略和細(xì)節(jié)是耦合的,就很難重構(gòu),因?yàn)樗鼈兓旌显谝黄?,更改會從一個(gè)傳播到另一個(gè)。
在設(shè)計(jì)良好的系統(tǒng)中,策略和細(xì)節(jié)之間的分離是關(guān)鍵。
那么這如何應(yīng)用于抽象層呢?
抽象層
抽象層暴露了接口,并隱藏了它背后的實(shí)現(xiàn)細(xì)節(jié)。
抽象層的目的是創(chuàng)建抽象。層里面的方法和屬性應(yīng)該是暴露的接口,而這些方法里面的實(shí)現(xiàn)是細(xì)節(jié)層中的一切。
創(chuàng)建抽象層主要有三個(gè)好處:
1. 集中:通過在一層中創(chuàng)建抽象,與其相關(guān)的所有內(nèi)容都是集中的,因此可以在一處進(jìn)行任何更改。集中與“不要重復(fù)自己”(DRY)原則有關(guān),這很容易被誤解。
DRY不僅涉及代碼的重復(fù),還涉及知識的重復(fù)。有時(shí),兩個(gè)不同的實(shí)體可以復(fù)制相同的代碼,因?yàn)檫@可以實(shí)現(xiàn)分離,允許這些實(shí)體將來分別演進(jìn)。
2. 簡化:通過創(chuàng)建抽象層,您可以暴露特定的功能并隱藏實(shí)現(xiàn)細(xì)節(jié)。現(xiàn)在代碼可以直接與您的接口交互,避免處理不相關(guān)的實(shí)現(xiàn)細(xì)節(jié)。這提高了代碼的可讀性,減輕了閱讀代碼的開發(fā)人員的認(rèn)知負(fù)擔(dān)。為何?
因?yàn)椴呗圆蝗缂?xì)節(jié)復(fù)雜,所以與其交互更直接。
3. 測試:抽象層非常適合測試,因?yàn)槟梢园鸭?xì)節(jié)換成另一組細(xì)節(jié),這有助于隔離正在測試的區(qū)域,并正確創(chuàng)建測試替代(test doubles)。
測試代碼時(shí),開發(fā)人員需要測試特定的功能,同時(shí)為某些功能創(chuàng)建測試替代,以避免調(diào)用真正的數(shù)據(jù)庫之類的對象。策略和細(xì)節(jié)糾纏在一起時(shí),過度使用測試替代很常見,這使得覆蓋率更低,測試的用處也大大降低。
為數(shù)據(jù)庫實(shí)現(xiàn)對象創(chuàng)建抽象層時(shí),開發(fā)人員可以替換該層,確保在測試其余功能時(shí)僅替換數(shù)據(jù)庫響應(yīng)。
創(chuàng)建抽象層的示例
假設(shè)您為組創(chuàng)建API編寫代碼:
- function createUserGroup(group, userId) {
- logger.info('Creating group for user ${userId}')
- db.startTransaction();
- const isValidGroup = validateGroup(group);
- if (!isValidGroup) throw new Error('Invalid group');
- db.addDoc('groups', group)
- dc.addDoc('quotas/groups', 1)
- .
- .
- .
- }
可從上述例子看出,該函數(shù)邏輯與策略和細(xì)節(jié)混合在一起。它處理很多不同的功能,并不使用任何抽象層。
這是使用抽象層的代碼:
- class GroupsService {
- GROUPS_COLLECTION = 'groups';
- createGroup() {
- db.startTransaction();
- const isValid = this.validateGroup();
- if (!isValid) throw new Error('Invalid group')
- db.addDoc(GROUPS_COLLECTION, group)
- quotasService.setQuota('/groups', 1);
- db.finishTransaction();
- }
- validateGroup()
- deleteGroup();
- }
- class QuotasService {
- setQuota(collection: string, value: any) {
- dc.addDoc(`quotas/${collection}`, value)
- }
- }
- function createUserGroup(group, userId) {
- logger.info(`Creating group for user ${userId}`)
- groupsService.createGroup();
- return {
- status: 200,
- message: 'Group created successfully'
- }
- }
第二個(gè)實(shí)現(xiàn)有諸多好處:
1. 更容易理解,因?yàn)閷?shí)現(xiàn)細(xì)節(jié)是抽象的,您在閱讀的是與策略交互的代碼。
2. 一切都集中在一項(xiàng)服務(wù)中。想象一下與組有關(guān)的代碼散布在整個(gè)應(yīng)用程序中。所做的每一次更改都需要到處進(jìn)行;至少可以說,這會有問題。
3. 代碼更加封裝。注意控制器createUserGroup現(xiàn)在不知道配額,只知道組創(chuàng)建,因?yàn)榕漕~無關(guān)緊要。
4. 我們可以專注于測試實(shí)現(xiàn),同時(shí)僅把細(xì)節(jié)層換成測試替代,使測試更容易。至于集成測試,我們可以替換QuotaService和GroupService,并測試該特定控制器所實(shí)現(xiàn)的實(shí)現(xiàn)。
可能的應(yīng)用
抽象層可以通過許多不同的方式實(shí)現(xiàn),其中最常見的用例是:
1. 通過分離策略和細(xì)節(jié)創(chuàng)建更精簡的組件:如果變更和重構(gòu)很容易,您的代碼將通過時(shí)間的考驗(yàn)。分離策略和細(xì)節(jié),同時(shí)僅用接口保持組件之間的交互提供了未來代碼演變所需的基礎(chǔ)設(shè)施。
2. 包裝第三方庫:您的代碼中過時(shí)的第三方庫阻止您升級其他依賴項(xiàng)是一場噩夢,如果該依賴項(xiàng)存在安全風(fēng)險(xiǎn),尤為糟糕。
通過在一個(gè)中央抽象層中使用您自己的接口包裝第三方庫,變得將很容易,因?yàn)樗鼈冎恍枰诒┞督涌诘哪且惶庍M(jìn)行。
3. 創(chuàng)建實(shí)用服務(wù):實(shí)用服務(wù)是提高開發(fā)速度和重用通用代碼段的關(guān)鍵方法。
比如說,如果您在開發(fā)處理大量不同時(shí)間和日期功能的特性,為什么不創(chuàng)建幾個(gè)實(shí)用函數(shù)來幫助您、并將它們放在一處供進(jìn)一步重用?
小結(jié)
創(chuàng)建抽象層通過提供三大好處來幫助顯著改進(jìn)代碼:集中、簡化和更好的測試。
請記住,抽象層和一般的抽象不是目的,而是實(shí)現(xiàn)目的的手段。抽象可能有缺點(diǎn)。一個(gè)常見的例子是某些抽象會影響性能。所以總是要先了解不足。
原文標(biāo)題:Why Your Code Needs Abstraction Layers,作者:Yair Cohen
【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請注明原文譯者和出處為51CTO.com】