在函數中需要用到大量參數時如何傳參可以更簡潔合理?

在需要大量傳參,調用子函數又要不停使用參數時,有沒有什麼好方法可以讓函數更簡潔?可不可以先把參數寫入文件,使用時讀出?

int myfun1 ( var1, var2, ...,var20){
int myfun2(var2, var3, ...,var10){

}
int myfun3(var1, var2,..., var15){

}
}


如果需要一級一級傳下去的話,那最好把不會變的參數打包成一個struct,然後你就會發現這樣做的意義


是時候祭出這本我珍藏多年的武林秘籍了:

讓我們來看看它講了些什麼:

第一式:刪除

這話聽起來不可思議:函數體中已經用到的形參,還怎麼刪除啊?

但既然書里已然這麼寫了,編也要編兩條方法出來。

強編方法1.從其他途徑獲得形參

如有一函數

public myFunction(int var1, int var2, int var3) {
...
}

  1. 仔細分析參數的獲取方式、參數間的關係;
  2. 其中某些形參能由別的辦法獲取,如 f(var1, var3) =&> var2;
  3. 且 f 的推導代價(從運行時間、從代碼邏輯兩方面)不大;

則可以考慮在函數體內獲取參數var2。重寫後的代碼為:

public myFunction(int var1, int var3) {
int var2 = getVar2(var1, var3);
...
}

private int getVar2(int var1, int var3) {
//f(var1, var3) =&> var2的代碼邏輯
...
}

強編方法2.拆分函數

一個函數擁有大量的參數,有理由懷疑它具有如下特徵:

  • 函數做了很多事情;
  • 各個參數被分散的用到了函數體的各個角落;

這兩點特徵換一種表達方式:

在函數體中的函數的形參,有一些用在一起,為這個函數實現一些功能。而另外一些聚在一起,為該函數實現別的一些功能。

我們可以從2個維度來分析這種函數:

  • 為函數取一個名字,描述函數的功能。是否可以得到形如getXXAndSetXX這種函數名稱?
  • 在函數體中,一些參數出現的位置,是否比另一些參數顯得更為靠近?

這種函數的結構大約為這樣:

public myFunction(int var1, int var2, int var3, int var4) {
//下面一段邏輯,使用var1,var2獲取x
...

// 下面一段邏輯,使用上面獲得的x, 加上參數var3, var4為y賦值
...
}

public static void main(String[] args) {
int arg1 = ...;
int arg2 = ...;
int arg3 = ...;
int arg4 = ...;
myFunction(arg1, arg2, arg3, arg4);
}

這種情況可以這麼來減少參數:

  1. 以形參的聚合緊密度為標準,把函數體中的代碼邏輯分段;

  2. 每一段代碼用寫成一個子函數;

  3. 調用原函數的地方,改為調用一個個新作的子函數;

這樣我們可以將函數改造為:

public mySubFunction1(int var1, int var2) {
//下面一段邏輯,使用var1,var2獲取xx
...
}

public mySubFunction2(int middleTmp, int var3, int var4) {
// 下面一段邏輯,使用middleTmp, var3, var4為xx賦值
...
}

public static void main(String[] args) {
int arg1 = ...;
int arg2 = ...;
int arg3 = ...;
int arg4 = ...;
int temp = mySubFunction1(arg1, arg2);
mySubFunction2(temp, arg3, arg4);
}

↓相較於android複雜的系統控制按鈕,iPhone上只一個小圓點。

簡化參數最明顯的方式

就是刪除不必要的參數。

第二式:組織

將參數組織起來,並不能從本質上減少參數的個數。但它能使函數的結構簡明,並更容易尋找到優化和重構的空間。

常見的組織參數的方式,就是將相關聯的參數寫成一個java bean,或是c struct。

例如一個描繪點運動軌跡的函數:

public void drawOrbit(int x, int y, int time, int velocity){
...
}

很明顯x和y成雙成對出現的場合比較多。將它們組織起來,無論是業務層面,還是代碼層次上都是不錯的簡化。

通過組織的方式,將代碼簡化為:

