奧推網

選單
科技

不要讓框架控制你的專案,過度依賴框架,可能最終會害了你

摘要:不要讓框架控制你的專案!

原文連結:https://berk。es/2022/09/06/frameworks-harm-maintenance/

宣告:本文為 CSDN 翻譯,未經允許禁止轉載。

作者 | Bèr Kessels

譯者 | 彎月

出品 | CSDN(ID:CSDNnews)

在本文中,我們來探討一下使用框架構建軟體,對軟體的可維護性有哪些危害。我認為:

使用框架有損於軟體的可維護性。

框架與個人或團隊有著不同的目標。

框架設計中的權衡會危及專案的可維護性。

框架的構建初衷就是為了控制你的專案。

以解耦的方式採用框架,不僅能享受框架帶來的好處,而且還可以避免損害可維護性。

框架是什麼?

首先,我們來弄清楚框架的準確含義。框架不僅僅是使用第三方程式碼,也不僅僅是一種方法或架構:

軟體框架(software framework),通常指的是為了實現某個業界標準或完成特定基本任務的軟體元件規範,也指為了實現某個軟體元件規範時,提供規範所要求之基礎功能的軟體產品。

軟體框架與普通的程式碼庫之間有幾個關鍵的區別:

控制反轉:框架與庫或標準使用者應用程式不同,整個程式的控制流不是由呼叫者決定的,而是由框架決定的。而這通常是透過模板來實現的。

可擴充套件性:使用者可以按照過載的方式擴充套件框架,即編寫使用者專用的程式碼來提供特定的功能。

不可修改的框架程式碼:一般來說,框架程式碼不應該被修改,但可以接受使用者的擴充套件。換句話說,使用者可以擴充套件框架,但不能修改其程式碼。

根據定義,框架的主要功能是提供功能、行為、流程和預設值,而且所有這些都是框架內建的,其中一些是不可更改或指定的。框架允許使用者新增程式碼,但不能更改其程式碼。

所有的軟體框架都可能引入維護的問題,但我個人使用框架的經驗僅限於Web服務(API、後端、全棧)、命令列和GUI。2022年,越來越多的軟體朝著Web發展,因此,本文討論的例子也僅限於Web框架。

人們使用框架的目的是,以更標準化、更快、更容易、更安全、更好的可擴充套件性、更一致或更有趣的方式開發軟體。然而很諷刺的是,根據維基百科的介紹,使用框架不會提供任何好處,相反只有種種弊端。

