為什么您的代碼需要抽象層?
譯文【51CTO.com快譯】抽象是編寫設計良好的軟件最重要的方面之一。
了解這個基本概念將為您提供可遵循的系統和清晰的思維模型,以了解如何創建好的抽象。
好的抽象降低了復雜性,并允許開發人員更輕松地更改代碼并減少錯誤。但是創建抽象并非易事。那么您究竟如何做到這一點,需要采取哪些步驟?
什么是抽象?
談論代碼中的抽象層之前,不妨簡要地談談抽象是什么。
抽象可以定義為通過以下方式簡化實體的過程:
1. 省略不重要的細節。
2. 暴露接口。
所有抽象在這方面都大同小異。
自動駕駛汽車是抽象的實際例子。在這種情況下,離合器是抽象的,駕駛員可以更輕松地換檔。
抽象也有不足。比如說,雖然駕駛員可以更輕松地換檔,但現在對汽車的控制也較少,因此為賽車駕駛員抽象離合器可能是壞主意。
作者John Ousterhout在《軟件設計理念》一書中談到了抽象可能出錯的兩種方式:
1. 包含不重要的細節:由于包含不重要的細節,抽象變得過于復雜,導致開發人員的認知負擔加大。
2. 省略重要細節:Ousterhout將這種抽象稱為“虛假抽象”,因為查看抽象的開發人員不會擁有他們需要的所有信息。
所以,好的抽象需要兼顧和權衡。
代碼中的抽象
我們已知道了抽象,但它如何應用于代碼?
所有代碼可以歸類為策略或細節。
- 策略:這些是實體和業務邏輯。
- 細節:這是策略的實現。細節執行策略。
假設您有一個 User 實體。用戶有某個接口以及某個業務邏輯。這個User實體還有組,您被指派編寫獲取所有用戶組的代碼。
在這里,策略是用戶本身,因為它是一個實體,但它也是getUserGroups函數,因為它是與該實體相關的業務邏輯。
它如何實現、使用哪個數據庫、使用哪個ORM(對象關系映射)、使用哪些庫、如何編寫代碼以及所有不同的實現,這些都是代碼的細節部分。
在您的代碼中,您希望在隱藏細節的同時暴露策略。策略和細節之間的這種分離讓您可以切換和輕松重構實現。
如果您的策略和細節是耦合的,就很難重構,因為它們混合在一起,更改會從一個傳播到另一個。
在設計良好的系統中,策略和細節之間的分離是關鍵。
那么這如何應用于抽象層呢?
抽象層
抽象層暴露了接口,并隱藏了它背后的實現細節。
抽象層的目的是創建抽象。層里面的方法和屬性應該是暴露的接口,而這些方法里面的實現是細節層中的一切。
創建抽象層主要有三個好處:
1. 集中:通過在一層中創建抽象,與其相關的所有內容都是集中的,因此可以在一處進行任何更改。集中與“不要重復自己”(DRY)原則有關,這很容易被誤解。
DRY不僅涉及代碼的重復,還涉及知識的重復。有時,兩個不同的實體可以復制相同的代碼,因為這可以實現分離,允許這些實體將來分別演進。
2. 簡化:通過創建抽象層,您可以暴露特定的功能并隱藏實現細節。現在代碼可以直接與您的接口交互,避免處理不相關的實現細節。這提高了代碼的可讀性,減輕了閱讀代碼的開發人員的認知負擔。為何?
因為策略不如細節復雜,所以與其交互更直接。
3. 測試:抽象層非常適合測試,因為您可以把細節換成另一組細節,這有助于隔離正在測試的區域,并正確創建測試替代(test doubles)。
測試代碼時,開發人員需要測試特定的功能,同時為某些功能創建測試替代,以避免調用真正的數據庫之類的對象。策略和細節糾纏在一起時,過度使用測試替代很常見,這使得覆蓋率更低,測試的用處也大大降低。
為數據庫實現對象創建抽象層時,開發人員可以替換該層,確保在測試其余功能時僅替換數據庫響應。
創建抽象層的示例
假設您為組創建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)
- .
- .
- .
- }
可從上述例子看出,該函數邏輯與策略和細節混合在一起。它處理很多不同的功能,并不使用任何抽象層。
這是使用抽象層的代碼:
- 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'
- }
- }
第二個實現有諸多好處:
1. 更容易理解,因為實現細節是抽象的,您在閱讀的是與策略交互的代碼。
2. 一切都集中在一項服務中。想象一下與組有關的代碼散布在整個應用程序中。所做的每一次更改都需要到處進行;至少可以說,這會有問題。
3. 代碼更加封裝。注意控制器createUserGroup現在不知道配額,只知道組創建,因為配額無關緊要。
4. 我們可以專注于測試實現,同時僅把細節層換成測試替代,使測試更容易。至于集成測試,我們可以替換QuotaService和GroupService,并測試該特定控制器所實現的實現。
可能的應用
抽象層可以通過許多不同的方式實現,其中最常見的用例是:
1. 通過分離策略和細節創建更精簡的組件:如果變更和重構很容易,您的代碼將通過時間的考驗。分離策略和細節,同時僅用接口保持組件之間的交互提供了未來代碼演變所需的基礎設施。
2. 包裝第三方庫:您的代碼中過時的第三方庫阻止您升級其他依賴項是一場噩夢,如果該依賴項存在安全風險,尤為糟糕。
通過在一個中央抽象層中使用您自己的接口包裝第三方庫,變得將很容易,因為它們只需要在暴露接口的那一處進行。
3. 創建實用服務:實用服務是提高開發速度和重用通用代碼段的關鍵方法。
比如說,如果您在開發處理大量不同時間和日期功能的特性,為什么不創建幾個實用函數來幫助您、并將它們放在一處供進一步重用?
小結
創建抽象層通過提供三大好處來幫助顯著改進代碼:集中、簡化和更好的測試。
請記住,抽象層和一般的抽象不是目的,而是實現目的的手段。抽象可能有缺點。一個常見的例子是某些抽象會影響性能。所以總是要先了解不足。
原文標題:Why Your Code Needs Abstraction Layers,作者:Yair Cohen
【51CTO譯稿,合作站點轉載請注明原文譯者和出處為51CTO.com】