簡體   English   中英

將許多 arguments 傳遞給方法的最佳實踐?

[英]Best practice for passing many arguments to method?

有時,我們必須編寫接收許多 arguments 的方法,例如:

public void doSomething(Object objA , Object objectB ,Date date1 ,Date date2 ,String str1 ,String str2 )
{
}

遇到這種問題,我經常把arguments封裝成一個map。

Map<Object,Object> params = new HashMap<Object,Object>();
params.put("objA",ObjA) ;

......

public void doSomething(Map<Object,Object> params)
{
 // extracting params 
 Object objA = (Object)params.get("objA");
 ......
 }

這不是一個好的做法,將 params 封裝成 map 完全是浪費效率。 好處是,干凈的簽名,只需最少的修改即可輕松添加其他參數。 此類問題的最佳做法是什么?

Effective Java ,第 7 章(方法),第 40 項(仔細設計方法簽名)中,Bloch 寫道:

有三種縮短過長參數列表的技術:

  • 將方法分解為多個方法,每個方法只需要參數的一個子集
  • 創建輔助類來保存一組參數(通常是靜態成員類)
  • Builder 模式從對象構造調整到方法調用。

更多細節,我鼓勵你買這本書,真的很值得。

使用帶有魔法字符串鍵的地圖是一個壞主意。 您失去了任何編譯時檢查,而且還不清楚所需的參數是什么。 您需要編寫非常完整的文檔來彌補它。 幾周后,您會在不看代碼的情況下記住這些字符串是什么嗎? 如果你打錯了怎么辦? 使用錯誤的類型? 直到你運行代碼你才會發現。

而是使用模型。 創建一個類,作為所有這些參數的容器。 這樣你就可以保持 Java 的類型安全。 您還可以將該對象傳遞給其他方法,將其放入集合等。

當然,如果這組參數沒有在其他地方使用或傳遞,則專用模型可能會過大。 有一個平衡需要達到,所以使用常識。

如果您有許多可選參數,您可以創建流暢的 API:用方法鏈替換單個方法

exportWithParams().datesBetween(date1,date2)
                  .format("xml")
                  .columns("id","name","phone")
                  .table("angry_robots")
                  .invoke();

使用靜態導入,您可以創建內部流暢的 API:

... .datesBetween(from(date1).to(date2)) ...

它被稱為“引入參數對象”。 如果您發現自己在多個地方傳遞了相同的參數列表,只需創建一個包含它們的類即可。

XXXParameter param = new XXXParameter(objA, objB, date1, date2, str1, str2);
// ...
doSomething(param);

即使您沒有發現自己經常傳遞相同的參數列表,這種簡單的重構仍然會提高您的代碼可讀性,這總是好的。 如果您在 3 個月后查看您的代碼,就會更容易理解何時需要修復錯誤或添加功能。

當然,這是一個普遍的哲學,因為你沒有提供任何細節,我也不能給你更詳細的建議。 :-)

首先,我會嘗試重構該方法。 如果它使用那么多參數,它可能太長了。 分解它既可以改進代碼,又可以減少每個方法的參數數量。 您也可以將整個操作重構為它自己的類。 其次,我會尋找使用相同參數列表的相同(或超集)的其他實例。 如果您有多個實例,則可能表明這些屬性屬於一起。 在這種情況下,創建一個類來保存參數並使用它。 最后,我會評估參數的數量是否值得創建地圖對象以提高代碼可讀性。 我認為這是個人的決定——這個解決方案的每一種方式都有痛苦,權衡點可能會有所不同。 對於六個參數,我可能不會這樣做。 對於 10,我可能會(如果其他方法都不起作用)。

這在構造對象時通常是一個問題。

在這種情況下,使用builder object pattern ,如果您有大量參數列表並且並不總是需要所有參數,它會很好地工作。

您還可以將其調整為方法調用。

它還大大增加了可讀性。

public class BigObject
{
  // public getters
  // private setters

  public static class Buider
  {
     private A f1;
     private B f2;
     private C f3;
     private D f4;
     private E f5;

     public Buider setField1(A f1) { this.f1 = f1; return this; }
     public Buider setField2(B f2) { this.f2 = f2; return this; }
     public Buider setField3(C f3) { this.f3 = f3; return this; }
     public Buider setField4(D f4) { this.f4 = f4; return this; }
     public Buider setField5(E f5) { this.f5 = f5; return this; }

    public BigObject build()
    {
      BigObject result = new BigObject();
      result.setField1(f1);
      result.setField2(f2);
      result.setField3(f3);
      result.setField4(f4);
      result.setField5(f5);
      return result;
    }
  }
}

// Usage:
BigObject boo = new BigObject.Builder()
  .setField1(/* whatever */)
  .setField2(/* whatever */)
  .setField3(/* whatever */)
  .setField4(/* whatever */)
  .setField5(/* whatever */)
  .build();

您還可以將驗證邏輯放入 Builder set..() 和 build() 方法中。

有一種稱為Parameter object的模式。

想法是使用一個對象代替所有參數。 現在即使以后需要添加參數,也只需要將其添加到對象中即可。 方法接口保持不變。

您可以創建一個類來保存該數據。 雖然需要足夠有意義,但比使用地圖(OMG)要好得多。