public void drawOrbit(Coordinate coordinate, int time, int velocity) {
...
}
class Coordinate() {
int x;
int y;
Coordinate() {
this.x = x;
this.y = y;
}
int getX() {
return x;
}
int getY() {
return y;
}
}

把坐標參數組織成類後,我們還可以遍歷與坐標相關的代碼邏輯,基於下述兩點做進一步的代碼重構:

  • 取x、y的屬性(或屬性的變形)值的代碼邏輯 -&> 調用Coordinate的相關方法;

  • 與x、y有關的重複邏輯多次出現 - &> 提取重複代碼成函數,將函數作為Coordinate類的一個公用方法

↓將相似功能的設置項組織到一起,用section分割,使得設置選項變得多而不亂,易於用戶查找。

組織往往是簡化參數

的最快捷方式。

第三式: 隱藏

像題主舉例,在不同的函數中,相同的變數作為形參,是一種重複和繁瑣的方式。

這種情況下,若var1, var2, ...,var20的形參獲取邏輯較複雜,可以考慮使用Hash表來存儲形參。

/*假設:f(x1) -&> var1;f(x2) -&> var2;...f(x20) -&> var20;
則我們在函數所在類中定義一個map成員變數,利用key-value的方式獲取上述變數。*/
public class Foo{
//定義一個變數map
private final Map& paramMap = new HashMap&();
//在構造函數中初始化變數map
public Foo(int x1, int x2, ... int x20) {
//這裡是f(x1) -&> var1的代碼實現
Integer var1 = ...;
...
//這裡是f(x20) -&> var20的代碼實現
Integer var20 = ...;
paramMap.put("var1", var1);
...
paramMap.put("var20", var20);
}

int myfun1 (){
int tempVar1 = paramMap.get("var1");
...
int tempVar20 = paramMap.get("var20");
}

...
}

有很多種方式,可以讓函數體代碼獲得變數,傳遞形參是一種顯式的方式。其他隱藏方式有:全局變數、成員變數、配置文件、資料庫、redis等緩存技術、session變數等。

隱藏不是一勞永逸的辦法,在打算這麼干之前,必須好好的評估下面2個負面影響:

  • 這麼做可能會將變數暴露給不需要的類方法;

  • 這麼做會讓變數變得不可控。如果某些地方意外修改了變數,真正調用時就會使用不正確的"形參值";

↓在wuli鍵盤還流行的年代,google g1以隱藏式鍵盤的設計,兼顧了傳統與美觀,功能與簡捷。

隱藏部分參數

是一種低成本的方案。

第四式:轉移

所謂轉移,是指將傳遞參數的重任,轉移到對形參更熟悉的函數、與形參更親密的類來完成。從而在上層調用時,達到減少參數的目的。

1.下層函數才暴露參數

例如一個對象有多種創建方式,每種創建方式用到的參數各不相同。則可定義一個基礎構造函數,用所有參數的全集,作為該構造函數的形參。其餘的構造函數,調用該基礎構造函數

public class Foo {
...
//基礎構造函數
public Foo(Integer var1, Integer var2, Integer var3, Integer var4) {
...
}

//其餘構造函數,調用該基礎構造函數,形成構造函數鏈
public Foo(Integer var1, Integer var3, Integer var4) {
this(var1, null, var3, var4);
...
}
public Foo(Integer var1, Integer var2, Integer var4) {
this(var1, var2, null, var4);
...
}
}

再如有一些函數,需要傳入一個標誌位,根據標誌位來做不同的業務處理。

例如:

public Response createResponse(HttpServeletRequest request, boolean isSuccessful) {
if(isSuccessful) {
//創建一個代表成功的Response
} else {
//創建一個代表失敗的Response
}
}

這種情況下,將成功和失敗分別寫成一個函數,既減少了傳入參數,又使得函數所做的事更單一、純粹。

public Response createSuccessResponse(HttpServeletRequest request) {
//創建一個代表成功的Response
}
public Response createFailureResponse(HttpServeletRequest request) {
//創建一個代表失敗的Response
}

2.類來傳遞函數

假設A類里有一個方法myFunctionInA,它用到了不少的形參,其中大部分都來自另一個類B。那麼我們調用的方式很可能是這樣:

