重構學習筆記(5)-大話重構第五章-第一步:從分解大函數開始-筆記

重構

關於遺留系統

筆者看軟體退化重災區挺有感,任何工程師都會遇到遺留系統問題,筆者遇過某個資訊系統,欄位資料表都是用代號以及代號名稱來處理,這是非常嚴重情形,筆者曾經改過小需求,實際修改要半天,一點都不誇張,你覺得改一個欄位很快…

是的,非常快!

但你要知道修改這欄位,會遇到狀況有下列情形:

  1. 要牽動多少地方?
  2. 花多少時間理解?
  3. 改的只是這部份,其他部分異動會有多少?(註記:改的只有這畫面,其他畫面或是其他系統呢?)
  4. 改了程式,風險值多大?(要有心理準備被砲的準備)

要知道非常嚴重遺留系統和遺留程式會有下列這些代價:

  1. 低效落的產出。
  2. 低效落生產。
  3. 改的可能會延伸出,新的bug。

尤其上級主管還會追殺你,問你多久時間會改好?   

那你會問有沒有解? 當然有解嚕,那就是重構。

但是請記住重構是要針對你所負責地方去重構,千萬不要別人開發中,你還順手重構他人,這叫做自作聰明,你能為別人開發時程負責?  

也要尊重他人!  

真要順道請跟人問一下取得對方授權XD…對方會很樂意… 

軟體重災區的特性

  1. 超級大類別。
  2. 超級大方法。
  3. 非常難理解程式。
  4. 非常難變更。

起初專案開發時,邏輯可能清晰,到系統開發完成後期,多數奮戰都是維護居多,因為需求會追加,系統也會進化,日子一長就就會越來越肥大,超級大函數和超級大方法就誕生嚕。

軟體規律就是業務邏輯開始複雜,因為大部分狀況會習慣在原有程式結構會一直往上追加新的功能,新的程式一直狂加,就會肥大,就會複雜,就會難懂,它會變成是一個資訊系統流行通病,正確解法是時常重構與改善程式碼。

筆者曾經開發一個功能之後,本來幾二、三十行,半年下來,經過輪流的替換,換到筆者,筆者一看到這區塊,短短半年加成到幾百行,複雜度成長很快,這讓筆者是難忘的寶貴經驗…

所以筆者的總結是以下:

  • 在初期就要想辦法維持基礎的骨架,就要拆細一點一次做到定位。
  • 仰賴於工具熟悉還有持續提高自己開發速度和效率和做事的方法熟悉。
  • 持續改善的模式,來因應日後三到五年,甚至十年以上的系統演變的進化,讓系統可以進化。

另外筆者認為應該措施

  • 該堅持有一定品質的骨架,文件和測試程式都要寫。
  • 針對有興趣有熱忱的工程師,提升這群軟體工程師的知識,分享自己的做法,進行知識擴散。
  • 只要是團隊的夥伴,就要養成互相Code Review好習慣,並制定規範和通用規則出來,並慢慢認知是程式是一個資產。

從先前的例子來看好了,初期的需求如下:

    public class HelloWorld
    {
        public string sayHello(DateTime now, string user)
        {
            string words = string.Empty;
            int hour = DateTime.Now.Hour;
            if (hour >= 6 && hour < 12)
            {
                words = "Good morning!";
            }
            else if (hour > 12 && hour < 19)
            {
                words = "Good afternoon!";
            }
            else
            {
                words = "Good night!";
            }
            return "Hi, " + user + "." + words;
        }
    }

隨者時間需求追加下,使用者又希望能夠有個節日問候,工程師開始加入節日的需求對應…

    public class HelloWorld
    {
        public string sayHello(DateTime now, string user)
        {
            string words = string.Empty;
            int hour = DateTime.Now.Hour;
            int month = DateTime.Now.Month;
            int day = DateTime.Now.Day;

            if (month == 1 && day == 1)
                return "Happy New Year!";
            else if (month == 2 & day == 14)
                return "Happy Valentine's Day!";
            else if (month == 3 & day == 8)
                return "Happy Women's Day!";
            else if (month == 5 && day == 1)
                return "Happy Labor Day!";
            else if (hour >= 6 && hour < 12)
                words = "Good morning!";
            else if (hour > 12 && hour < 19)
                words = "Good afternoon!";
            else
                words = "Good night!";
            return "Hi, " + user + "." + words;
        }
    }

