顯示具有 Clean Code 標籤的文章。 顯示所有文章
顯示具有 Clean Code 標籤的文章。 顯示所有文章

2013年11月20日 星期三

CleanCode 讀書筆記 (六) 用程式說故事

命名是個藝術


clean code裡面有提到,越多的註解代表程式寫得越差。
最近一直在體會這句話的涵意,讓我們來看看下面的例子:

            
            bool bo = Dic.TryGetValue("hello",out val);
            if (bo)
                Console.WriteLine("do something");




從上面的例子我們知道當bo為true時,我們必須 do something。
再看看下面的例子:
            
            bool HasKey = Dic.TryGetValue("hello",out val);
            if (HasKey)
                Console.WriteLine("do something");

上面的例子讓我們感覺像是在讀文章

first, we get a variable HasKey and then if  it hsa key  we will do something.

接著我們再看一段簡單的程式:
  
            int x = 0;
            int y = 0;
            int z =0;
            while (z < 3)
            {
                if (IsTrue())
                    x++;
                else
                    y++;

                if (x == 3)
                    x_event();
                if (y == 4)
                {
                    y_event();
                }
            }

我是故意寫那麼醜的,我相信大多數的人看完都只有一個感想  WhatThe....
接著我們再看看修改過的程式:


            static int Three_People_Out =3;
            int Strike = 0;           
            int ball = 0;
            int out_People =0;
            while (out_People < Three_People_Out)
            {
                if (IsInStrikeZone())
                    Strike++;
                else
                    ball++;

                if (Strike == 3)
                    StrikeOut();
                if (ball == 4)
                {
                    base_on_balls();
                }
            }

相信看到修改過的程式應該可以看出這是在寫有關棒球的程式。

if IsInStrikeZone 則 好球++ 否則壞球++
if 三好球  則 StrikeOut
if 四壞球  則 保送 ...

善用命名可以讓我們不需要使用大量的註解,卻可以寫出可讀性高的程式。
命名真是一門藝術!!

2013年10月25日 星期五

CleanCode 讀書筆記 (五) 錯誤處理II 從呼叫者角度定義例外類別