Code Complete* 提出了幾點建議:

  • “將例程參數的數量限制為大約 7。7 是人們理解的神奇數字”(第 108 頁)。
  • “按輸入-修改-輸出順序放置參數......如果多個例程使用相似的參數,請將相似的參數放置在一致的順序中”(第 105 頁)。
  • 將狀態或錯誤變量放在最后。
  • 正如tvanfosson 所提到的,只傳遞例程需要的結構化變量(對象)的部分。 也就是說,如果您在函數中使用大部分結構化變量,則只需傳遞整個結構,但請注意,這會在一定程度上促進耦合。

* 第一版,我知道我應該更新。 此外,自 OOP 開始變得越來越流行時編寫第二版以來,這些建議中的一些可能已經改變。

使用 Map 是一種清理調用簽名的簡單方法,但是您還有另一個問題。 您需要查看方法的主體內部以查看該方法在該 Map 中期望什么,鍵名稱是什么或值具有什么類型。

一種更簡潔的方法是將對象 bean 中的所有參數分組,但這仍然不能完全解決問題。

你在這里遇到的是一個設計問題。 如果一個方法有超過 7 個參數,您將開始難以記住它們代表什么以及它們的順序。 從這里開始,僅通過以錯誤的參數順序調用方法,您就會得到很多錯誤。

您需要更好的應用程序設計,而不是發送大量參數的最佳實踐。

好的做法是重構。 這些對象意味着它們應該傳遞給這個方法嗎? 是否應該將它們封裝到單個對象中?

創建一個 bean 類,並設置所有參數(setter 方法)並將此 bean 對象傳遞給該方法。

  • 查看您的代碼,看看為什么要傳入所有這些參數。有時可以重構方法本身。

  • 使用地圖會使您的方法容易受到攻擊。 如果有人在使用您的方法時拼錯了參數名稱,或者在您的方法需要 UDT 的地方發布了一個字符串怎么辦?

  • 定義一個傳輸對象 它至少會為您提供類型檢查; 您甚至可以在使用點而不是在您的方法內執行一些驗證。

我會說堅持你以前做的方式。 您示例中的參數數量並不多,但替代方案要可怕得多。

  1. 地圖 - 你提到的效率問題,但這里更大的問題是:

    • 來電者不知道要向您發送什么內容而不提及某些內容
      否則......你有javadocs,它准確地說明了哪些鍵和
      值被使用? 如果你這樣做(這很好),那么擁有大量參數也不是問題。
    • 接受不同的參數類型變得非常困難。 您可以將輸入參數限制為單一類型,也可以使用 Map<String, Object> 並強制轉換所有值。 大多數情況下,這兩種選擇都很糟糕。
  2. 包裝對象 - 這只是解決了問題,因為您首先需要填充包裝對象 - 而不是直接填充到您的方法,而是填充到參數對象的構造函數。 確定移動問題是否合適取決於該對象的重用。 例如:

不會使用它:它只會在第一次調用時使用一次,所以很多額外的代碼來處理 1 行......?

{
    AnObject h = obj.callMyMethod(a, b, c, d, e, f, g);
    SomeObject i = obj2.callAnotherMethod(a, b, c, h);
    FinalResult j = obj3.callAFinalMethod(c, e, f, h, i);
}

可以使用它:在這里,它可以做得更多。 首先,它可以分解 3 個方法調用的參數。 它本身也可以執行另外兩行……所以它在某種意義上變成了一個狀態變量……

{
    AnObject h = obj.callMyMethod(a, b, c, d, e, f, g);
    e = h.resultOfSomeTransformation();
    SomeObject i = obj2.callAnotherMethod(a, b, c, d, e, f, g);
    f = i.somethingElse();
    FinalResult j = obj3.callAFinalMethod(a, b, c, d, e, f, g, h, i);
}
  1. 建造者模式——在我看來這是一種反模式。 最理想的錯誤處理機制是早點檢測,而不是晚點; 但是在構建器模式中,缺少(程序員不認為包含它)強制參數的調用從編譯時移動到運行時。 當然,如果程序員故意在插槽中放置 null 或諸如此類,那將是運行時,但是仍然較早地捕獲一些錯誤對於迎合拒絕查看他們正在調用的方法的參數名稱的程序員來說是一個更大的優勢。 我發現它只適用於處理大量可選參數時,即使這樣,好處充其量也是微不足道的。 我非常反對建造者的“模式”。

人們忘記考慮的另一件事是 IDE 在這一切中的作用。 當方法有參數時,IDE 會為您生成大部分代碼,您會看到紅線提醒您需要提供/設置什么。 使用選項 3 時……您完全失去了它。 現在由程序員來做正確的事情,並且在編碼和編譯期間沒有任何提示……程序員必須對其進行測試以找出答案。

此外,如果選項 2 和 3 被不必要地廣泛采用,由於它生成的大量重復代碼,在維護方面會產生長期的負面影響。 代碼越多,需要維護的越多,維護它所花費的時間和金錢就越多。

這通常表明您的班級承擔了多個責任(即,您的班級做得太多)。

參見單一職責原則

了解更多詳情。

如果您傳遞的參數過多,請嘗試重構該方法。 也許它正在做很多它不應該做的事情。 如果不是這種情況,請嘗試用單個類替換參數。 通過這種方式,您可以將所有內容封裝在單個類實例中並傳遞實例而不是參數。

... 鮑勃是你的叔叔:用於 object 創建的無障礙花哨 API!

https://projectlombok.org/features/Builder

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM