iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 15
1
Software Development

可不可以不要寫糙 code系列 第 15

如何寫高品質 function (輸出+輸入篇)

良好程式碼的優點大同小異。
不好的程式碼的糙點卻各有巧妙之處。

-- 台南原地方法院,最棒的古蹟修復案例之一。基本的從外觀的牆面油漆去除,到馬薩式屋頂的木構造修復。建築最美的雙重圓頂與大廳裝飾,都原汁原味的重現在現在的古蹟內。不只是外觀修復得很美,連內部的構造都依原本的構想,聽說是以零件抽離一件修復一件的方式修復,以確保正確性。也是台南唯一開放貓道,可以參觀馬薩式屋頂木構造的古蹟,長達八年的修復已完成,現在是台南司法博物館。

在說了什麼多的「不要這麼做」之後,偶爾也來寫一下「那要怎麼做」比較好。
這次花一點篇幅來介紹 function 怎麼寫,比較好。(好像語法工匠)
在《忍者》[1]第一版中,直接就指出 JavaScript 的 function 有 4 種

  1. 一般的 function
  2. method: 物件中的 function
  3. constructor: 建構物件的 function
  4. recursive: 呼叫自己的 function

(ES6 之後,加上的 arrow function ,請看《忍者2》)

Pure function

這是一個好語法的心法。

  1. Its return value is the same for the same arguments (no variation with local static variables, non-local variables, mutable reference arguments or input streams from I/O devices).
  2. Its evaluation has no side effects (no mutation of local static variables, non-local variables, mutable reference arguments or I/O streams).

意思是說

  1. function 的輸出永遠只與 function 的輸入相關 (取隨機數就不算 pure )
  2. 沒有 side effects (沒有使用全域變數,或儲存永久變數)

盡可能的讓 getter/setter 寫成 pure function ,將有助於你 debug。
這樣的設計,是 flux 在對狀態做更新與取用時,特別要注意的。(ex: vuex 的 mutation, getter)

要不要用 return

要不要 return 這件事,其實是值得討論的。
而且,我們其實早就習慣 return 了。

高中數學有學過

y = sin(x)

假想一下 sin() 的實作會是長怎樣?

function sin (radians) {
  const PI = 3.1415926588;
  while (parseInt(radians % PI)) {
    //...
  }
  
  //...  
  return opposite / hypotenuse
}

有 return 將斜邊與對邊相除之後的比例,回傳出來。
換句話說,比例 = sin 是不是等同於 y = sin(x) 的語意呢?

如果今天,我們寫了這樣正確的 sin 函數,但是命名不寫 sin 寫了 ChrisCalculated() 這樣是不是完全看不出來,函數實作內容了?如果不了解 sin 定義的人,看了也一定不懂裡面就是 sin 函數。

有時要不要 return 的問題,其實是命名的問題。
請務必隨時檢查整體概念性

兩個輸出要怎辦?

在 C++ 中,兩個輸出很簡單,輸出處傳入指標或參考即可。
兩者在函數的修改,都會影響其變數本體,而不是副本。

void Fun(input i1, output* o1, output* o2) {

}

void Fun(input i1, output& o1, output& o2) {

}

在 JavaScript 中,就將輸出變成物件再傳入。

function Fun(input i1, {output o1, output o2}) {

}

這兩種都不是好方法,因為變數傳入函數之後,一般來說都是預設不會修改其變數本身,或者它的修改都不會影響傳入參數之前的樣子 (傳入副本)。

這樣做確實會產生某些「變數與函數之間的耦合性」要特別寫註解告知伙伴。

Fun(i1, {o1, o2}) // o1, o2: Fun1 的執行結果

上一篇
如何寫高品質 function (命名+參數篇)
下一篇
如何寫高品質 function (內聚性篇)
系列文
可不可以不要寫糙 code30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
jackithome
iT邦新手 5 級 ‧ 2018-11-07 14:45:01

Chris大大您好:
我是剛踏入職場的新鮮人,看了你的文章,我非常喜歡,
我也是希望自己的程式碼能越來越好,不要很糙,我有幾個關於設計類別的問題想提問,
就是在function傳入參數,是要使用function時傳入參數,還是要在創立類別時,利用建構子將參數設為欄位,執行function時,使fuction去抓取類別的欄位。
以及function結束時,是否要回傳結果,或者是將結果設為類別的欄位並取得。
關於兩種方式的使用時機,我實在不會拿捏,想問一下是否有各自的使用時機,還是依照個人的撰寫習慣呢?
希望大大能幫忙解答,謝謝~

看更多先前的回應...收起先前的回應...
Chris iT邦新手 4 級 ‧ 2018-11-12 08:40:32 檢舉

