聊聊那些年遇到過的奇葩程式碼

張哥說技術發表於2022-12-22

引言

無論是開發新需求還是維護舊平臺,在工作的過程中我們都會接觸到各種樣式的程式碼,有時候會碰到一些優秀的程式碼心中不免肅然起敬,但是更多的時候我們會遇到很多奇葩程式碼,有的時候罵罵咧咧的吐槽一段奇葩程式碼後定睛一看作者,居然是幾個月以前自己的寫的,心中難免浮現曹操的那句名言:不可能,絕對不可能。很多同學可能會說要求別太高了,程式碼能跑就行。但是實際程式碼就是程式猿的名片,技術同學不能侷限於實現功能需求,還是得有寫高質量程式碼的追求。那麼今天就和大家聊聊那些年遇到過的奇葩程式碼,看看自己以前有沒有寫過這樣的程式碼,現在還會不會這樣寫了。

奇葩程式碼大賞

命名沒有業務語義


public void handleTask(Long taskId, Intger status) {
    TaskModel taskModel = taskDomainService.getTaskById(taskId);
    Assert.notNull(taskModel);
    taskModel.setStatus(status);
    taskDomainService.preserveTask(taskModel);
}


可能乍一看這段程式碼其實沒啥大問題,但是如果要知道這段程式碼到底是幹嘛的可能你一下子反應不過來,需要好好看看程式碼邏輯才知道。透過檢視程式碼我們知道此處的程式碼業務語義是變更任務狀態,但是實際的方法名稱是handleTask,命名明顯過於寬泛了,不能精確表達實際的業務語義。

那麼為什麼要把程式碼擼一遍才能明確方法的含義呢?歸根到底就是方法命名不夠準確,不能完全表達這段程式碼所對應的業務語義。那為什麼我們經常不能很準確的進行類或者方法的命名呢?我想最根本的原因還是碼程式碼的同學沒能夠精準把握這段程式碼的業務語義,因此在起名字的時候要麼過於寬泛,要麼詞不達意。

因此無論是類命名或者方法命名都要能夠明確的表達業務語義,只有這樣無論是一段時間自己回過頭來看或者其他維護者來看程式碼都能夠透過看命名就可以明確程式碼蘊含的業務邏輯。

單個方法過長

特別是在一些老專案中,我們經常會遇到一個方法裡面能塞進去幾百行程式碼。一般造成這種單個方法程式碼過長的原因無非有兩個,一個是用過程化的思維編寫程式碼,想到哪些業務步驟都統統寫在一個方法中;另一個就是後來的維護者需要增加新的功能,一看程式碼這麼長也不敢改只能在長方法中繼續碼程式碼,造成方法原來越長難以維護。

無論是從後期程式碼可維護性還是從SRP設計原則來說,單個方法中程式碼行數最好不要超過100行,否則帶來的後果就是各種業務邏輯糅合在一起,不僅後期維護程式碼的同學不容易理解其中包含的業務語義,而且如果功能變化修改起來也比較費勁。


public void shelveFreshGoods() {
    //檢查貨品
    //幾十行程式碼(檢查重量、檢查新鮮度等等)
    
    //貨品擺渡
    //幾十行程式碼(生成貨品編號、裝載等等)
    
    //上架
    //幾十行程式碼(貨品打標、繫結庫存等等)
    ...
    
}


如上架鮮品的邏輯,可以看的出來在上架生鮮產品的時候會經歷貨品檢查、貨品擺渡、貨品上架等多個個步驟,但是在這個shelveFreshGoods方法中將這些業務步驟走雜糅在了一起,如果我們想修改或者增加業務邏輯的時候就需要在這個方法中只能在這個長方法中進行修改,可能會導致方法越來越長。而如果透過拆分的方式進行業務子過程劃分,也就是說將上述的幾個步驟都封裝成方法。那麼修改某業務邏輯可直接在對應拆出來的步驟中進行,這樣修改的範圍就縮小了,另外業務邏輯看上去一目瞭然。


public void shelveFreshGoods() {
 //檢查貨品
  check();
  //貨品擺渡
  transfer();
 //上架
  shelve();  
}


業務資料迴圈插入

在進行業務程式碼開發的時候,批次進行業務資料插入是非常常見的CRUD基但是有的同學在寫批次插入介面的時候會這麼寫,透過for迴圈或者stream來進行迴圈資料寫入。這樣的寫法會白增加服務與資料庫的互動次數,佔用不必要的資料庫連線,很容易遇到效能問題。如果一次性插入的資料不多的話(幾條資料)倒也影響不大,但是如果資料量起來的話必定會成為效能瓶頸。


for(TaskPO taskPO : taskPOList) {
    saveTask(taskPO);
}


很明顯可以看得出來,原先的寫法需要與資料庫進行多次互動。而最佳化後的寫法只需要和資料庫互動一次。實際上我們可以在mapper檔案中進行批次插入進行最佳化,這樣實際上透過批次插入的sql語句,從而實現服務與資料庫只互動一次就完成資料的批次儲存。


<insert id="batchSaveTask" parameterType="java.util.List">  
  insert into task   
  (c_id,c_name,c_type,c_content,c_operator,i_container_type,c_warehouse_type) 
  values   
  <foreach collection="taskList"  item="item" open="(" close=")" separator=",">  
    (#{item.id},#{item.type},#{item.content},#{item.operator},#{item.containerType},#{item.warehouseType}
  </foreach>  
</insert>  


先查資料再更新資料庫

在進行業務程式碼編寫的時候,經常會碰到這樣的場景,如果資料庫中有資料則進行更新,如果沒有資料則直接插入。我們來看看下面這種寫法,先從資料庫中查詢資料,如果存在則進行更新,如果不存在進行資料插入,有兩次資料庫互動操作。


Task task = taskBizService.queryTaskByName();
if(Objects.isNull(task)) {
  taskBizService.saveTask(); //省略引數
}
taskBizService.updateTask(); //省略引數


實際上可以直接透過資料庫的sql進行控制,存在資料則進行更新,不存在則插入,這樣可以避免和資料庫的多次互動。


insert into task(c_id,c_name,c_type,c_content,c_operator,i_container_type,c_warehouse_type) values (#{name},#{type},#{content},#{operator},#{containerType},#{warehouseType}) on conflicct(c_name) do update set c_content=#{content}


業務依賴技術細節

我們先來看下Robert C. Martin提出來的依賴倒置原則怎麼描述的:

High-level modules should not depend on low-level modules. Both should depend on abstractions.

高層模組不應該依賴於低層模組,二者都應該依賴於抽象。

Robert C. Martin

Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

抽象不應該依賴於細節,細節應該依賴於抽象。

Robert C. Martin

這兩句話聽上去有點不明覺厲,不如我們結合下具體的業務場景更好理解一點。假設在一個監控告警平臺中,如果線上平臺出現了問題,比如呼叫訂單生成介面失敗,無法生成訂單。平臺檢測到這樣的異常之後需要通知研發同學進行問題排查定位。此時監控告警平臺會將告警資訊傳送到釘釘群中進行通知。因此我們需要一個傳送釘釘訊息的介面,如下所示。


public boolean sendDingTalk(Message message) {
        ...
    }


看上去程式碼是沒什麼問題的,有了告警就呼叫傳送釘釘訊息的介面方法。但是實際上這樣的寫法違反了依賴倒置的設計原則。為什麼這麼說,試想一下如果哪天公司決定不用釘釘接收告警資訊,改用企業微信了或者是自己公司的通訊軟體。那麼此處的sendDingTalk必定是要進行修改的,因為我們的告警通知業務依賴具體的傳送訊息通知的實現細節,明顯是不合理的。

因此此處比較好的做法是,定義一個notifyMessage的介面,具體的實現細節上層不必關心,無論是透過釘釘通知還是企業微信通知也好,只要實現這個通知的介面就OK了。即便後期進行切換,原來的業務邏輯並不需要進行修改,只要修改具體通知介面的實現就可以了。


public interface NotifyMessage {
    boolean notifyMessage(Message message);

}

public class DingTalk implements NotifyMessage {
 
  @Override
  public boolean notifyMessage(Message message){
       ...
    }
}

public class WeChat implements NotifyMessage {
  
   @Override
   public boolean notifyMessage(Message message){
       ...
    }
}


長SQL

程式猿接手專案的時候,最怕遇到的就是專案中那些動不動上百行的長SQL。這些長SQL中有的存在各種巢狀查詢,甚至包含了三四層子查詢;有的包含了四五個left join,inner join連線了五六張表,這些長SQL一個電腦螢幕都裝不下,彷彿裝不下的還有寫這個長SQL的同學的“才華”。更無語的是如果寫這個SQL的同學已經離職了,你想問下大致的查詢邏輯都沒人可以問,即便是沒有離職,寫的人過了一段時間後再看這段SQL估計也挺費勁。

可能有的同學會說我也不想寫長SQL啊,奈何資料分散在各個表中,業務邏輯也比較複雜,所以只能各種join各種子查詢,不知不覺就寫了長SQL。但是實際上長SQL並不能解決上述資料分散業務複雜的問題,反而帶來了後期維護差等各種問題,長SQL表面上看是一個資料庫操作,但是在資料庫引擎層面還是將長SQL分成了多個子操作,各個子操作完成後再將結果資料進行統一返回。

那麼如何避免寫出來這種維護性很差的長SQL呢?對於一些查詢場景比較多的長SQL可以嘗試使用大寬表來承載需要展示的各個欄位資料,這樣頁面查詢的時候直接在大寬表上進行查詢,而不必再組合各個業務資料進行查詢,或者將又有的長SQL拆分成多個檢視以及儲存過程來簡化SQL的複雜性。

介面引數過多

這個問題在實際專案開發中經常遇到,當你需要調一個別人封裝好的介面的時候,對方突然丟過來一個方法包含了七八個引數。我想當時你的心情應該是想對他深深說一句真是栓Q你了。其實對於一個方法的引數來說,這裡建議引數個數還是最多不要超過5個。


Integer preserveTask(String taskId, 
             String taskName, 
             String taskType, 
             String taskContent,
             String operator,
             Integer containerType,
             String warehouseType)
;


實際上我們可以用模型物件來進行引數封裝,這樣可以避免方法中引數個數過多導致後期維護困難。因為隨著業務的發展,有可能會出現修改介面能力來滿足新的需求,但是這個時候如果動介面引數的話,那麼對應的介面以及實現類都需要修改,萬一有其他地方呼叫這個介面,那麼修改的地方就會更多,很明顯這不符合OCP設計原則。因此這個時候如果使用的是一個物件作為方法的引數,那麼無論是增加或者減少引數都只需要修改引數物件,並不需要修改對應方法的介面引數,這樣介面的擴充套件性會更加強一點。因此我們在寫程式碼的時候不能光著眼於當下,還要考慮對應需求發生變化的時候,我的程式碼怎麼才能適應這種變化做到最小化修改,後期無論是自己維護還是別人的同學維護都會更加方便一點。


Integer preserveTask(TaskDO taskDO);


重複程式碼

之前專門寫過關於如何消除系統重複的程式碼的文章,具體可以參見如下:

如何優雅的消除系統重複程式碼

常見程式碼最佳化寫法

儘量複用工具函式


集合判斷

日常開發的時候我們經常遇到關於資料集合非空判斷的邏輯,常見的寫法如下,雖然沒什麼問題但是看起來非常不順溜,簡單來說就是不夠直接,一眼望過去還得反應一下。


if(null != taskList && !taskList.isEmpty()) {
    //業務邏輯
}


但是透過使用封裝好的工具類直接進行判斷,所看即所得,清楚明白表達集合檢查邏輯。


if(CollectionUtils.isNotEmpty(taskList)) {
    //業務邏輯
}


Boolean轉換

在一些場景下我們需要將Boolean值轉化為1或者0,因此常見如下程式碼:


if(switcher) {
    return 1;
else {
 return 0;
}


實際上可以藉助於工具方法簡化為如下程式碼:


return BooleanUtils.toInteger(switcher);


lambda表示式簡化集合

集合最常見的場景就是進行資料過濾,篩選出符合條件的物件,程式碼如下:


List<Student> oldStudents = new ArrayList();
for(Student student: studentList) {
 if(student.getAge() > 18) {
        oldStudents.add(student);
    }
}


實際上我們可以利用lambda表示式進行程式碼簡化:


List<Student> oldStudents = studentList.stream()
                            .filter(item -> item.getAge() > 18)
                            .collect(Collectors.toList());


Optional減少if判斷

假設我們要獲取任務的名稱,如果沒有則返回unDefined,傳統的寫法可能是這樣,包含了多個if判斷,看上去有點囉裡囉唆不夠簡潔。


public String getTaskName(Task task){
        if (Objects.nonNull(task)){
            String name = task.getName();
            if (StringUtils.isEmpty(name)){
                return "unDefined";
            }
             return name;
        }
        return "unDefined";
    }


我們嘗試使用Optional進行程式碼簡化最佳化之後,是不是看上去立馬簡潔很多了?


public String getTaskName(Task task){
                return Optional.ofNullable(task).map(p->p.getName()).orElse("unDefined");
    }


總結

本文主要和大家聊了聊日常工作中比較常見的奇葩程式碼,當然吐槽並不是目的,研發同學能夠識別到奇葩程式碼並進行最佳化,同時自己在實際開發工程中能夠儘量避免寫這些程式碼才是真正的目的。不知道大家在工作中有沒有遇到過類似的奇葩程式碼或者自己曾經寫過哪些現在回過頭來看比較奇葩的程式碼,如果有的話歡迎大家在評論區一起討論交流哈 。



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2929069/,如需轉載,請註明出處,否則將追究法律責任。

相關文章