1. 程式人生 > >分享:面相物件的建模(封裝業務層)

分享:面相物件的建模(封裝業務層)

我一直想寫這篇如何構建一完整系統的文章(從需求採集-分析-設計-實施-重構-整合--測試),目的是從思想上改變程式設計方式(而不是從某個技術上改變,如果是這樣,最終會變成形而向上的東西了),因為所有的行為都由思想驅使,技術相對是硬性或形式的東西(容易掌握但也容易變化),思想卻是軟性而實質東西(不容易掌握但也不容易變化)。

一直沒有動手的原因:一來是沒有時間(藉口吧),更重要的是我不知道如何寫,因為很多思想性的東西我不知道 如何用文字或語言來描述(程式設計師的表達能力公認是不怎麼樣的),但在工作中經常看到一些程式設計師寫程式碼,他們寫程式碼真的很任性。我並不是對他們有什麼鄙視或什麼不好的意思,我相信他們已經儘自己的所能做到最好的了,我只是希望在這個資訊從未如此發達的時代裡,他們應該能做的更好(早些年,網路發展初期,在電腦上有個api幫助文件就很滿足了)。

在構建我們的業務層的前,須要搞清楚幾件事:

1.搞清楚業務(這個對於初級程式可能有些困難),它是整個系統的靈魂或基礎,對業務中的每個過程(場景)需要有一個初步的認識,我習慣對一些比較複雜或想不透的業務過程用圖形的方式描述出來 (寫文件是一方面,另一面是能過寫文件把腦海中的思路理清),如果這塊沒有搞清楚,我想相信你會在後面死的很難看(我試過,也看過別人試過),這裡我也就不多說了,這裡給個建議:不要緊著寫程式碼,三思而後行

2.搞清楚邊界(廣義上的介面,它是建立在業務之上的),這個和建表的道理很相近:保持業務功能的單一性(這也是設計模式中的原則之一)

我們這個時代,工作分的越來越細,因為分工協作對處理大型而又複雜的工作很有效率,同樣我們的業務層也應該是這樣,把每一個實體類當成人一樣對待,對每個實體分派不同的功能,然後運用語言自身的特性(interface、abstract、 class、public、delegate)實現之間的互動、組合。依此類推,元件之間,層之間,系統之間。(可以把業務層當成一個元件來做也可以)

這裡所說的邊界不一定是指程式碼中的interface或com或其它形式化的定義,它是種思想或概念,當我們熟知它後,就知道在什麼時候用interface、abstract、 class、service.....形式化的語法, 而它在我們生活中無處不在(門,窗,電器中的控制器或控制面板,車站,電話.等等)

3.搞清楚什麼是檢視邏輯和業務邏輯,不要混為一談,這樣就知道那些程式碼應該放在那裡了

4.搞清楚物件與類的關係(這個本是基礎知識,本不想寫這點的,還是提示一下吧)

下面構建業務模型:

依據業務,整理出業務實體關係圖(設計與初步實施的階段):幫助我們從整體上觀察業務的合理性,減少由於業務需求變化對工作效率的衝擊。

這個是我其中一個覺得做的比較理想中的一個專案的業務關係圖(這個圖已被改過很多次了,在開發過程中會一直的修改,因為客戶方的需求很不穩定),箭頭代表對另一端的業務實體的引用(或看作地址指標或資料表中的外來鍵),對於每個實體中具體有什麼欄位或功能,我們可以在腦海裡有個大概就行。這裡的關注點在:業務的每個過程是否都可以對映(滿足)到業務關係圖中的某個或部分結點。

這個工作好象在設計資料庫,你可以這麼想,但去實施它時,我會先去寫Class,而不是去建庫建表。為什麼啦,在前期需求不穩定的情況下(可以是客戶方的問題,也有我們理解上的問題),而另一方面業務層是依賴於資料層的,如果資料層發生了改變,業務層也要改變動(如果檢視是同步實施與整合的話,還要改檢視層的程式碼,一般情況下這個階段不會對檢視的邏輯程式碼的實施,一般是樣式效果的設計),如果我們先從業務層著手的話,上述的問題對我們的程式碼就影響很小了。在我看到的初級程式設計師都習慣從資料層下手,最後可能會變成業務層形同虛設或兩極分化(在檢視層寫業務的邏輯程式碼或在資料層寫邏輯程式碼,早期的桌面時代比較偏重於後者--儲存過程+檢視)或變成資料訪問元件的代替品。當然,如果覺得整個業務層與業務過程都很相近了,十有八九都不會變了,那就直接建表也沒有問題。

