2018年4月25日 星期三

【MVC教學】Controller簡介


Demo範例 : Git位置

前面幾篇解說了Route設定,讓我們的程式能順利找到對應的Controller來執行,那今天要來談談Controller幫我們做些什麼?


我們通常會在Controller這層就將使用者的參數驗證完畢,並且依據傳入的參數,找到對應的商業邏輯去執行,並且回傳結果給使用者知道,已註冊頁面作為實際應用來解說。



接著我們來實作上述的內容,但因為只是MVC初學,所以我們還不加入分層、資料庫溝通之類比較深的內容,只專注在Controller怎麼做。


我們先建立一個UserController,專門處理User相關的服務跟邏輯



接著寫一個SignUp的Action,這邊有看到我們掛HttpGet的Attribute,只是指定呼叫動詞必須是Get的方式才會執行到這個Action,如果不知道Http動詞的話可以參考 : HTTP請求方法




而這個Action沒有任何內容,只有回傳一個View而已,接著我們來實作出回傳的頁面,在這個Action中點擊右鍵 > 新增檢視







接著直接執行,應該就能看到我們剛剛做出來的SignUp頁面


而這邊網址是 /user/signup,我覺得不夠直覺,所以到RouteConfig改一下




接著把頁面簡單做起來



寫一個Action來接收傳過來的資料

[HttpPost]
        public ActionResult SignUp(string account,string password)
        {
            bool Result = false;
            //帳號密碼都不能為空值
            if (!string.IsNullOrWhiteSpace(account) &&
                !string.IsNullOrWhiteSpace(password))
            {
                //帳號必須要有@字元
                //密碼必須大於六個字元
                if (account.Contains("@") && password.Length > 6)
                {
                    //我們判斷是否有註冊過的帳號,因為還沒有連結資料庫
                    //所以先假定steven@mymail.com被註冊過
                    if (account != "steven@mymail.com")
                    {
                        TempData["Message"] = "註冊成功!!";
                        Result = true;
                    }
                    else
                    {
                        TempData["Message"] = "帳號已經存在";
                    }
                }
                else
                {
                    TempData["Message"] = "帳號密碼不符合格式";
                }
            }
            else
            {
                TempData["Message"] = "帳號密碼不能為空值";
            }

            if (Result)
            {
                //註冊成功,導到首頁 
                return RedirectToAction("index", "home");
            }
            else
            {
                return RedirectToAction("signup", "user");
            }
        }

這邊注意到我們的Attribute就下了HttpPost,表示只有Post可以呼叫到這個Action,而我們在Form那邊也設定了方法用Post



接著你可能會注意到裡面有用到TempData,通常TempData是用來跨Action傳遞資料用的,底層其實是將資料存在Session之中,而且你只要取用過一次裡面的值就會被清掉。 而因為我們這個Action只是在處理註冊的相關邏輯,執行完後可以看到最後回傳的都是RedirectToAction,也就是導到我們指定的Action去回傳頁面,所以會跨兩個Action以上,存在TempData是個簡單的處理方式。


接著執行看看你會發現,好像一切有照著我們的邏輯在執行,但唯獨訊息不會顯示出來,因為我們雖然將TempData之中,卻沒有寫顯示訊息的那一段,通常這一段邏輯我們會放在共用的_Layout裡面。




因為還沒解說View的關係,這邊就先照著寫,之後會解說到。再次執行就會發現訊息會正確S顯示出來了。





上述的Controller撰寫方式,就是通常我們在Controller做的工作,驗證參數 、 依據使用者輸入的值執行對應的邏輯 、最後回傳結果。


但你應該也會發現,整個Action邏輯裡面光是驗證參數就佔了大半的篇幅,這往往會讓程式碼複雜度提高,閱讀變得困難,這部分我們會在下一篇講解該如何把這類的邏輯分隔出去,讓程式碼更好維護美觀一些。


另外Action其實還可以回傳很多種結果,前面範例用到了

View : 回傳頁面

RedirectToAction :回傳導頁結果

底層還支援了一些回傳方式,靈活應用就可以達成大部分的功能了。詳細參考: MSDN
圖片出處:https://msdn.microsoft.com/zh-tw/library/dd410269(v=vs.98).aspx


2018年4月24日 星期二

雜湊表(Hash Table)


學生時期學資料結構跟演算法時,每次看到厚厚的課本加上一堆用C語言寫的範例,雖然都有修過,但說真的不知道它可以拿來做什麼? 直到出了社會開始寫一些專案要調教效能時,才發現原來以前學的是這麼厲害的東西阿。


開始前想先推薦一下這本書【演算法圖鑑:26種演算法 + 7種資料結構,人工智慧、數據分析、邏輯思考的原理和應用全圖解】,作者用簡單的圖解方式帶領讀者瞭解艱澀的資料結構與演算法的歷程,雖然要實際應用在專案中還需要一些內化,但已經比我以前的課本好多了(拭淚),對這方面有興趣的非常推薦買這本書來看看


圖片出處: http://www.books.com.tw/products/0010771263


這篇要說的是雜湊表的原理跟實際上如何應用在專案中。


案例 :

公司有派送Coupon券的需求,而條件是該券不能與過往中的任何一張重複,所以在產生Coupon券代碼後,最好跟以前的做一下比對來確保沒有發生重複的情形。


而Coupon券為英數字12個字元格式,區分大小寫,比對方法想過以下幾種方式

1. 寫入的時候,用SQL語法的方式要求DB先搜尋確定沒有再寫

優點 : 寫法簡單

缺點 : 耗掉DB效能,當要寫入的筆數一多時,DB會出現效能瓶頸


2. 將全部的Coupon券撈出來後,進行比對,確定沒有再進行寫入

優點 : 因為是將資料撈出DB再從Application端做比對,所以對DB負擔較小

缺點 : 進行字串比對時該如何有效率執行是個問題,尤其是字串比對,如果處理方式不佳,也一樣會在Application端產生效能瓶頸


這邊我採取方式2並搭配雜湊表來解決,而為何要用雜湊表以下慢慢說明

產生Coupon券的程式如下

/// <summary>
/// 隨機產生Couopn
/// </summary>
/// <param name="number">幾位數</param>
/// <returns></returns>
public string CreateNewCode(int number)
{
 string allChar = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z";
 string[] allCharArray = allChar.Split(',');
 string randomCode = string.Empty;

 Random rand = new Random();
 int temp = -1;
 for (int i = 0; i < number; i++)
 {
  if (temp != -1)
   rand = new Random(i * temp * Guid.NewGuid().GetHashCode());

  int t = rand.Next(62);
  if (temp != -1 && temp == t)
   return CreateNewCode(number);

  temp = t;
  randomCode += allCharArray[t];
 }
 return randomCode;
}


執行方式如下


如果你是寫C#的,那要比對是否有一樣的東西存在List中最快的方式就是用Any()這個方法,而我們知道List儲存方式實際是這樣



記憶體位置放置資料內容,而每個節點會記錄下一筆資料的記憶體位置在哪,所以List裡面的資料未必是一個相連的記憶體區段,但它只要知道開頭那筆資料,就可以依序將資料逐筆讀取出來。


換言之,如果要搜尋一個列表中是否有相同的資料存在,必須用線性的方式搜尋,也就是逐筆檢查,從第一筆開始每筆拿出來看看,直到比對到為止,最佳的狀況是第一筆就是你要比對的資料,最差,就是最後一筆才是你要的資料,而且搜尋的成本會隨著資料的增長而遞增。


驗證


先從1萬筆開始
int 產生的資料筆數 = 10000;

void Main()
{
 //準備要用來搜尋的資料
 var Pools = CreateSearchPool();
 
 //透過Stopwatch來看看實際搜尋要花費的時間
 Stopwatch sw = new System.Diagnostics.Stopwatch();

 Random rnd = new Random();
 
 //總共花費的時間
 double TotalTime = 0;
 
 //搜尋一百次
 for (int i = 0; i < 100; i++)
 {
  //動態從產生的資料母體中抽一筆作為我們要搜尋的目標
  var RandomIndex = rnd.Next(0, 產生的資料筆數 -1);
  
  //取出要搜尋的字
  var Target = Pools[RandomIndex];
  
  //碼表歸零
  sw.Reset();
  //碼表開始計時
  sw.Start();
  
  //透過Any方式對List做搜尋
  var Result = Pools.Any(x=> x == Target);
  
  //搜尋結束,碼錶停止
  sw.Stop();
  
  //將時間加上這次搜尋花費的時間,為毫秒
  TotalTime += sw.Elapsed.TotalMilliseconds;
 }
 
 //算出平均每次搜尋,耗費的秒數
 (TotalTime /100).Dump();
}

//建立要搜尋的母體
private List<string> CreateSearchPool() 
{
 List<string>  Pool = new List<string>();
 for (int i = 0; i < 產生的資料筆數; i++)
 {
  //動態新增Coupon資料
  var Code = CreateNewCode(12);
  //丟進我們要用來搜尋Pool
  Pool.Add(Code);
 }
 
 return Pool;
}


// 隨機產生Couopn券代碼
public string CreateNewCode(int number)
{
 string allChar = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z";
 string[] allCharArray = allChar.Split(',');
 string randomCode = string.Empty;

 Random rand = new Random();
 int temp = -1;
 for (int i = 0; i < number; i++)
 {
  if (temp != -1)
   rand = new Random(i * temp * Guid.NewGuid().GetHashCode());

  int t = rand.Next(62);
  if (temp != -1 && temp == t)
   return CreateNewCode(number);

  temp = t;
  randomCode += allCharArray[t];
 }
 return randomCode;
}


實際算出來的平均搜尋時間為 0.083978(毫秒)


將搜尋母體放大100倍,也就是從100萬筆資料中隨機抽樣搜尋100次,結果為8.540132(毫秒)


可以觀察到搜尋效率隨著資料量的增長快速遞減

而一百萬筆對於Coupon券來說其實不多,如果你的會員數有一萬人,每個人發到第100張時,總共發出去的數量就達到這個等級了 ,相信很多店商平台所產出的Coupon數遠遠大於這個量。


用雜湊表來試試看

先說明雜湊表的概念,它跟List的最大差異是它非線性搜尋,它將所有要放入的資料先進行雜湊的方式算出一個值後,依據算出來的值放到對應的記憶體位置去,搜尋時也是先將要搜尋的值進行雜湊運算,算出對應位置,直接取出該記憶體的資料進行比對



特點是它非線性搜尋,也就是說它不需要抓到第一筆資料後,依序依據指標,往下找下一筆資料,即便不是還是要每筆遍尋才能知道結果,雜湊表的好處就在於,你要搜尋時,就已經知道該去哪找了。


而可能會有一個問題,那如果經過運算後,兩筆資料要儲存的地方一樣呢? 這時候就是發生所謂的碰撞,一張好的雜湊表理論上要盡量避免碰撞發生,但現實中難以避免,所以進階的用法就是將相同位置內再放入List來存入更多筆資料。




這邊可能會有一個疑問是,那跟我一開始用List有什麼差別 ?  如果我們相信資料是平均分佈,那雜湊結果理論上也會平均分佈,但就如前面提的,現實中實在難以避免碰撞的發生,所以即便真的發生碰撞,我們也能確定List中的資料絕對不會有很多筆,多到導致效能瓶頸的發生。


所以雜湊表的陣列該開出幾格來就是需要經過考量的,如果你有數百萬筆的資料,只開出100格,那最平均的結果就是每一格裡面會有1萬筆的資料,這顯然不理想。


驗證

一樣先從1萬筆開始


int 產生的資料筆數 = 10000;
int 雜湊表格數 = 1000;
void Main()
{
 //準備要用來搜尋的資料
 var Pools = CreateSearchPool();

 //透過Stopwatch來看看實際搜尋要花費的時間
 Stopwatch sw = new System.Diagnostics.Stopwatch();//引用stopwatch物件
 
 Random rnd = new Random();
 
 //總共花費的時間
 double TotalTime = 0;
 
 //搜尋一百次
 for (int i = 0; i < 100; i++)
 {
  //動態從產生的資料母體中抽一筆作為我們要搜尋的目標
  var RandomIndex = rnd.Next(0, 產生的資料筆數 - 1);

  //取出要搜尋的字
  var Target = AllCode[RandomIndex];
  
  //碼表歸零
  sw.Reset();
  //碼表開始計時
  sw.Start();
  
  //取得Hash後應該存放的位置
  var HashPosition = GetHashPosition(Target);
  
  //從陣列中取出該筆資料
  var PositionData = Pools[HashPosition];
  
  //如果有資料
  if (PositionData != null)
  {
   //檢查這個List是否存在相同的Coupon代碼
   var Result = PositionData.Any(x => x == Target);
  }
  //碼錶停止
  sw.Stop();
  
  //將時間加上這次搜尋花費的時間,為毫秒
  TotalTime += sw.Elapsed.TotalMilliseconds;
 }
 
 //算出平均每次搜尋,耗費的秒數
 (TotalTime / 100).Dump();
}

List<string> AllCode =new List<string>();
private List<string>[] CreateSearchPool() 
{
 List<string>[] SearchPool = new List<string>[雜湊表格數];
 for (int i = 0; i < 產生的資料筆數; i++)
 {
  var Code = CreateNewCode(12);
  AllCode.Add(Code);
  
  var p = GetHashPosition(Code);
  var PositionData = SearchPool[p];
  if (PositionData == null)
  {
   PositionData = new List<string>();
   SearchPool[p] = PositionData;
  }

  PositionData.Add(Code);
 }

 return SearchPool;
}

