許多人撰寫程式,專案開發,為什麼這麼做,程式為什麼這麼寫,卻也說不出所以然來。其實筆者在先前的文章一直強調其實系統分析、設計其實是有方法可循的。尤其使用 UML 作為系統分析的方法的時候更是如此
自從去年撰寫一篇Visual Studio 2010_塑模化應用程式設計(六) [實例解說一個Shopping網站] 文章後,筆者大概有一年沒寫過類似的文章了。許多人撰寫程式,專案開發,為什麼這麼做,程式為什麼這麼寫,卻也說不出所以然來。其實筆者在先前的文章一直強調其實系統分析、設計其實是有方法可循的。尤其使用 UML 作為系統分析的方法的時候更是如此。
所以本篇希望再以 UML 為主題,試著從分析開始、到系統設計、撰寫程式,從頭到尾,實做一遍,讓有興趣的讀者實際的體驗一下這種感覺。這裡筆者選用 UML 作為分析系統的工具的原因。
那麼我們就開始進入主題了,今天某家公司有一個需求,需要設計一套請假資訊系統,假設訪談時我們擷取到了如下的需求。
進行分析後,主要需求如下4點:
1. 員工可以向公司內向直屬主管請假,直屬主管必須核可後,員工方可請假成功。
2. 員工必須有設定代理人才可以請假。
3. 主管可以查詢部門內當日的請假狀況。
4. 員工也可以查詢自己的請假狀況,年度共休幾天、剩餘多少特休天數等。
先以 Use Case Diagram 分析基礎需求,找出系統中有哪些參予者(Actor),與之間的互動關係。通常找出需求敘述中的動作者、實際的使用者、人名等為基本找出參予者(Actor) 的方式。還不了解 Use Case 的讀者可以參考筆者先前的文章 Visual Studio 2010 塑模化應用程式設計(四)[Use Case 使用案例圖(簡介)]。其實實際從需求中分析發現還蠻簡單的,很明顯的 Actor 並不多,這個需求中有三個 Actor
- 員工
- 代理人
- 主管
在前面的基本需求中,各個的角色的職責非常的清楚,員工可以請假、設定代理人、查詢請假狀況。 而主管可以進行核准作業、不核准,以及也能夠查詢請假狀況,而代理人也是員工,應該要有一種通知機制,我們先保留在後面。因此,很快的,我們就可以畫出基本的 Use Case Diagram 如下:
在 Use Case 清楚之後,通常基本的實體也會浮現。一樣使用名詞分析法,在這個需求 與 Use Case Diagram 中應該可以找出
- Employess
- Agents
- Leave
- Boss
這個時候我們就可以來試著畫出 Domain Class Diagram
這時會發現,Boss 老闆要放哪裡呢?員工可以對直屬主管請假,但這條線不會指向主管,雖然員工與主管具有某種程度的關係。別忘了,需求中員工要可以在部門內查詢自己的請假狀況,所以很明顯這裡少了 Departments 這個物件。修正後的 Domain Class Diagram 如下:
接著在進行較細部的分析的時候,需求中員工與主管都可以查詢請假狀況以及記錄,所以 Leave 應該還要有 LeaveLog 來記錄請假的歷史紀錄資訊以提供之後的查詢。還有需要檢核員工剩餘的請假,也許是特別休假等,所以應該還要有 LeaveType 等。且部門之中通常具有多位員工,部門裡面會有主管,因此將主管、部門、員工的關係畫出來。而需求中也提到了員工可以向直屬主管請假,所以直屬主管也必須放入 Domain Class Diagram 中。這時 Domain Class Diagram 又會修改程如下:
看起來好像有點複雜,其實都只是對大的項目 (物件) 找出它們彼此之間的關聯性。而當我們有了 Use Case Diagram & Domain Class Diagram 之後,這個時候如果直接從功能面來思考的話,功能也會浮現。初步的分析我們發現會有四個功能,這主要從 Use Case 中得知的。所以,應該會有四張 Sequence Diagram,我們先將它畫出來,如下:
A. 員工請假 Sequence Diagram
基本分析:
在畫 Sequence Diagram 時候也是我們進行分析誰該與誰互動並得到結果的時候。根據下面 4 點分析內容:
(1). 員工會對 Leave 執行請假作業,因此 Leave 必須提供一個請假的方法以提供員工來叫用
取名 CallLeave() 方法,由於 Leave 必須知道是哪一個員工需要請假,所以必須將 Employees 傳入
(2). 當員工設定請假時,必須有設定代理人才可以繼續執行請假作業。所以 Employess 自身必須提供一個檢核方法。我們將它取名為CheckHaveAgent() 方法。
(3). 請假要能夠成功,當然必須員工自身還有剩餘足夠的天數。因此 Leave 必須提供檢查的方法。筆者取名為 ChecnFreeLeaveDays(Employess) ,同樣它必須傳入 Employess 物件,它才會知道要檢查誰剩下多少假期天數可以使用。
(4). 實際執行請假作業的方法,名為 ExecLeave(Employees).
畫出 Sequence Diagram 如下:
B. 主管核可請假的流程 與 主管不核可請假的流程 Sequence Diagram
C. 主管查詢部門請假狀況的 Sequence Diagram
D. 員工查詢自己請假狀況的 Sequence Diagram
在到了這個步驟後,筆者發現少了東西!什麼東西呢?在第一張 Sequence Diagram 圖中,員工必須檢核自己有沒有設定代理人,可是在流程中似乎少了設定代理人的 Sequence Diagram ,不應該是說,檢核是否有設定代理人的機制應該放到實際執行請假的 ExecLeave(Employees) 之中,所以第一張 Sequence Diagram 應該改為如下:
我們還是讓 Employess 可以檢查自身是否有設定代理人,這裡我就不詳細的交代它在檢查有無代理人的實作如何。
接著就是進行細部 Class Diagram 分析,有些物件可能是某些物件的屬性,比如員工姓名,請假 (Leave) 的假別 (LeaveType) 應該是 Leave 中的屬性,員工要請假,一定有區間,因此一定會有 [開始日期]~[結束日期]等等,Agents 也會有代理的期間,這部分的分析目的是找出物件的屬性,因為有些[名詞]並不會獨立存在,在這裡我們要把他們找出來。
找出不是獨立存在的物件外,同時也要將具備的關係找出來。因此最後的 Class 可能只剩下 Employess、 Agents、 Leave、 Boss、 Departments, LeaveLog 等等,這時就不算是 Domain Class Diagram ,應該是 Class Diagram,需要的方法與屬性我們都已經填上去了,如下:
不過對 SD 的規劃來說,這也只是初步的設計,當然目前的 Class Diagram 是可以 Generate Code 出來,VS 2010 會幫您產出程式碼的框架,但舊架構上,實際由 PG 開始 Coding 並不會直接使用這樣來撰寫程式,尤其在套用 MVC 來進行開發的時候更不會這麼做。那怎麼辦呢?沒有關係,將 Class Diagram 在稍作修改,筆者打算使用 Repository Design Pattern 來設計這套請假範例系統,並使用 ASP.NET MVC 4.0 來開發。
Design Pattern Repository 說明:
什麼是 Repository ?它是種使 Business Logic 與 DAL 層的偶合度降低的一種設計方式,實務上還蠻常使用的。當然也不是類似的也不是只有 Repository ,比如:Facade 等等。對 Repository Design Pattern 有興趣的讀者可以參考 Clark 最近的文章 [Architecture Pattern] Repository 寫得相當的清楚 。
寫在前面:
在開始之前筆者先說明一下,本範例系統會 EDM 來進行開發,只是我們會透過 Repository 的 Design Pattern 來降低 Business Logic 與 DAL 層之機的偶合度,當使用 ORM 相關技術時,ORM 所提供的資料存取介面即是為 DAL,它有可能被抽換為其他的資料庫或甚至其他種類的 ORM 相關技術,因此才需要透過 Repository 降低對資料存取的偶合度。這裡筆者只會用一部份 Repository 的概念,不會全部使用,重點是這裡的 Repository 會使用泛型來帶入當時 Runtime 實際的 Entity 物件,這個部分一樣可以使用 VS 2010 的 Class Diagram 來繪製並 Generate Code 出來。筆者先劃出 Repository Design Pattern 的雛形,這裡筆者共提供四個方法,如下:
- T GetById(object id);
- IEnumerable<T> GetAll();
- void Add(T entity);
- void Remove(T entity);
以 Class Diagram 畫出後如下:
如上,一個簡單的 Repoitory Pattern ,在這裡比者使用了泛行與條件約束,因為筆者希望執行時帶入當時執行的 Entity 。不過在 VS 2010 這裡這設計有一點要注意的是條件約束寫法在這裡有可能出現如下訊息:
會出現這個訊息是因為我們在類別與介面中使用了泛型<T>,而在VS 2010 的 Class Diagram 的 Generate Code 的 Configure default Code Generate Settings.. 裡面,在 Target Name 使用了 {Name} 帶入到路徑裡
而路徑裡面有<> 符號當然是不行的,解決方式也很簡單,就是在新增加一個 Interface Template ,並將 Target Name 設為固定名稱 IRepository
如此就能夠 Generate Code 成功。
接下來最重要設計是,我們在這個階段所設計出來的 UML Class Diagram 為套用 Repository Pattern 的實體,因此屬性[欄位] 的部分我們要再進行一個抽離的動作,因為到時候前端的 View 所呈現的欄位不見得會是如此的關聯性,也就是說到時候實際 View 所顯示的不見得都只對應到單一個資料表。所以在這個時候其實初步的 ER-Model 雛形其實也已經出現了,就是我們現在要抽離的屬性[欄位]。這裡 ER 筆者就以 Entity Framework 來繪製,在使用Entity Framework 所提供的[由模型來產生資料庫]的功能來產生我們所需要的 db Schema,繪製的Entity Framework 的 Model 如下:
很明顯的,ER 就是我們第一階段在 Class Diagram 所找到的物件關聯與欄位。在相關的 Table 都決定了之後,同時我們也可以將底下的已經實作 IRepository 的 Class Diagram 所 Generate Code 產生出來的程式碼再加以實作相關內容。
如上的 Class Digram 可以順利的產生如下程式碼框架:
在重點的 IRepository<T> 與 Repository<T> 的程式碼框架均可以順利的產生。
下面是 Generate Code 自動產生出來的 IRepository<T> 的程式碼:
1: //------------------------------------------------------------------------------
2: // <auto-generated>
3: // This code was generated by a tool.
4: // Changes to this file will be lost if the code is regenerated.
5: // </auto-generated>
6: //------------------------------------------------------------------------------
7: using System;
8: using System.Collections.Generic;
9: using System.Linq;
10: using System.Text;
11:
12: public interface IRepository<T> where T : class
13: {
14: void Add(T entity);
15:
16: IEnumerable<T> GetAll();
17:
18: T GetById(object id);
19:
20: void Remove(T entity);
21: }
22:
Repository<T> 的部分如下,其他的筆者就不一一的列出了:
1: //------------------------------------------------------------------------------
2: // <auto-generated>
3: // This code was generated by a tool.
4: // Changes to this file will be lost if the code is regenerated.
5: // </auto-generated>
6: //------------------------------------------------------------------------------
7: using System;
8: using System.Collections.Generic;
9: using System.Linq;
10: using System.Text;
11:
12: public abstract class Repository<T>: IRepository<T>
13: {
14: public void Add(T entity)
15: {
16: throw new NotImplementedException();
17: }
18:
19: public IEnumerable<T> GetAll()
20: {
21: throw new NotImplementedException();
22: }
23:
24: public T GetById(object id)
25: {
26: throw new NotImplementedException();
27: }
28:
29: public void Remove(T entity)
30: {
31: throw new NotImplementedException();
32: }
33: }
34:
當然,程式碼不是 Generate 出來之後就可以直接使用,我們還是要稍作修改。主要是修改 Repository<T> 這個類別,因為到時其他的物件均會繼承它,除了實作基本的四個方法之外,自己也可能衍伸出其他的方法。
首先,加入 ObjectContext 與 IObjectSet<T> 這兩個 protected 的變數,並實作 Add、GetAll、Remove、等 方法,修改完成的 Repository<T> 變成如下:
1: //------------------------------------------------------------------------------
2: // <auto-generated>
3: // This code was generated by a tool.
4: // Changes to this file will be lost if the code is regenerated.
5: // </auto-generated>
6: //------------------------------------------------------------------------------
7: using System;
8: using System.Collections.Generic;
9: using System.Linq;
10: using System.Text;
11: using System.Data.Objects;
12:
13: public abstract class Repository<T>: IRepository<T>
14: where T : class
15: {
16: protected ObjectContext _context;
17: protected IObjectSet<T> _objectSet;
18:
19: public Repository(ObjectContext context)
20: {
21: _objectSet = context.CreateObjectSet<T>();
22: _context = context;
23: }
24:
25: public void Add(T entity)
26: {
27: _objectSet.AddObject(entity);
28: }
29:
30: public virtual IEnumerable<T> GetAll()
31: {
32: return _objectSet;
33: }
34:
35: public abstract T GetById(object id);
36:
37: public void Remove(T entity)
38: {
39: _objectSet.DeleteObject(entity);
40: }
41:
42: protected void SaveChanges()
43: {
44: _context.SaveChanges();
45: }
46: }
47:
繼承 Repository<T> 的 Employess 的程式碼如下:
1: public class EmployeesRepository : Repository<Employess>
2: {
3: public EmployeesRepository(ObjectContext context)
4: : base(context)
5: {
6: }
7:
8: public virtual bool CheckHaveAgent()
9: {
10: throw new System.NotImplementedException();
11: }
12:
13: public override Employess GetById(object id)
14: {
15: var result = _objectSet.Where(c => c.ID==(int)id);
16: return result.FirstOrDefault<Employess>();
17: }
18: }
當然,這時的 CheckHaveAgent() 還未實作。而 LeaveRepository、LeaveRepository、AgentRepository、等.. 這裡筆者就不一一列出了。
接下來是 View 的設計。在開始說明之前,關於設計的時機筆者說明一下:當然實際的開發並不會到這裡才設計 View,我們在進行軟體開發的階段過程中,這裡的設計有時會是同時進行的,一般來說,在 SA 階段時,要決定使用者的需求時,通常會透過使用 Prototype 畫面來確認使用者需求,決定了 Prototype 的設計,通常也決定了 View 需要哪些欄位,(當然也是有一些系統不見得在 SA 階段就與使用者確認畫面) ,而View 的決定,也會決定我們這裡的 MVC 的 View 的設計。
也就是說,前面我們在繪製 Sequence Diagram 的時候,其實就已經找出系統所需的功能,在那個時候就可以同時進行 View 的設計,在 SA 階段分析時,同時間會修改 Class Diagram ,當然 View 的欄位也是從 Class Diagram 找出來的,而 Class Diagram 的欄位可能是在進行分析 Use Case Diagram 與業務規則時所找出來的 。各圖型也不是說畫完Class Diagram 就一定畫 Sequence Diagram ,當然,更不是說畫完 Sequence Diagram 就不能回頭修改 Class Diagram ,實際設計的時候有時分析 Class Diagram 時才會發現 View 缺少什麼東西,或在繪製Sequence Diagram 時發現 View 設計的缺失等等。
待續…
下一篇我們開始進行 View 的設計,與實際的實作部分。
我們下一次見!
謝謝閱讀到此的讀者 :D
簽名:
學習是一趟奇妙的旅程
這當中,有辛苦、有心酸、也有成果。有時也會有瓶頸。要能夠繼續勇往直前就必須保有一顆最熱誠的心。
軟體開發之路(FB 社團):https://www.facebook.com/groups/361804473860062/
Gelis 程式設計訓練營(粉絲團):https://www.facebook.com/gelis.dev.learning/
如果文章對您有用,幫我點一下讚,或是點一下『我要推薦』,這會讓我更有動力的為各位讀者撰寫下一篇文章。
非常謝謝各位的支持與愛護,小弟在此位各位說聲謝謝!!! ^_^