從業務層下手帶來的問題:

如果我們的業務層初步完成時,需要與檢視進行整合,出一個演示版本給客戶看,以確定最後產品應該是怎麼一個樣子時,由於這個時候還沒有資料庫,怎麼辦;

我的辦法是實體類中方法中返回虛擬資料, 更新-》insert (..... ){  new Class(....)  ; return OK ;  } , 查詢-》GetObjects(...){  list.Add( new(...) , new(...) , ...  },

可以做一個容器物件當作臨時的資料庫來用。等到給客戶演示完後,核心需求比較穩定的時候再建庫(不是一定的,還是依據情況來定),而且這些程式碼可以變成業務層的測試程式碼

設計完業務關係圖後(主幹部分),下來就是設計每個實體類的屬性和方法(支葉部分)。

這一塊主要分成兩塊,

@業務部分:把業務拆分成多個子過程,對每個過程的思考分析,推斷出過程的需要儲存的屬性

@程式碼實施部分:下面內容基本上都在描述這個部分,這個部分主要是如果把具體的業務過程轉換成產品程式碼,並能適應檢視層、資料層,第三方元件或系統的對接或整合,以及程式碼的複雜度,邏輯完整性的適度。如果把這部分單一的拿出來思考 ,很容易,當需要考慮到各方面的適合度的時候就比較難處理了(就好象一幫人分配分錢一樣,那個人分的多或分的少都會引起混亂)

類成員:表示類內部定義的所有元素,如屬性,方法--構造,事件等等,包括例項和靜態的

###實體類設計基本原則###

類的內部要求是高內聚,與外部的關聯要求是低偶合的

如何做:

所有與當前類相關(更新與查詢)的資料及操作的程式碼都必須寫在這個類裡面,反之不相關的則不可以寫,只能引用 

通過構造方法的引數來指定與其它的關聯

外部類與之相互,都必須呼叫靜態成員來實現相互過程(包括更新和查詢)

(這裡說的模糊,下面會進一步說明)

(這裡最好分清一下

@物件的生命週期(這個應該好理解)

@邏輯生命週期:業務實體物件的邏輯生命週期或叫做記錄的生命週期:記錄被建立到修改到刪除的這個過程 , 它不是一個連續過程,但是一個先後過程,這個概念會與設計產生一些影響)

###屬性分為###

持久屬性:與資料表中一至,需要儲存到資料庫中的資料

輔助屬性:(狀態、格式化、外部(上下文)關聯的物件,或理解為持久屬性之外的屬性吧)

###檢視應用層(服務層)###

依據檢視的要求,對實體物件進行組合或過濾,返回檢視所對應的資料(控制資料包的size和複雜度),它是檢視的服務提供者,且內部的方法是獨立存在的,類似window的底層的API,方法之間沒有直接的關係,如web service , 檢視層的後臺程式碼(我不太喜歡那種html與後面程式碼混寫的方式,如asp,jsp,php前端寫法,因為這樣寫法對相互的依賴十分大,如果要更換檢視層,或增加檢視的展現裝置,如增加移動客戶端,這樣基本要重寫一次檢視層及服務層),如果服務層及檢視相互部分沒有設計好,也會帶來這種不良影響,但這個內容不在我的討論主題

###成員可以分成例項和靜態,還有呼叫範圍之分(如:public protected , private ,internal 等) ,我們在例項成員及呼叫範圍的基礎上,再分成可兩個類別###

@可序列(屬性,檢視使用的成員)與非序列(非public屬性及方法,服務層用到或只允許內部訪問的成員)

Code:#屬性:Name{ get:() ,  protected set() } ,  #方法:GetFullName(){ return this.Name +  this.CreateTime.ToString()  }

控制序列:因為實體物件之間關係就象一顆樹一樣,任意一個物件可以訪問到任意一個物件, 並且物件與物件存在雙向關聯 , 如果我在設計實體類時,需要在序列的層面上阻斷這種關係,以防止在序列時進入無限迴圈(不同於死迴圈,可理解為三角戀吧),開發工具很難察覺這個錯誤;或者把整個系統結構都序列出來,這個可不是我們想要的結果。

@向量(把計算後的結果儲存在變數中,呼叫時則直接返回結果--貪婪演算法)返回與動態(時間計算)返回結果

(我不知道如何描述才能清楚,我說一下這個劃分的作用是:業務層是處在檢視與資料層之間的一個連線及加工橋樑,現實中想象成一個建在一座橋上的加 工廠,或流水線上的一個機器手,在業務層與檢視層一般還有一個應用層(服務層)  )

(以上的都是一些概念性的描述,至於如何用,需要依據業務情況和檢視情況來確定,沒有什麼公式性東西,如:可以把業務層的程式碼寫到應用層,就是說在實體類與服務層或其它的程式碼都放在同一個dll中,或分散到不同的檔案中去,這樣做沒有問題--這更多是與部署相關,但你在的腦海中存在這種概念的劃分,是知道那些程式碼或類是屬於你概念中的那一部分內容,如果這樣,我覺得把在檔案中沒有分層也沒有問題,只不過後期的程式碼維護和部署會有問題)。

說一些擴充套件案例:有一次的中途接收某個專案,開啟程式碼檔案,檔案分類及類命名都做的很好,一看就知道每個類是做什麼的,一目瞭然,我心想這次終於不用幫別人擦屁股了,後來進行第二階段的開發後才發現我被它的外表給騙了,業務過程基本上是不完整的,業務層基本上呼叫一下資料庫就返回給檢視,大部的業務邏輯及第三方元件都集中到檢視層(疼啊)。說真的上一手開發人員能做到這個份上我還真很佩服他們的。

隨便說一下,中間會產生一些業務過程的分析文件,這裡我覺得對理解主題的內容幫助不大,這裡就不說了(屬於需求分析的範疇)

如果上述的一些概念都基本理解了,我說下去程式碼具體實現:

上圖為這個專案的類圖,

###專案介紹###

這個專案是用html+css+ js + C# + mssql2008 開發的,分為web端和app端

沒有用到第三方框架(用的是自己的一個框架),也沒有用到aps.net的控制元件(為什麼 不用,我後面會進行解釋原因)

檢視與服務層使用 AJAX +  json + 自定協議 進行互動

資料庫只建立了表,表只有主鍵,沒有建立外來鍵關係,沒有檢視,沒有儲存過程(這樣做考慮有二:@方便更換資料庫,@分散式資料庫)

大概知道一下就好,我找了一個代表的類來說

這個專案主要有三個角色:管理員、老師、學生 。

public abstract class Member : EntityBase //在庫中只有一個表,記錄了三個角色的基本資訊,Member類是所角色的基類

public class Manager : Teacher

public class Teacher : Member

public class Student : Member

我主要講解Member這個類,其它實體類形式上都基本上是一樣的

EntityBase :主要是一些底層的功能:如:資料訪問介面,通道介面、業務的基本配置資訊,全域性的業務ID標識建立

這樣子,我分成三個部分來講

@更新的操作

#建立及刪除:定義為static ,方便外部呼叫這些操作

public static Result Insert(Member newMember)

 public static Result Insert_Regist(Member newMember, int status)

public static Result Delete(Member theMember)

#修改:由於修改操作是對當前物件內部分資料的改變 ,所以做成了例項方法 , 如以下訪求的定義

public Result Update(string loginName, string loginPwd)

 public Result Update(string loginName, string loginPwd, string newPwd)

(可以發現,所有的更新操作都返回Result物件,因為一個更新操作需要做很多事,並不是上來就持久這麼簡單,所以每個處理都會返回不則的資訊,如:驗證,除錯資訊,是否返回物件,是否延遲返回等等的資訊,來指導呼叫方的下步的處理;不是一個簡單的int 就可以解決的 , 其實它也一種介面形式。另一個原因是要保持輸出的型別的統一性和有效性(在任何情況下都是如此,否則在呼叫方看來就會覺得太隨意了,他不好對結果進行定向處理),查詢也是一樣)

(另外,某個物件的更新,都能很方便被當前實體類的其它成員捕捉到,而又不會影響到其它的物件,主要是方便維護,修改程式碼無須考慮外部因素,這樣出bug的可能性大大降低,即使隨著系統複雜不斷提高,程式碼的複雜度也不會提高)

@查詢的操作

public static Member GetByID(string id) //因為這種面向物件的做法在效能上存在很大的問題,很多開發者都在這裡受限,從而被逼放棄面相物件的陣地,這個方法對於解決這個問題起到關鍵作用;我把這個問題說完吧,不想留到後面說了,我把程式碼貼出來:


其實這個方法裡有個快取的作用,如果依據ID找到物件後並快取起來,下次再查詢同一個ID的物件時則返回快取中的物件;

(注意,如果是分散式的或多程序的部署,就不能在當前程序中進行快取的操作處理了,這裡就要做須要一個介面與第三方的全域性快取元件進行對接,如mencache或monsql)

注意快取器是statice的,在更新操作的時候會對這個快取器進行同步的更新(因為所有的更新操作都在類的內部實現的),這樣就表示快取器的物件與資料庫某條記錄是一至,那麼這樣做與上述問題有什麼關係呢,我們來看看,無任是修改還查詢,都是對實體物件進行操作的,比如我想要查詢一個物件集,但問題是底層返回是一個記錄,這樣查詢多少打記錄就要進行轉換的多少次物件,並且這樣查詢是很頻繁,如果用快取,並且保持物件的更新與記錄是同步的,那麼,我們在進行轉換前看看這個ID記錄是否在快取記憶體在,這樣就免了轉換的工作了,效能自然就提上去了。後面的進行實體物件的組合操作也容易多了。

另外,我留意到很多開發者在這裡會這樣做,如果檢視需要一次返回多個表的查詢物件集合,通常的做法一個聯表查詢,這樣與實體類產生差異(無法進行轉換),要麼做多一個檢視物件,要麼直接返回資料集給檢視,前者的處理還好點(進行了二次組裝),後者看上去很方便,但存在資料完整性不可控的問題(出現了問題就會很不好處理),這樣返回給檢視層的資料結構會是扁平的,而面向物件應該層疊的有層次的,即使返回給檢視層的也是如此。如果你能用扁平的結構解決上述的問題,並能控制程式碼的複雜度的話,這樣也沒有問題(但我嘗過,似乎做不到,就象把多個問題交織在一起容易處理還是把問題分而自治的處理一樣的道理)

面向物件的的結構應該是複合形的或立體的,我們可以通過某個物件訪問到間接關係的物件(整個物件圖有點想一個樹一樣,每個葉子視為一個物件,樹枝視為物件之間的關係,每個葉子通過樹枝的關係能訪問到任何位置的其它葉子)

public static IList<Member> GetMember_NotSubmit(WorkersLessonFixup theFixup)

internal static int GetMemberCountByOrg(Organization theOrg)

public IList<Courseware> GetCoursewareByComments() //例項方法

最後 一個查詢為例項方法,其它都是靜態方法,有什麼區別呢,例項的查詢方法是依據當前物件的來進行查詢的,如查某職員的工作記錄,和查詢某個部門下所有職員的工作記錄,都是返回工作記錄,但呼叫物件是不一樣的,一個是通過某個職員物件呼叫自己的工作記錄,一個是某個部門物件呼叫所有職員的工作記錄

另外需要注意的是:方法的引數,有些開發人員習慣把條件引數定義為一個ID值,這樣會有點問題,無法確定ID的有效性,因為我這裡的對查詢的返回要麼是物件(返回null或有效物件),要麼是集合(即使查詢失敗,也會一定返回有效的空集合),如果傳入一個ID還要在方法內進行驗證ID的有效性(而且這種驗證程式碼會大量重複),出現問題如何返回,如果查詢條件不是主ID值,而物件的其它屬性值(可能是參考值),如有效時間區間或當前物件處理某種狀態時再進行查詢操作等。如果直接傳入一個物件(只要是任意一個物件都獲取與之相關的物件資料,因此,這裡輸入的不是一個單純的物件,它還包括了其它關聯資訊),驗證的問題在外部分就被處理了,再說驗證的工作本就不應該由這個查詢方法去做。(所在確定邊界很重要要,層的邊界,類的邊界,方法的邊界,無處不在)

@外部關聯:我們通過這種關聯把每個物件聯絡,最終形成一個物件樹,當然這只是一種方式,方式還有很多種,但要保持程式碼的一至性,最好選擇一種。

在當前Member物件其中有一個關聯是:當前member物件是屬於那個Organization(組織)物件的,這裡的Organization類也是一個抽象類,而在外部使用時都是使用Organization的型別物件,如果在其它物件中需要判斷Organization是那個類別的組織,可以能過Organization物件的類別屬性來區別。

因為Member物件依賴Organization,如果我在建立或構造一個Member物件時必須指定一個Organization物件,這是建立Member物件的必須條件,如果下程式碼:

 protected Member( Organization the )

下面是內部的關聯物件的訪問:

public Organization GetMyOrganization()

 //獲取當前成員所屬的組織 ,通過個例項方法就能獲取當前Member物件所在的組織資訊,為什麼做方法而不是屬性呢:主要是檢視顯示一個Member物件時只須要顯示它的組織名稱,我用了一個OrgFullName做為屬性來顯示當前Member所在組織名稱,另一個原因是想阻斷上向序列,可以看一下關係圖,如果繼續向上序列,那整個資料包會很大了

如果某檢視確實需要知道所在組織物件的所有資訊的話,我只需要繼承下當前的Member類,建立一個檢視類,然後在Member類下定義一個static查詢方法(返回的仍舊就Member型別,而不是繼承型別,這樣做是要保持邊界的統一性,至於呼叫方想知道物件的子型別,在呼叫方去處理---《給你做什麼,你就吃什麼》),由需要的物件去引用就可以了;如果想更方便一些,又不怕程式碼亂的話,就直接在Member類下加上個Organization物件屬性也行。

建構函式:protected Member(Organization the)  //強制在構造時就要指定一個Organization物件,保持任何時候都可以關聯到所屬的組織

注意到這個建構函式是protected,想要在外部New一個例項的話,須要呼叫 一個靜態訪求來建立一人新例項:

public static Member New(Organization org, E_MType mType)

 \\如果外部建立 一個Member物件的話,除了傳入Organization物件,還要指定當前建立Member的類別,前面的程式碼已看到Member類是一個抽象類,如果要new 則只能new 它的子類,所以通過第二個引數來確定建立那個子類的例項。

基本上每個實體類都不能直接去new ,需要通過一個外部的靜態方法來建立實體物件,這樣做只是想區分一下是內部new 還是外部new 的一些控制,即使沒有區別,也要控制一下,你不能保證,那一天需要返回它的子類物件(如檢視改變了, 版本控制,資料表改變了:可能是拆分或合併的情況,這些情況者有可能建立子類)

原則:如果想要改變邏輯,擴充套件優先,修改程式碼次之(也就是改變邏輯,但不改變程式碼)

基本的設計思想分享到這裡了。(同一個功能,一百個程式設計師可能有一百種設計)

附上原始碼的下載地址:http://download.csdn.net/detail/bo111/8902321

接來說一下,第三方的框架,

我曾在專案接觸過一些通用框架  如:EF(實體框架),Microsoft.Practices,J2EE--JDFrame,Asp.Net MVC , php-CpdeIgniter

還是很多我不知道一些框架,這些框架都很強大,至於開發效率是否能大副度的提高,我保留我的意見,我接觸過用到第三方面框架的專案,這些功能佔用率都不是很高(沒有超過30%,有的5%都沒有用到),為什麼會是樣,問題有很多方面,這裡不多說了

我想說的一下Web系統中(如果是桌面應用系統就沒有什麼問題),用到的MVC框架, 

MVC這種設計思想,更多是站在開發者一方考慮問題的思想,它的關注點是:把檢視與檢視邏輯進行分離,把UI人員的工作與後端開發人員的工作分離出來(如果是小專案,UI和程式碼實現都是現一個人),為了達到這種目的,需要以效能代價來換取程式碼的可控性(反正現在的硬體都很便宜,計算速度也很快),這種技術通過在服務端對頁面進行動態重構,最後轉換成html,返回給瀏覽器。有的開發者為解決這種行效能上的問題,使用AJAX技術,的確能有很好的作用,但程式碼的複雜度卻隨著功能的增加而增加(因為Web MVC 與Ajax的程式碼風格完全不一樣,即使把Ajax技術整合到MVC的框架中,問題也依然存在,只不過問題的程度不一樣)。

我只是想說明一下有這種問題,但工具用的好不好,不在在工具自身,而是受使用者來決定,還要看當前情況來決定。

無任那種框架,業務層還是需要自己去構建的。

最後說一下:構建業務層也好,構建系統也好,設計要簡單明瞭,即容易上手,也容易擴充套件和維護。 

祝你好運!