先看看下面的例子
public void GetContain()
{
  try{
      str.contains("KeyWord");
  catch(NullPointerException ex)
  {
       throw new NullPointerException ();
  }
 }
 public String executeHttpGet(String Url) {
  HttpClient httpClient = new DefaultHttpClient();
  String result="";
  try{
   HttpResponse response = httpClient.execute(request);
   if(response.getStatusLine().getStatusCode()==HttpURLConnection.HTTP_OK)
   {
      result = EntityUtils.toString(response.getEntity());    
   }
  }
  catch(NullPointerException ex)
  {
       throw new NullPointerException ();
  }
  finally
  {
       httpClient.getConnectionManager().shutdown();
  }
  return result;   
 }

以上的code 在出現例外時會丟出 NullPointerException 
然而這樣子的寫法使用者實際上並不知道發生甚麼事情
到底NullPointerException是由誰丟出來的呢?
 比較好的寫法應該像第二個範例


public void GetContain()
 {
  try{
        str.contains("KeyWord");
  catch(NullPointerException ex)
  {
       throw new DataAccessException("Get Contain Fail",ex);
  }

 }
 public String executeHttpGet(String Url) {
  HttpClient httpClient = new DefaultHttpClient();
  String result="";
  try{  
   HttpResponse response = httpClient.execute(request);
   if(response.getStatusLine().getStatusCode()==HttpURLConnection.HTTP_OK)
   {
       result = EntityUtils.toString(response.getEntity());    
   }
  }
  catch(NullPointerException ex)
  {
      throw new DataAccessException("Http Request Fail",ex);
  }
  finally
  {
   httpClient.getConnectionManager().shutdown();
  }

  return result;   
 } 

以上的code可以很清楚的知道例外是由誰產生
這樣的好處 讓我們將原本無法理解的exception變成我們所能理解exception 
讓我們知道確切的問題點在哪

接著下面的例子是將第三方API可能產生的exception 利用單一例外處理使得我們更好管理
將所有的例外使用單一的例外(DataAccessException)去處理
由於第三方的API 出現例外時不管錯誤為何
通常處理的方式都會類似
所以可以使用這樣的方式來處理
可以將該 API 包成一個class 專門處理該API

 
ThirdPartyHandler{
    private ThirdPartyAPI ThirdParty;
    public ThirdPartyHandler(ThirdParty)
    {
       this.ThirdParty = ThirdParty;
    }
    public String executeThirdPartyAPI(String Url) {
    try{  
        result = ThirdParty.Execute();
    }
    catch(NullPointerException ex)
    {
       throw new DataAccessException(ex);
    }
    catch(IOException ex)
    {
       throw new DataAccessException(ex);
    }
    finally
    {
      ...
    }
    return result;   
   }
}

從呼叫者角度定義例外類別告訴我們
利用封裝自定義 exception或者是增加message 讓我們更輕易的管理例外

2013年10月17日 星期四

CleanCode 讀書筆記 (四) 邊界

邊界主要是描述使用第三方API時如何想辦法
將API與我們內部的程式分隔開來   減低耦合
裡面有提到可以使用 Adapter  (轉接器)
Adapter是系統 與 API 之間的橋梁
使得API有所更動時  不會影響到系統  只需要在 Adapter上做修改即可

以下兩個範例  廠商在開發初期出版了  FactoryAPI_0
以及過了一陣子出版了新版  FactoryAPI_1
在兩版之間  裡面有一個method做了修正
如果沒有使用Adapter  會使得主程式必須做修正  修正後也可能會衍生出bug
但是使用Adapter的話  由於修改都在一個class內   相對來說比較不會衍生出新問題

廠商舊版FactoryAPI_0


    public class FactoryAPI_0
    {
        public void Make()
        {
            System.Diagnostics.Debug.Write("Make");
        }
    }

    public class FactoryAdapter
    {
        private FactoryAPI_0 ApI;
        public FactoryAdapter(FactoryAPI_0 API)
        {
            this.ApI = API;
        }
        public void Process()
        {
            ApI.Make();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            FactoryAPI_0 api = new FactoryAPI_0();
            FactoryAdapter adapter = new FactoryAdapter(api);
            adapter.Process();
            Console.Read();

        }
    }



廠商新版 FactoryAPI_1


    public class FactoryAPI_1
    {
        public void Execute()
        {
            System.Diagnostics.Debug.Write("Make");
        }
    }
    public class FactoryAdapter
    {
        private FactoryAPI_1 ApI;
        public FactoryAdapter(FactoryAPI_1 API)
        {
            this.ApI = API;
        }
        public void Process()
        {
            ApI.Execute();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            FactoryAPI_0 api = new FactoryAPI_0();
            FactoryAdapter adapter = new FactoryAdapter(api);
            adapter.Process();
            Console.Read();

        }
    }
由上面兩個範例可以發現    主程式並沒有被更動到
所有的更動都在 class 內部
造成的影響也都在class內   主程式成功的與API 分開

簡潔的程式邊界
當改變來臨時
好的設計能適應變化
不好的設計必須重新改寫或投入大量時間做修正

API 可以用封裝的方式  或者使用adapter
使API 被修正時  我們只需要做及少的修正

2013年10月16日 星期三

CleanCode 讀書筆記 (三) 錯誤處理I

裡面提到了一些例外的處理方式
其中有一點我覺得蠻重要的
不要回傳null  
與其回傳null不如回傳一個special case
這樣呼叫這個class的人就不需要去做 if(xx ==null)的判斷
也不會出現 NullPointerException  以下是範例


    abstract class EmployeeData
    {
        protected string name;
        protected string birthday;
        protected string address;
        public string GetName()
        {
            return name;
        }
    }
    class Ted : EmployeeData
    {
        public Ted()
        {
            name = "Ted";
            birthday = "1009";
            address = "Taipei";
        }
    }
    class NullEmployeeData : EmployeeData
    {
        public NullEmployeeData()
        {
            name = "";
            birthday = "";
            address = "";
        }
    }
    class EmployeeList
    {
        public EmployeeData GetData(string name)
        {
            switch (name)
            {
                case "Ted":
                    return new Ted();
                default:
                    return null;
            }

        }
    }
    class EmplyeeListBetter
    {
        public EmployeeData GetData(string name)
        {
            switch (name)
            {
                case "Ted":
                    return new Ted();
                default:
                    return new NullEmployeeData();
            }

        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            EmployeeData employeedata;
            EmployeeList badEmployeeList = new EmployeeList();

            employeedata = badEmployeeList.GetData("Marry");
            if (employeedata!=null)
                employeedata.GetName();


            EmplyeeListBetter betterEmployeeList = new EmplyeeListBetter();
            employeedata = betterEmployeeList.GetData("Marry");
            employeedata.GetName();
            Console.ReadKey();
        }
    }

上面的範例說明了當 user想要去Get Marry的資料時  系統內並沒有這個資料
badEmployeeList  return 了 null
然而 betterEmployeeList 回傳了  NullEmployeeData這個物件
這樣寫的好處是就算user沒有做  if (employeedata!=null) 這個判斷式
也不需要擔心 NullPointerException 的發生  系統還是可以繼續 run 下去

CleanCode 讀書筆記 (二) 物件及資料結構

在這個章節提到的重點是
結構話化程式碼容易添加新的函式  而不需要變動已有的資料結構
而物件導向得程式碼則是容易添加新的類別而不用變動已有的函式


以下是結構化的程式碼
 
    class SalaryCalculator
    {
        public double Calculate(string person)
        {
            if (person.Equals("boss"))
            {
                return 500 * 22 * 24;
            }
            else if (person.Equals("DayWorker"))
            {
                return 200 * 22 * 24;
            }
            else if (person.Equals("partTime"))
            {
                return 100 * 22 * 24;
            }
            throw new NullReferenceException();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            SalaryCalculator calculator = new SalaryCalculator();
            Console.WriteLine(calculator.Calculate("boss"));
            Console.ReadLine();
        }
    }
假設要添加一個SetWorkDays的函式
是蠻容易而且不會變動到已有的結構但是如果要新增一個
NightWorker (夜班) 人員的薪資  就是必要更改現有的函式

    class SalaryCalculator
    {
        private int workHours = 24;
        public void SetWorkHours (int workHours )
        {
            this.workHours = workHours ;
        }
        public double Calculate(string person)
        {
            if (person.Equals("boss"))
            {
                return 500 * 22 * workHours ;
            }
            else if (person.Equals("DayWorker"))
            {
                return 200 * 22 * workHours ;
            }
            else if (person.Equals("partTime"))
            {
                return 100 * 22 * workHours ;
            }
            else if (person.Equals("NightWorker"))
            {
                return 300 * 22 * workHours ;
            }
            throw new NullReferenceException();
        }
    }


接著我們看看  物件導向的程式

    
    interface CalculatorBase
    {        
        public double Calculate();
    }
    class BossSalaryCalculate : CalculatorBase
    {
        private int workHours = 8;
        public override double Calculate()
        {
            return 500 * 22 * workHours;
        }
    }
    class DayWorkerSalaryCalculate : CalculatorBase
    {
        private int workHours = 8;
        public override double Calculate()
        {
            return 200 * 22 * workHours;
        }
    }
    class partTimeSalaryCalculate : CalculatorBase
    {
        private int workHours = 8;
        public override double Calculate()
        {
            return 100 * 22 * workHours;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            //SalaryCalculator calculator = new SalaryCalculator();
            CalculatorBase calculator = new BossSalaryCalculate();
            Console.WriteLine(calculator.Calculate());
            Console.ReadLine();
        }
    }


    interface  CalculatorBase
    {             
       public double Calculate();
       public void SetWorkHours(int workHours);
    }
    class BossSalaryCalculate : CalculatorBase
    {
        private int workHours = 8;
        public void SetWorkHours(int workHours)
        {
            this.workHours = workHours;
        }
        public double Calculate()
        {
            return 500 * 22 * workHours;
        }
    }
    class DayWorkerSalaryCalculate : CalculatorBase
    {
        private int workHours = 8;
        public void SetWorkHours(int workHours)
        {
            this.workHours = workHours;
        }
        public double Calculate()
        {
            return 200 * 22 * workHours;
        }
    }
    class partTimeSalaryCalculate : CalculatorBase
    {
        private int workHours = 8;
        public void SetWorkHours(int workHours)
        {
            this.workHours = workHours;
        }
        public double Calculate()
        {
            return 100 * 22 * workHours;
        }
    }
    class NightWorkerSalaryCalculate : CalculatorBase
    {
        private int workHours = 8;
        public void SetWorkHours(int workHours)
        {
            this.workHours = workHours;
        }
        public override double Calculate()
        {
            return 300 * 22 * workHours;
        }
    }
上面的程式使用了interface 將計算的方式分隔開來使我們在新增NightWorker的時候
並不會更改到其他的class  但是當我們需要添加新的函式時反而造成了很大的困擾
必須針對每個class 實做SetWorkHours  當class一多  這樣是很痛苦的

所以必須觀察系統所需要的
若要增加worker的薪資計算的彈性  就要使用物件導向的設計
若要增加worker的函式功能的彈性  就要使用結構化的設計

2013年10月6日 星期日

CleanCode 讀書筆記 (一) 有意義的命名

最近正在讀 Clean Code 第二章剛讀完  就順便來整理一下讀後心得
有意義的命名  目的就是在讓人讀完程式碼之後就知道在幹嘛
舉例來說
C#
        
        public List<int[]> GetList(List<int[]> LJ)
        {
            List<int[]> ListK = new List<int[]>();
            foreach (int[] i in LJ)
            {
                if (i[0] == 1)
                {
                    ListK.Add(i);
                }
            }
            return ListK;
        }
看完上面的範例之後其實完全不知道程式在幹嘛
讓我們在來看看下面的範例
          
        public List<int[]> GetOpenTVList(List<int[]> TVList)
        {
            List<int[]> TV_OnList = new List<int[]>();
            foreach (int[] TV in TVList)
            {
                if (TV[State] == State_TV_On)
                {
                    TV_OnList .Add(TV);
                }
            }
            return TV_OnList;
        }
上下兩段程式的執行結果其實是一模一樣的
但是我相信下面的code看起來比較知道在幹嘛......吧(至少我是這樣覺得~_~)


這個function的工作就是從所有的電視中找出目前是開著的電視


關於變數的命名盡量使用有意義的命名 例如 ListJ vs TVList

使用可發音的變數  LJ vs TVList
使用不可發音的變數  會讓讀code的人去猜測他的用處  這是非常不好的情況

關於數字盡量使用variable去代替  例如  1 vs State_TV_On
當程式變得龐大時  搜尋 1 和 State_TV_On 的結果是完全不同的

在介面實作部分
舉例來說許多人習慣這樣寫
 
    abstract class ICellPhoneFactory
    {
    }
    class CellPhoneFactory
    {
    }
這樣寫其實並不好  我們並不希望使用者知道我們給他的interface
所以與其這樣寫不如
 
    abstract class CellPhoneFactory
    {
    }
    class OEMCellPhoneFactory
    {
    }
使用者只需知道這是一個CellPhoneFactory

關於類別的命名  應該要清楚  例如  Customer vs Data

關於方法的命名  要取出資料就應該有 Get   例如  GetCustomerData
                              要設定資料就應該有 Set    例如  SetCustomerId
                              要判斷資料就應該有 Is      例如  IsMyCustomer

書中有特別強調  命名的時候不要裝可愛 不要發揮幽默感  請正常的命名
沒有人會知道  IsMmmmmonsterKill 這個function是幹嘛用的 XDD
請用正常的命名方式  IsKillManyPeople

在方法得命名時不要出現兩種命名方式  例如  GetData 和  AccessData
請統一用同一種方式命名

在套用某些pattern或演算法時  盡量使用那些術語  讓讀程式的人能一眼看出這是在做甚麼
例如  CustomerFactory  閱讀的人一眼就知道這是個factory

結論  :
寫程式的命名就像是在寫作文一般
最好的programmer  就是讓使用者一眼就看出你現在想幹嘛
使用者根本不需要去看  function裡面實踐甚麼
只需要看主架構就知道程式的功能
好好的學習命名    這也是一門學問