標準化背後的思想是,迫使開發人員按照事先定義好的方式編寫程式碼。使用框架不僅可以統一程式碼的組織方式,而且API和邏輯也更容易辨識。然而,我發現結果卻事與願違。2021年開發運維現狀報告(參考連結:https://puppet。com/blog/2021-state-of-devops-report/)表明,使用框架之類的技術根本無法保證專案成功。而強迫開發人員使用框架只能導致情況進一步惡化。

低效公司的通病往往表現在:由一個團隊定義標準、流程、實踐、框架或架構,而其他團隊則必須遵守。

相反,高績效公司往往缺乏這些所謂的“統一標準”。

換句話說:強制標準化技術,往往得不償失。

其實,這並非沒有道理:如果強制公司中的每個人都使用 Django,而無論專案的實際情況如何,那麼最終必然有很多專案會因為選擇Django而處處碰壁。

儘管如此,框架確實能夠為某個專案或團隊提供好處。但是,標準化(和統一)基本沒有任何好處,甚至弊大於利。

專案的開發速度、趣味性以及難易度,在很大程度上取決於專案所處的階段。利用框架生成模型的程式碼,可以節省編寫初始程式碼的時間。這一點我也同意。但是,對於一個開發了十幾年的中大型團隊來說,節省的這點時間(半個小時?)是微不足道的。尤其是,經過了這麼長的時間,框架可能生成了數百個這樣的模型,而其餘幾萬個小時都花在了修改和維護現有程式碼上。下面,我會詳細說明從專案的長期發展來看,這種短暫的“開發速度提升”換來的卻是對可維護性的損害。

此外,安全性和效能非常依賴於大環境。框架會向專案新增大量程式碼。運氣好的話,這些程式碼無傷大雅;但倘若運氣不佳,則可能引入大量的潛在攻擊和大量的開銷。我將在下文中展示其實不使用框架更加容易確保安全性並提高效能。

“有損於維護性”指什麼?

軟體順利啟動,並投入使用,接下來我們只需要正常維護。維護通常分為以下幾類:

糾正式的軟體維護:修復bug;

預防式的軟體維護:防止錯誤,穩步改進;

完美式的軟體維護:修飾與潤色;

適應式的軟體維護:持續開發。

不過,在本文中,我打算將軟體投入使用後的所有變更都視為維護。

在維護期間內,任何阻礙維護工作持續開展的因素,都應被視為危害。因此,如果使用框架會導致新功能的釋出速度減慢,則視為危害。

此外,如果在軟體開發的早期,使用框架有助於快速釋出功能,但相應的代價是導致後期新功能的釋出速度減慢,則視為有損於維護性。

第三種危害是,框架的使用導致我們需要付出額外的努力,但這部分工作並不能為客戶提供價值,比如框架升級、棄用、教育和資訊攝入(例如學習新功能)等。這些工作需要付出昂貴的代價,而且往往是稀缺資源,比如你需要花費大量時間升級技術棧,原本這些時間應花在提供使用者或市場想要的新功能。

最後一種危害是,將來框架有可能不再適合專案。如果框架朝著不同的方向發展,或者使用了框架的軟體朝著不同的方向發展,那麼二者就不再適配。

框架與個人或團隊有著不同的目標

Ruby on Rails創始人DHH曾表示:

雖然你寄予了框架巨大的希望,但框架並沒有對你做出任何承諾。框架可以按照創始人的喜好,朝著任何方向發展。而你只能像一隻忠實的小狗一樣默默跟隨。

我敢肯定,大多數框架的創始人對使用者沒有任何敵意,他們發自真心關心使用者,而DHH肯定也希望使用者在使用Rails時感受到快樂。但是,這些創始人更關心的是有多少使用者願意使用框架,並一路相隨,而不是你能否在接下來的十五、二十年內繼續創造價值。

許多Web框架,比如Django、Rails、Spring、Gatsby 和 Symfony等的營銷詞中都提到了維護以及可維護性。

Symfony:加快建立和維護PHP Web應用程式的速度。擺脫重複的程式設計任務,享受控制程式碼的力量。

那麼他們是如何實現的呢:

使用最佳實踐確保應用程式的穩定性、可維護性和可升級性。

關於框架如何提供長期的支援,Rails 的官方立場是:

當某個版本系列不再受支援時,修復錯誤和安全問題的責任由您自行承擔。我們會提供修補程式的向後移植併發布到git,但是不會發布新版本。如果你無力維護自己的版本,則應升級到受支援的版本。(參考連結:https://rubyonrails。org/maintenance)

他們的立場很明確:框架不會長期提供支援。為了讓專案使用最新版本的Rails,你需要更新或移植框架,但這些工作都需要資源。

再者,即便眼下框架與你的目標完全一致,但將來呢?尤其是對於剛剛啟動的專案來說,誰又能預知未來呢?你的產品會堅持Web應用的路線?你確定將來只發布Windows桌面版的應用程式?你確定在接下來的幾年中關係資料庫是最佳儲存解決方案?你確定你需要可擴充套件性?十年之後JavaScript PWA還會存在嗎?

然而,在選擇框架構建產品時,你就與它深度綁定了。永遠綁定了。在專案之初,在擁有的資訊量最少的那一刻,你卻做出了最關鍵的決定。

框架設計中的權衡會危及專案的可維護性

與其他軟體一樣,框架的建立者必須做出權衡。例如,從流行框架的網站宣傳中就可以看出,所有的流行框架都格外注重開發速度和可擴充套件性。

然而,這兩個特徵與可維護性沒有任何關係,相反在有些情況下還會損害可維護性。

開發速度的提升部分來自樣板程式碼的生成,但更多時候來自繼承。框架生成程式碼就意味著建立新程式碼,但不負責維護這些程式碼。例如react-boilerplate 或 create-react-app等框架就會生成大量的樣板程式碼,它們只是程式碼生成器。但程式碼必須維護,否則就會降級,並引發各種問題,比如大量重複、不一致、不相容等,也就是我們常說的“程式碼腐爛”。

框架可以透過其他手段解決程式碼腐爛的問題,比如將所有程式碼都放入超類(或可重用函式)中,這樣就能在一個合理的地方統一提供樣板程式碼。作為使用者(即使用框架的開發人員),你可以繼承類,或者採用mixin的方式使用其他類、模組或函式的程式碼。

例如,在Rails中,你只需要繼承“一個模型”,就可以讓物件公開大量方法。舉個例子,假設Post有三個資料庫欄位:

class Post < ActiveRecord::Base; end

那麼,你至少可以獲得 767 個公共類方法和 487 個公共例項方法,也就是說,你可以透過子類化繼承1200 多個方法!

由於Post類提供了這麼多方法,所以你就必須維護它們。畢竟,你的類為使用者提供了這些方法。這些方法存在於你的類中、你的例項中。

它們深埋於框架的程式碼中,這就成了你的責任,由你來維護它們。這就是框架的本質,你無法改變,也無法控制。

框架甚至可以決定在某個時刻棄用或修改某個方法。由於使用了框架,所以我們提供了大量的公共介面,卻沒有能力控制它。我們的一切都將受到牽制,寄希望於框架的建立者是個好心人,能提供更新,並保證框架的向後相容性和可用性。雖然大多數框架的建立者都很友好,但誰也無法保證這些API永遠穩定。還有Drupal之類的框架提供的升級如此龐大,導致使用者不得不完全重寫專案,而且每隔幾年就要經歷一次這樣的升級!雖然有些框架很友好,會努力保持向後相容,而且每次升級都是很小的一步,但更新還是避免不了。而我們只能俯首聽命,必要時修改現有程式碼。

雖然許多框架不像 Rails 那樣極端,公共介面包含 1200 多個方法。但所有框架都為使用者提供了 API、函式和類,畢竟這正是框架存在的意義。

我們使用這些程式碼,並隨著時間的推移,將我們的程式碼更加緊密地耦合到框架中。直到我們的程式碼完全依賴於框架。

所以人們常說,在框架內開發軟體,而不是利用框架開發軟體,因為你確實是在框架中構建專案。

此外,框架所能提供的效能與擴充套件水平是相較於其他類似的框架而言的。如果我們能選擇底層架構,並進行最佳化,那麼就能利用更少的程式碼,編寫更高效、更具擴充套件性的軟體。而另一方面,各種框架卻因導致專案出現效能問題,而頻繁地出現在各大新聞頭條中。例如,推特的“Fail-Whale”(失敗鯨)事件就是因為Rails糟糕的效能引發的,後來推特宣佈用Java重寫了Rails程式碼庫。此次事件證明,大多數框架都會顯著增加效能開銷。

擴充套件和效能問題的常見解決方案是,選擇適合的架構,最佳化底層程式碼,並減少總程式碼量,這就意味著我們必須能夠在發現效能問題時自由修改程式碼。只有掌握足夠的資訊,我們才能做出正確的選擇和最佳化。而框架會損害可擴充套件性,因為我們很難從一個框架遷移到更適合的其他框架或架構,或者建立更合適的設定。在遇到“Fail-Whale”之類的問題時,我們都希望最佳化有問題的程式碼,而不是用Java重寫所有程式碼。

框架的構建初衷就是為了控制你的專案

使用框架開發軟體時,專案必然會與框架深度繫結。每次我們在Rails中編寫:belongs_to(:author),或者在Django中編寫:models。ForeignKey(“Band”),就會導致我們的專案與框架的繫結更加緊密。

如果只是很小的一部分程式碼繫結到框架,那麼還能保證一定的可維護性。然而,當這種繫結的覆蓋範圍很大,界限模糊或完全消失時,就很難維護了。當我們的領域和業務邏輯與框架程式碼混在一起;當高階業務概念與底層的架構機制混在一起;當業務邏輯混入底層架構,我們必須閱讀控制器、檢視、模型、工廠、服務、配置檔案、庫、框架程式碼,才能搞明白為什麼案例A中建立了User,而案例B不需要,那麼可維護性就無從談起了。

框架抽象出了許多技術細節,它們會提供一個ORM來抽象資料庫的處理,有時開發人員甚至根本不需要知道自己正在使用資料庫。他們只需呼叫model。save或User。find_by(email: “example。com”) ,就能儲存或獲取資料,而根本不知道這些資料實際上儲存在PostgreSQL、sqlite還是MongoDB中。雖然我們不會被繫結到特定的資料庫,但會繫結到ORM和框架。你可以自由使用任何資料庫,但代價是無法再使用另一個ORM和框架。

HTTP、儲存(如資料庫)、事件匯流排、日誌記錄、訊息傳遞等底層的機制,所有這些都是細節,它們與你的業務邏輯和領域無關。

會計應用的架構應該叫做“會計”,而不是 Spring & Hibernate。@unclebobmartin

然而,這些框架還鼓勵開發人員將邏輯與框架程式碼混合在一起。他們提供了各種API、類和函式,供我們在業務邏輯中使用。因此,我們的程式碼不僅會與框架緊密耦合,而且還會將業務邏輯和樣板程式碼徹底混在一起。更糟糕的是,他們經常鼓勵我們透過這些“細節”來傳播業務邏輯。在MVC模型中,M是儲存,V是模板,而C是HTTP層,卻沒有提供一個統一的、合乎邏輯的地方來儲存邏輯和領域程式碼。框架鼓勵我們將這些程式碼放在最近的地方,而不是最方便維護的地方。

在框架中開發軟體時,類似於如下的情況並不少見:

def create if User。exists?(email: params[:email]) render :new, status: :already_exists elsif user。save flash[:success] = flash_message_for(@user, :successfully_created) redirect_to edit_admin_user_path(@user) else render :new, status: :unprocessable_entity endend

def user_params params。require(:user)。permit(permitted_user_attributes | [:use_billing, role_ids: [], ship_address_attributes: permitted_address_attributes, bill_address_attributes: permitted_address_attributes])end

仔細閱讀上述程式碼,會讓人感到心驚肉跳。這段程式碼非常缺乏連貫性,我們的思維從領域邏輯一躍而下,經過框架API到交付機制的細節,然後輾轉安全細節,再到業務邏輯,最後返回。看似是一段HTTP層的程式碼,裡面卻夾雜著許多業務邏輯。

如果是在一個乾淨的分層架構中,我們肯定會分離這些技術細節,避免將它們混合在一起,同時將業務邏輯統一放在一個地方。

在這樣的架構中,框架的作用並不重要,領域(或層)的意義就在於獨立、沒有任何依賴關係。這樣的領域程式碼不會依賴於反序列化 JSON、HTTP 標頭、資料庫事務、連線池等任何技術細節。這樣的領域只關心領域語言,比如它只會呼叫抽象方法posts_repository。create(post)。

這樣的系統擁有良好的可維護性,因為所有程式碼的作用都很明確。這樣的系統是隔離的,而且是一個整體。如果你想修改Post的儲存(比如你放棄MongoDB,轉而採用直接在磁碟中儲存Markdown檔案),則只需修改PostsRepository。任何與業務邏輯相關的程式碼都不需要動。

將這些實現細節放入單獨的一層,那麼軟體就會更加易於維護,因為程式碼變更都是單獨的。有了這樣的架構,即便使用了框架,也會被拋在一邊,而且每次只需更換一小塊的難度會大大降低。

以解耦的方式採用框架,不僅能享受框架帶來的好處,而且還可以避免損害可維護性

許多人可能會說,不使用框架則意味著我們需要動手編寫所有程式碼。這種非黑即白的看法有點過於極端。我們可以很好地利用庫和框架,同時也要編寫好程式碼。我們應該依靠(安全)專家來編寫關係到安全的程式碼。如果可以避免,我們又何須學習如何編寫加密演算法或處理密碼的程式碼。我們應該使用庫來處理這些細節。

但是,我們應該明確指定一個單獨的地方。負責將HTTP路徑對映為方法呼叫的程式碼就應該放在HTTP層,不應該牽扯任何業務邏輯。隔離度越高,可維護性就越好。程式碼令牌認證等處理不應該由我們編寫,而是應該統一放入一個單獨的、有界限的區域。最好將其封裝起來,並轉換成領域語言,如authentication。is_known_as_admin(request。token)。

傳送訊息的方法應該簡單地定義為messenger。deliver(recipent, body)。該方法的背後是一個完整的訊息傳遞框架,不僅提供指數退避重試、緩衝、智慧路由等功能,而且可以推送通知和傳送電子郵件。

儲存費用的方法叫做expenses_repository。add(expense),其背後可能使用了世界上最複雜的分散式資料庫框架,或者使用了一個漂亮的框架將費用推送到某個線上會計工具中。

關鍵不是永遠不要使用框架,而是要隔離它們,並統一從一個地方呼叫。將框架的影響範圍降到最低,這是我們的責任。

然而,大多數框架預先定製了很多技術細節,並且都混合在一起。因此,我們很難將它們分開。這樣的框架已經失去了意義,很快就會變成庫。

為什麼沒有這樣的框架?

首先,我們的基本思路是不依賴於框架,但構建框架卻不使用框架,這與框架本身的目標背道而馳。

其次,可維護性良好的軟體需要隨著時間的推移而不斷髮展,以適應不斷變化的需求。

從HTTP遷移到事件匯流排時,顯然你不再需要HTTP框架。當從基於 Web 的服務轉而使用原生移動應用的服務時,你所需要的也不再是HTML/CSS/asset,而是序列化和處理 JSON 請求的方法。可維護性要求軟體不斷髮展。HTTP框架提供HTTP服務,但是當需求發生變化,且你不再需要HTTP服務時,卻沒辦法刪掉這些框架。一些 MVC 框架提供使用關係資料庫的 ORM,但如果ORM框架過時,你也沒辦法擺脫它們。

第三,有些實現並不需要框架。例如,CQRS之類的架構實際上就是一個簡單的if語句:if(is_command) { command(params) } else { query(params) },寫這種程式碼根本不需要框架。

最後,維護工作的難易程度與使用特定的工具或框架無關。正如Symfony指出的那樣:

最佳實踐可以保證應用程式的穩定性、可維護性和可升級性。

而“最佳實踐”之一就是不要讓框架控制你的專案!