我稍微整理一下你的問題如下。(另外,我假設你的 function 指的是物件裡的 method,不是全域的 function)

問題一,在function傳入參數

  1. 使用function時傳入參數
  2. 在創立類別時,利用建構子將參數設為欄位,執行function時,使fuction去抓取類別的欄位

問題二,function結束時

  1. 是否要回傳結果
  2. 將結果設為類別的欄位並取得。

問題很難回答,有幾個面向可以考量

  1. 是不是要增加物件 Property 的問題
    是「抽象」這件事是不是可以做得很好。抽象指的是「將具現化的事物,抽取部份特性足以描述事物」
  2. Method 使用的變數
    也許會用到 side effect Method 存取物件的 Property 也是很正常的設計。不過,是不是適合的,要看你的語意是否有表達出「操作真實世界事物」的感覺。
  3. 回傳值,若不是執行狀態,可以依適合的命名決定要不要回傳值。
  4. 「將結果設為類別的欄位並取得」這是要不要 getter 的問題。我想會回歸到思考「抽象是否有做好」。

補充一件事

  • setter 是用來擋住垃圾值傳入,也就是要維持 property 符合定義域
  • getter 是用來滿足「呈現 property 的各種形式」,像是時間就有各種格式,但是值只有一個。

希望有回答到你的問題,如果還有疑問,可以再繼續問哦。
可以的話可以寫一些 sample code 來討論或者描述你的問題看看要怎麼寫比較適合。

抱歉,回覆的時間晚了。
假設不談論抽象的可能性,以簡單的程式舉例來說:

 class MemberService_A
    {
        public bool Login(string account,string password)
        {
            if (/*Check account & password...*/)
            {
                return true;
            }
            return false;
        }
    }

    class MemberService_B
    {
        string Account { get; }
        string Password { get; }
        public bool Result { get; set; }
        public MemberService_B(string account, string password)
        {
            Account = account;
            Password = password;
        }
        public void Login()
        {
            if (/*Check Account & Password...*/)
            {
                Result = true;
            }
            Result = false;
        }
    }

    static void Main(string[] args)
    {
        MemberService_A menberA = new MemberService_A();
        MemberService_B menberB = new MemberService_B("account", "password");

        bool Logined_A = menberA.Login("account", "password");

        menberB.Login();
        bool Logined_B = menberB.Result;
    }

這兩種都可以辦到相同結果。但不清楚哪種寫法是對於程式比較好的,又或者是兩種其實大同小異,不用專牛角尖哪種寫法。
還有,你指的"表達出「操作真實世界事物」的感覺"是甚麼意思,我有點不明白

Chris iT邦新手 4 級 ‧ 2018-11-16 21:47:40 檢舉

MemberService 這真的是很好的例子。
A 和 B 都做一樣的事的話,這兩種設計之間的差異。
是不是 Login 之後,才可以啟動所有的服務,是關鍵。

B 我會改寫成這樣 (之類的寫法)

class MemberService_B
{
    public MemberService_B(string account, string password, bool *Result)
    {
        Result = Login(account, password)
    }
    bool Login(string account,string password)
    {
        if (/*Check account & password...*/)
        {
            return true;
        }
        return false;
    }
}

或者

class MemberService_B
{
    public bool Result { get; set; }
    public MemberService_B(string account, string password)
    {
        Result = Login(account, password)
    }
    bool Login(string account,string password)
    {
        if (/*Check account & password...*/)
        {
            return true;
        }
        return false;
    }
}

使用上,就會像這樣

bool Logined_B;
MemberService_B menberB = new MemberService_B("account", "password", Logined_B);

或者

MemberService_B menberB = new MemberService_B("account", "password");
bool Logined_B = menberB.Result;

以 function 的 interface 來說的話

  • MemberService_A 的意思包含「就算不 Login 也可以提供其它的功能」,Login 前也可以使用某些 method
  • MemberService_B 傳達了「絕對要 Login 才可以使用」的意思,否則就算建構出來呼叫任何的 method 也沒辦法 work。
Chris iT邦新手 4 級 ‧ 2018-11-16 21:50:00 檢舉

「操作真實世界事物」的感覺呀!你的例子就應該有這種感覺。
就像是你舉的例子中的 MemberService ,某個服務,提供了哪些功能或可以操作的方式。
而不是一直在堆疊呼叫資料庫或條件判斷、迴圈的程式碼細節。

謝謝Chris大大的詳細解答,讓我了解其中的設計理念與差別,解開我心中疑惑。
希望未來還能繼續拜讀大大您的文章,因為這些文章都讓我受益良多,也希望未來有疑惑還能請教大大您,謝謝!

我要留言

立即登入留言