重構,有品位的代碼之重構API
本文轉載自微信公眾號「前端萬有引力」,作者 一川 。轉載本文請聯系前端萬有引力公眾號。
寫在前面
伙伴們,最近事情有點多、空余時間都花在學習新知識、新技術以及鞏固基礎上了,在實踐開發越來越覺得自己的技術和能力有限,認識到了自己的短板和不足。后面我會把自己所學所看,以及在項目實踐中對方法進行總結,分享給各位伙伴們共同學習、批評指正。
今天就繼續分享《重構,有品味的代碼》系列第八篇文章,As you know,模塊和函數組成了軟件的鋼筋水泥,而api就是整個軟件建筑的棟和梁。顯而易見,在對軟件開發有了深層次的理解,我們會發現如何改進api將更新數據的函數和讀取數據的函數進行分割。讓每個函數都做自己的本分,銜接它們之間的事情交給模塊去調用。
重構API
常見的重構API的方法有:
- 將查詢函數和修改函數分離
- 函數參數化
- 移除標記參數
- 保證完整性
- 以查詢取代參數
- 以參數取代查詢
- 移除設值函數
- 以工廠函數取代構造函數
- 以命令取代函數
- 以函數取代命令
1. 將查詢函數和修改函數分離
如果函數只是作為取值函數,沒有其他多余的實現功能,那么這個函數是很單純的、很有價值的東西。因為可以任意調用此函數,可以在整個項目的任意角落使用,無需擔心有其它多余的累贅。記住:任何有返回值的函數,不應該有其它多余的功能,即命令和查詢分開。
通常做法是:拷貝整個函數將其作為一個查詢來命名,在新建的此查詢函數中移除所有有附加功能的語句,并對其進行檢查原函數的所有調用處。如果調用處使用了該函數的返回值,就將其改為調用新建的查詢函數,并在下面立刻進行一次調用,且從原函數中移除返回值。
舉個栗子
- //原始寫法
- const setOk = ()=>{...}
- const selectPeopleFun = (people)=>{
- for(let p in people){
- if(p === "yichuan"){
- setOk();
- return "good";
- }
- if(p === "onechuan"){
- setOk();
- return "ok";
- }
- return "";
- }
- }
- //重構寫法
- const setOk = ()=>{...}
- const findNull = (people)=>{
- for(const p of people){
- if(p === "yichuan"){
- setOk();
- return;
- }
- if(p === "onechuan"){
- setOk();
- return;
- }
- }
- return;
- }
- const selectPeopleFun = (people)=>{
- if(findNull(people) !== "") setOk();
- }
2. 函數參數化
當我們發現兩個函數的邏輯非常相似,只有某些字面量值不同時,可以將其進行抽取合并成一個函數,以參數的形式傳入不同的值,從而消除重復的邏輯。此重構方法能夠使得邏輯更加簡潔、復用性強,因為每個函數都可以進行多次使用。
舉個栗子
- //原始邏輯
- function useFun(param){...}
- function baseFunction(param){
- if(param < 0) return useFun(param);
- const amount = bottomFun(param) * 0.1 + middleFun(param) * 0.2 + topFun(param) *0.3;
- return useFun(amount);
- }
- function bottomFun(param){
- return Math.min(param,100)
- }
- function middleFun(param){
- return param > 100 ? Math.min(param,200) - 100 :0;
- }
- function topFun(param){
- return param > 200 ? param - 200 : 0;
- }
- //重構代碼
- function commonFun(param,bottom,top){
- return param > bottom ? Math.min(param,top) - bottom:0;
- }
- function baseFun(param){
- if(param<0) return useFun(0);
- const amount = commonFun(param,0,100) * 0.1 + commonFun(param,100,200) * 0.2 + commonFun(param,200,Infinity) *0.3;
- return useFun(amount);
- }
3. 移除標記參數
標記參數直接理解就是作為標記的參數,即通常調用者用其來只是被調用函數應該執行哪部分邏輯。但事與愿違,標記參數在實際使用過程中并沒達到作為標記的作用,令人難以理解到底哪部分函數可以調用、應該如何調用。通常我們通過API查看哪部分是可調用函數,但是編輯參數卻會進行隱藏函數調用中存在的差異性,在使用這些函數我們還得閱讀上下文中標記參數有哪些可用的值。
要知道布爾值作為標記是多么荒唐的使用方法,因為其不能見名知意的傳遞信息,在函數調用時很難厘清true代表的含義,但是明確使用函數完成單獨的任務,就顯得清晰的多。
當然并非所有的類似參數都是標記參數,如果調用者傳入的程序中不斷傳遞的數據,那么這樣的參數就不叫做標記參數。只有當調用者初入字面量值時,或者在函數內部只有參數影響了函數內部的控制流,此時作為參數就是標記參數。
移除標記參數不僅使得代碼更加整潔,并且能夠幫助開發工具更好的發揮作用。去掉標記參數后,代碼分析工具能夠更清晰體現“高級”和“普通”邏輯在使用時的區別。如果某個函數有多個標記參數,此時想要移除得花費功夫,得不償失還不如將其保留,但是也側面證明此函數做的太多,需要將其邏輯進行簡化。
舉個栗子
- //原始代碼
- function setFun(name,value){
- if(name === "height"){
- this._height = value;
- return;
- }
- if(name === "width"){
- this._width = value;
- return;
- }
- }
- //重構代碼
- function setHeight(value){
- this._height = value;
- }
- function setWidth(value){
- this._width = value;
- }
4. 保證完整性
當看到代碼從一個記錄結構中導出幾個值,然后又把這幾個值傳遞給一個函數,那么可以把整個記錄傳遞給這個函數,在函數內部導出所需要的值。
- //原始代碼
- const low = aRoom.dayRange.low;
- const high = aRoom.dayRange.high;
- if(plan.goodRange(low,high)){...}
- //重構代碼
- if(plan.goodRange(aRoom.dayRange)){...}
5. 以查詢取代參數
函數的參數列表應該總結該函數的可變性,標識出函數可能體現出行為差異的主要方式,但是參數列表又應該盡量避免冗余,因為短小精悍易理解。什么是冗余,就是倘若調用函數中傳入一個值,而這個值由函數自己獲取,這個本不必要的參數會增加調用者的難度,因為調用者不得不去找出此參數定義的位置。
如果想要移除得參數值只需要向另一個參數值查詢即可得到,這就可以使用以查詢代替參數;如果在處理的函數具有引用透明性,即在任何時候只要傳入相同的參數值,該函數的行為永遠一致,可以讓它訪問一個全局變量。
6. 以參數取代查詢
在瀏覽函數實現時,會經常發現一些糟糕的引用關系,比如引用一些全局變量或者另一個想要移除得元素,其實可以通過將其替換成函數參數來解決,將處理引用關系的責任推卸給函數調用者。其實此重構思想是:改變代碼的依賴關系,讓目標函數不再依賴某個元素,將元素的值以參數形式進行傳遞給函數。當然,如果把所有依賴關系都變成參數,會導致參數列表冗長重復,其次倘若作用域間的共享太多,會導致函數間過度依賴。
具體做法:將執行查詢操作的代碼進行變量提煉,將其從函數體中分離,對現有函數體代碼不再執行查詢操作,而是使用上一步提煉的變量,對此部分代碼使用函數提煉。使用內聯變量就是把提煉出來的變量放到一個函數中,且對原先的函數使用內聯函數。
- targetFun(plan)
- const otherFun = {...}
- function targetFun(plan){
- curPlan = otherFun.curPlan
- ...
- }
- //重構
- targetFun(plan)
- function targetFun(plan,curPlan){
- ...
- }
7. 移除設值函數
當為某個字段提供了設置函數,表示此字段被改變,如果不希望在對象創建之后字段被改變,就不要提供設值函數,同時聲明此字段不可改變。但是呢,有些開發者喜歡通過訪問函數來讀取字段值,在構造函數內也是,這就會導致構造函數成為設值函數的唯一使用者,就這樣你還不如直接移除設值函數呢,沒有意義。
當然,對象有可能是由客戶端通過腳本(通過調用構造函數,即一系列的設置函數)進行構造出來的,而不是只有一次簡單的構造函數調用。在執行完創建腳本后,此新生對象的部分字段不應該再被修改,設值函數只能被允許在起初對象創建過程中被調用。其實此時也應該移除設值函數,能夠更加清晰的表達意圖。
- class User{
- get(){...}
- set(){...}
- }
- //重構
- class User{
- get(){...}
- }
8. 以工廠函數取代構造函數
很多面向對象語言都有構造函數用于對象的初始化,通常客戶端會通過調用構造函數來新建對象。但對于普通函數而言,構造函數具有一定的局限性,通常只能返回當前所調用類的實例,就是無法根據環境或參數信息返回子類實例或代理對象。且構造函數名字是固定的,因此無法使用比默認名字更清晰的函數名,此外還需要通過特殊的操作符(關鍵字new)來創建實例調用。然而,工廠函數就不受限制,可以實現內部調用構造函數,也可以使用其他方式調用。
9. 以命令取代函數
函數可以是作為獨立函數,也可以作為類對象中的方法,還是作為程序設計的基本構造模塊。將函數封裝成自己的對象成為命令對象,當然這種對象大多只服務于單一函數,獲得該函數的請求并進行執行函數,就是這種對象存在的意義。
與普通函數相比,命令對象提供了更加強大的控制靈活性和更強的表達能力,除了函數調用本身,命令對象還可以作為支持附加的操作,比如撤銷。可以通過命令對象提供的方法進行設置和取值操作,從而提升豐富的生命周期管理能力。
具體方法:為想要包裝的函數創建一個空類,并根據該函數的名字命名類,將函數搬移到空類中,并對每個參數創建一個字段,在構造函數中添加對應的參數。
舉個栗子
- //原始代碼
- function user(name,work,address){
- let result = "";
- let addressLevel ="";
- ...long code
- }
- //重構代碼
- class User{
- constructor(name,work,address){
- this._name = name;
- this._work = work;
- this._addrsss = address;
- }
- clac(){
- this._result="";
- this._addressLevel ="";
- ...long code
- }
- }
10. 以函數取代命令
命令對象為處理復雜計算提供了強大的機制,可以輕松將原本復雜的函數拆分成多個方法,彼此之間通過字段進行狀態共享。拆分后的方法可以分別進行調用,開始調用之前的數據狀態也可以逐步構建,但是這種強大功能是有代價的。通常我們調用函數讓其完成自身的任務,當此函數不是很復雜時,命令對象顯得得不償失,還不如使用普通函數呢。
通常的,將創建并執行命令對象的代碼單獨提煉到獨立函數中,對命令對象在執行階段用到的函數逐一使用內聯函數。使用改變函數聲明,將構造函數的參數轉移到執行函數。對于所有的字段在執行函數中找到引用它的地方,并將其改為使用參數,將調用構造函數和調用執行函數兩步進行內聯到調用函數中。
舉個栗子
- //原始代碼
- class ChargeClass{
- constructor(custom,param){
- this._custom = custom;
- this._param = param;
- }
- clac(){
- return this._custom.rate * this._param
- }
- }
- //重構代碼
- function charge(custom,param){
- return custom.rate * param;
- }