public class A {
public void myFunctionInA(int var1InB, int var2InB, int var3InB, int otherVar) {
...
}
public static void main(String[] args) {
...
A a = new A();
B b = new B();
a.myFunctionInA(b.getVar1InB(), b.getVar2InB(), b.getVar3InB(), otherVar);
}
}
public class B {
private int var1InB;
private int var2InB;
private int var3InB;
public int getVar1InB() {
...
}
public int getVar2InB() {
...
}
public int getVar3InB() {
...
}
}

我們將對myFunctionInA的調用,轉移到類B中:

public class A {
public void myFunctionInA(int var1InB, int var2InB, int var3InB, int otherVar) {
...
}

public static void main(String[] args) {
...
A a = new A();
B b = new B();
//下面的函數調用參數減少了!
b.myFunctionInA(a, otherVar);
}
}

public class B {
private int var1InB;
private int var2InB;
private int var3InB;
public int getVar1InB() {
...
}
public int getVar2InB() {
...
}
public int getVar3InB() {
...
}
//下面增加了一個對myFunctionInA的調用!
public void callMyFunctionInAFromB(A a, int otherVar) {
a.myFunctionInA(this.getVar1InB(), this.getVar2InB(), this.getVar3InB(), otherVar);
}
}

上面將對A類的方法調用 -&> 轉換成了對B類方法的調用。

∵B實例化時已經在成員變數中保存了要用到的大部分變數值,

∴我們在調用B方法時,只需傳入少部分變數 + A的實例化對象。

使用this是我最喜歡的重構方式,它的重構步驟簡單,能立竿見影的減少參數。

↓nano幾乎拋棄了播放器的所有物理按鈕。結合觸摸、滑動等事件,將功能轉移到屏幕。使設備更簡潔,操作也更加自然與靈活。

為什麼不把一些參數精簡掉,

讓函數調用者取而代之呢?

======================我是打總結的分割線======================

函數的參數過多,往往伴隨著函數體的龐大,調用者在調用時時常忘記各個參數的意義。這些橫亘在程序員面前的不可把控,便是我們重構函數的初心和號令。

在消除過多參數的過程中,秘笈只是一個方法論,個中方法也並非完全割裂。

事實上,刪除參數 -&> 提取成員變數 -&> 提取類 -&> 封裝類的方法。

這種由小至大的重構,是一步一步順其自然、水到渠成的。


說明你這個函數功能太多,得重寫拆成幾個函數互相調用,每個函數功能盡量少。


傳遞一個Provider。

如果格式再複雜,就傳遞一個object,並把各類參數封裝到不同的IXxxxProvider中。按需構造這個object對應的class(當然也可以實現所有介面)。

Provider內部可以用靜態值(對於小開銷數據),或者使用晚期綁定設計。


哈哈,看到了輪子哥,我也要來答一個。大量參數傳來傳去本來就是一個不好的辦法,如果您已經遇到這個情況,說明你的程序執行是一個複雜的有向無環圖(DAG, Directed Acyclic Graph),可以通過模塊化主要成分,寫一個流配置文件,告訴程序張三李四該何去何從,構建DAG solver的方法來解決。

這裡推銷一下我自己寫的Clojure DAG solver,有了它以後,您可以專註於寫核心內容,具體怎麼把模塊們穿插在一起,就由這個包為您解決吧

GitHub - hesenp/dag-runner: Execute functions specified by a DAG (directed acyclic graph)


封裝成對象


Java中有一種模式是把操作封裝成了類,不僅是參數,連同動作


用 對象


如果用數組之類的,記得別用 double alp= listVar[1];

用 double alp = listVar[ITEM_ALP];


struct / list / vector


推薦閱讀:

在寫代碼時,你們對變數的命名都是按照什麼規則,如何使變數名變成別人一看就懂的?
當開發同事辭職,接手到垃圾代碼怎麼辦?
工程師應該如何保證代碼質量?
你們的開發團隊有引入findbugs等代碼檢測工具嗎?老代碼改不改嗎?

TAG:編程 | 代碼質量 | CC |