如何消除程式碼山中那一大坨參數列表?

摘要:有經驗的程式設計師應該都見過,一個方法坐擁幾十上百個參數。

本文分享自華為雲社區《如何消除程式碼屎山中的一大坨參數列表?》,作者: JavaEdge 。

有經驗的程式設計師應該都見過,一個方法坐擁幾十上百個參數。

1 方法為何要有參數?

因為不同方法之間需要共享資訊。

但方法間共享資訊的方式除了參數列表,還有全局變數。但全局變數總能帶來意外之喜,所以,取消全局變數也是各大語言趨勢。於是參數列表就成了唯一選擇,於是,只要你想到有什麼資訊要傳給一個方法,就會直接將其加入參數列表,導致參數列表越來越長!

2 長參數列表怎麼了?

參數列表一旦過長,你一個 crud boy就很難完全掌控這些邏輯了呀!所以癥結是數量多,解決關鍵也就是降低參數數量。

3 解決方案

3.1 聚沙成塔

一個簡單的創建部落格的方法:

public void createActicle(final String title, 
                          final String introduction,
                          final URL coverUrl,
                          final ActicleType type,
                          final ActicleColumn column,
                          final String protagonists,
                          final String tags,
                          final boolean completed) {
  ...
  Acticle acticle = Acticle.builder
    .title(title) 
    .introduction(introduction)
    .coverUrl(coverUrl)
    .type(type)
    .column(column)
    .protagonists(protagonists)
    .tags(tags)
    .completed(completed)
    .build();
 
  this.repository.save(acticle);
}

參數列表包含了一篇部落格所要擁有的各種資訊,比如:部落格標題、簡介、封面 URL、類型、歸屬專欄、主角姓名、標籤、是否完結…

若只是想理解邏輯,可能你還會覺得這參數列表挺好啊,把創建一篇部落格所需的資訊都傳給了方法,這也是大部分人面對一段程式碼時理解問題的最初角度。雖然這樣寫程式碼容易讓人理解,但不足以讓你發現問題。

現在產品要求在部落格里增加一項資訊,標識這部部落格是否收費,咋辦?很簡單啊!我直接新增一個參數。很多屎山就這麼來的,積少成多,量變引起質變!

這裡所有參數都是創建部落格所必需,所以,可以做的就是將這些參數封裝成一個類,一個創建部落格的參數類:

public class NewActicleParamters {
  private String title;
  private String introduction;
  private URL coverUrl;
  private ActicleType type;
  private ActicleColumn column;
  private String protagonists;
  private String tags;
  private boolean completed;
  ...
}

這樣參數列表就只剩下一個參數了:

public void createActicle(final NewActicleParamters parameters) {
  ...
}

所以, 將參數列表封裝成對象吧 !

只是把一個參數列表封裝成一個類,然後,用到這些參數的時候,還需要把它們一個個取出來,這會不會是多此一舉呢?就像這樣:

public void createActicle(final NewActicleParamters parameters) {
  ...
  Acticle acticle = Acticle.builder
    .title(parameters.getTitle()) 
    .introduction(parameters.getIntroduction())
    .coverUrl(parameters.getCoverUrl())
    .type(parameters.getType())
    .channel(parameters.getChannel())
    .protagonists(parameters.getProtagonists())
    .tags(parameters.getTags())
    .completed(parameters.isCompleted())
    .build();
 
  this.repository.save(acticle);
}

若你這樣想,說明還沒有形成對軟體設計的理解。我們並非只是簡單地把參數封裝成類,站在設計角度,這裡引入的是一個新模型。

一個模型的封裝應該以【行為】為基礎。

之前沒有這個模型,所以想不到它應該有什麼行為,現在模型產生了,它就該有自己配套的行為。該模型的行為就是構建一個部落格對象,則程式碼就能進一步重構:

public class NewActicleParamters {
  private String title;
  private String introduction;
  private URL coverUrl;
  private ActicleType type;
  private ActicleColumn column;
  private String protagonists;
  private String tags;
  private boolean completed;
 
  public Acticle newActicle() {
    return Acticle.builder
      .title(title) 
      .introduction(introduction)
      .coverUrl(coverUrl)
      .type(type)
      .column(column)
      .protagonists(protagonists)
      .tags(tags)
      .completed(completed)
      .build();
  }
}

創建部落格的方法就得到極大簡化:

public void createActicle(final NewActicleParamters parameters) {
  ...
  Acticle acticle = parameters.newActicle();
 
  this.repository.save(acticle);
}

如此,一旦後續需求又擴展了,需要增加創建部落格所需的內容,則該參數列表是不變的,也就是說它是穩定的!

3.2 動靜分離

若這個類不斷膨脹,變成一個大類,該咋辦?畢竟並非所有場景下,參數都屬於一個類:

// 根據部落格 ID 獲取對應章節資訊
public void getSections(final long acticleId, 
                        final HttpClient httpClient,
                        final SectionProcessor processor) {
  HttpUriRequest request = createChapterRequest(acticleId);
  HttpResponse response = httpClient.execute(request);
  List<Section> sections = toSections(response);
  processor.process(sections);
}

單論參數的個數,數量並不多。若你只看這個方法,很難發現直接問題。絕對數量不是core,參數列表也應該是越少越好。

但注意,每次傳進來的參數:

  • acticleId 都隨請求不同而改變
  • 但 httpClient、processor 兩個參數一樣,因為都有相同邏輯,沒啥變化。
    即acticleId 的變化頻率同 httpClient 和 processor 這兩個參數變化頻率不同。

不同的數據變動方向也是不同關注點。這裡表現出來的就是典型的動數據(acticleId)和靜數據(httpClient、processor),它們是不同關注點,應該分離。

針對該案例:

  • 靜態不變的數據完全可以成為這個方法所在類的一個欄位
  • 只將每次變動的東西作為參數傳遞即可

因此,程式碼可重構如下:

public void getSections(final long acticleId) {
  HttpUriRequest request = createChapterRequest(acticleId);
  HttpResponse response = this.httpClient.execute(request);
  List<Section> sections = toSections(response);
  this.processor.process(sections);
}

這個壞味道是個軟體設計問題,程式碼缺乏應有的結構,所以,原本應該屬於靜態結構的部分卻以動態參數的方式傳來傳去,無形中導致參數列表變長。

長參數列表固然可以用一個類進行封裝,但能夠封裝成這個類的前提是:這些參數屬於一個類,有相同的變化原因!

若方法的參數有不同變化頻率,就要看情況了。對靜態部分,本小節案例已經看出,它可以成為軟體結構的一部分,而若有多個變化頻率,則還可以封裝出多個參數類。

3.3 再見了,標記!

public void editChapter(final long chapterId, 
                        final String title, 
                        final String content, 
                        final boolean apporved) {
  ...
}

待修改章節的ID、標題和內容,最後一個參數表示這次修改是否直接審核通過。

前面幾個參數是修改一個章節的必要資訊,重點在最後這個參數。

從業務上說,如果是作者進行編輯,之後要經過審核,而如果編輯來編輯的,那審核就直接通過,因為編輯本身扮演了審核人的角色。所以,你發現了,這個參數實際上是一個標記,標誌著接下來的處理流程會有不同。

使用標記參數,是程式設計師初學編程時常用的一種手法。正是這種手法實在太好用,導致程式碼里flag肆意飄蕩。不僅變數里有標記,參數里也有。很多長參數列表其中就包含了各種標記參數。

在實際的程式碼中,必須小心翼翼地判斷各個標記當前的值,才能做好處理。

解決標記參數,一種簡單的方式就是,將標記參數代表的不同路徑拆分出來。

這裡的一個方法可以拆分成兩個方法,一個方法負責「普通的編輯」,另一個負責「可以直接審核通過的編輯」。

// 普通的編輯,需要審核
public void editChapter(final long chapterId, 
                        final String title, 
                        final String content) {
  ...
}
// 直接審核通過的編輯
public void editChapterWithApproval(final long chapterId,
                                    final String title,
                                    final String content) {
 ...
}

標記參數在程式碼中存在的形式很多,有的是布爾值、枚舉值、字元串或整數。都可以通過拆分方法的方式將它們拆開。在重構中,這種手法叫做移除標記參數(Remove Flag Argument)。

只有短小的程式碼,我們才能有更好地把握,而要寫出短小的程式碼,需要我們能夠「分離關注點」。

總結

應對長參數列表主要的方式就是減少參數的數量,最直接的就是將參數列表封裝成一個類。但並不是說所有的情況都能封裝成類來解決,我們還要分析是否所有的參數都有相同的變動頻率。

變化頻率相同,則封裝成一個類。
變化頻率不同的話:

  • 靜態不變的,可以成為軟體結構的一篇分
  • 多個變化頻率的,可以封裝成幾個類

此外,參數列表中經常會出現標記參數,這是參數列表變長的另一個重要原因。對於這種標記參數,一種解決方案就是根據這些標記參數,將方法拆分成多個方法。

 

點擊關注,第一時間了解華為雲新鮮技術~