接者工程師又加入存取使用者資料的功能…

    public class User
    {
        public string UserId { get; set; }
        public string Password { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
    }
    public class UserDAL
    {
        public List<User> users = new List<User>() {
            new User{   UserId = "A001",
                        Password = "test002",
                        Name ="張三",
                        Email = "aaa@gmail.com"},
             new User{  UserId = "A002",
                        Password = "test002",
                        Name ="李四",
                        Email = "bbb@gmail.com"},
        };

        public User GetFindByUser(string userId) 
            => users.Where(x=>x.UserId == userId).FirstOrDefault();
    }

    public class HelloWorld
    {
        public string sayHello(DateTime now, string userId)
        {
            var userDAL = new UserDAL();
            var user = userDAL.GetFindByUser(userId);
            string words = string.Empty;
            int hour = DateTime.Now.Hour;
            int month = DateTime.Now.Month;
            int day = DateTime.Now.Day;

            if (month == 1 && day == 1)
                return "Happy New Year!";
            else if (month == 2 & day == 14)
                return "Happy Valentine's Day!";
            else if (month == 3 & day == 8)
                return "Happy Women's Day!";
            else if (month == 5 && day == 1)
                return "Happy Labor Day!";
            else if (hour >= 6 && hour < 12)
                words = "Good morning!";
            else if (hour > 12 && hour < 19)
                words = "Good afternoon!";
            else
                words = "Good night!";
            return "Hi, " + user.Name + "." + words;
        }
    }

從這個案例的演變,歸納如下:

程式會隨者需求和時間演變,短短的工作十行,會逐漸膨脹越來越大會變成N倍左右。

單單從SayHello()這個函數來看,進行資料存取進行查詢,也要處理問候語和節日的對應結果處理,就是最佳例子。

筆者在實務上,會跟同事強調或讀書會分享時,當開發進行中系統,測試完之後給使用者驗收,確認驗收沒問題,開始進行上線部屬,系統會跟你三到五年以上時間,要把你開發系統當作是資產看待,除非你離開,否則系統一旦上線,就會有維護和後續需求出現,就算你離開,你去別的地方也是會接手別家公司的祖產。

系統上線之後,使用者想要多一些功能或是外在的市場環境和系統的優化…等等,所以筆者會認為系統要有具備可演進的能力,這樣才能在工作上有價值,還能保住自己的飯碗!

超級大函數的步驟

  1. 進行程式碼分段處理。
  2. 並相對獨立的程式碼寫註釋。
  3. 調整程式次序。
  4. 將關聯的程式放在一起。
  5. Extract Method-提取方法。
  6. Rename Method-重新命名方法-(盡量跟使用情境和業務領域及使用意圖命名)。

注意事項

  1. 變數宣告和真正使用程式碼放在一起,有明顯前後關係的放在一起。
  2. 被抽走的程式碼,一定是功能內聚且只執行一個清晰功能
  3. 抽取方法是從一個數千行程式碼的大函數開始分解成多個函數,分解出來的函數又繼續持續的分解,到最後分解成函數只有數十行程式碼,最後標註註釋和用途,筆者會偏用<Summary></Summary>寫註釋和寫用途,參數和返回值,跟寫API文件一樣。
  4. 當3一定程度之後,就會開始進行類別的分解,就會開始進行Extract Class。
  5. 命名不要太過於專業。

 

提取方法補充

  1. 處理函數和原函數之間的資料交換。
  2. 參數不要超過太多個,參數1-4個範圍,會使用ValueObject-(筆者通常都是在定義一個類別出來把相關屬性放置類別,關於ValueObject筆者,還尚未在專案使用過,這概念在DDD上有提及,筆者尚未了解這是幹嘛,在這邊稍微筆記一下。)
  3. 關於返回值設計值物件會導致造成大量值物件濫用,這邊作者是建議把返回值返回資料回傳到參數的值物件。
  4. 這邊所提的值物件是只有資料傳遞的簡單物件,將一堆變數雜亂無章的塞在一個值物件。

參考資料:
1.關於值無件
2.關於內聚

元哥的筆記