/// <summary>
/// 隨機產生Couopn
/// </summary>
/// <param name="number">幾位數</param>
/// <returns></returns>
public string CreateNewCode(int number)
{
 string allChar = "0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z";
 string[] allCharArray = allChar.Split(',');
 string randomCode = string.Empty;

 Random rand = new Random();
 int temp = -1;
 for (int i = 0; i < number; i++)
 {
  if (temp != -1)
   rand = new Random(i * temp * Guid.NewGuid().GetHashCode());

  int t = rand.Next(62);
  if (temp != -1 && temp == t)
   return CreateNewCode(number);

  temp = t;
  randomCode += allCharArray[t];
 }
 return randomCode;
}

SHA256 sha256 = new SHA256CryptoServiceProvider();
//取得Hash後應該存放的位置
private int GetHashPosition(string code) 
{
 var ByteArray = sha256.ComputeHash(Encoding.Default.GetBytes(code));
 var IntResult = BitConverter.ToInt32(ByteArray, 0);

 //轉正
 IntResult = Math.Abs(IntResult);
        //除格子數,餘數就是這筆資料該放的位置
 return IntResult % 雜湊表格數;
}


實際算出來的平均搜尋時間為 0.004124(毫秒)



一樣將數字放大到100萬筆,實際算出來的平均搜尋時間為 0.033612(毫秒)


可以發現即便搜尋筆數擴張了100倍,效率並沒有完全等比遞減





當然各種資料型態跟搜尋狀況不同,可能適用的資料結構與演算法也會略有不同要取捨,雜湊法也並非沒有缺點,例如在製作表時比較耗時,所以適合用在資料變動不大的情境,先將表做起來後放到快取去更新維護都是一些優化的方法,以上提供給大家參考。

2018年4月4日 星期三

【MVC教學】4. 為你的Route加一點限制


上一篇寫了Route比對的邏輯,這次來點更進階的應用,讓我們幫Route比對加上一些些限制。

假設今天About的頁面,他是依據網址帶入的 ID取得對應的會員資料,回傳結果,而ID必定為數字,如果不是數字就不要進到程式碼,直接擋掉該如何做?

//我們希望的網址,最後的ID一定要為數字
/home/about/1

加上Constraints限制,而限制的方法用正規表示法來表達,以上述的只能為數字為例

constraints的部分,我們把ID限制在只能出現數字,(如果對於正規表示法不熟悉的話,推薦可以翻翻這本書,就算記不起來拿來當工具書也很實用 處理大數據的必備美工刀 - 全支援中文的正規表示法精解

接著執行網站試試看 /home/about/123

看起來沒問題,接著我們執行/home/about/Steven,這邊請記得把Default那組Route註解起來,如果還記得上篇Route比對方法的話,這組雖然會因為Steven不是數字而被About那組Route擋掉,但依然符合Default的萬用Route比對規則,而正確執行,為了測試請先註解掉Default那組。


如我們預期的,因為Steven不是數字的原因而被擋掉了,Constraints算是一個可以把Route用得更靈活的技巧,雖然需要懂的正規表示法,但我覺得這兩項學習投資很划算,正規表示法到很多地方都很萬用。


自訂更複雜的Constraints

我們再來出個更刁的要求,假設你老闆就叫Steven,而且他不希望跟別人一樣,每個人都是打ID查資料顯示太一般,他偏偏要只有輸入Steven也要能進到About頁時該怎麼辦?


當然也可以用正規表示法硬做,但可能會讓Constraint寫得很醜難維護,所以這次改用實作IRouteConstraint的方式來完成這個需求


建立一個StevenBossConstraint的Class


實作以下內容


將原本設定ID的Constrainte改成我們寫的StevenBossConstraint


執行後就會發現,數字跟Steven都可以通過Route的檢查,但你打Tom或是Tim之類的其他非數字參數,都會被擋掉




透過小工具來幫助偵錯Route設定

可以透過Nuget來安裝Route Debugger工具,他們告訴我們目前網頁之所以能夠顯示,是因為走了哪一條Route規則,這在初期我對這些設定還不熟悉時幫助非常的大




安裝這組套件



接著重新執行網站就可在看到詳細解說



相信剛開始要改Route的使用者來說,會是相當有幫助的工具喔!!