一、上章回顧
上章我們主要講述了系統設計規範與原則中的具體原則與規範。如何實現滿足規範的設計,我們也講述了通過分離功能點的方式來實現,而在軟件開發過程中的具
體實現方式簡單的分爲面向過程與面向對象的開發方式,而目前更多的是面向對象的開發設計方式。具體的內容請看下圖:
上圖描述了軟件設計的原則:低耦合,高內聚,並且簡單說明了,如何實現這2個原則,通過分離關注點的方式。我們把功能稱之爲關注點。
二、摘要
本文將通過實例來講解如何通過分離功能點,並且講解分離關注點實現相應功能點時應該注意的問題。比如說一些相關的重要部分的內容。分離功能點是實現軟
件功能的一項重要基礎,隨着軟件複雜度的不斷提高,傳統分離關注點的技術是隻從一種方式去分離關注點,例如按照功能或者按照結構等等,使得越來越多的關注點
得不到有效、充分的分離。因此有效、充分的分離關注點就是我們更好的實現軟件功能的重要標準,那麼我們如果想實現這個目的,就必須對軟件同時從多種方式進行
分解,因爲分解的越詳細,那麼系統的設計就越清晰,那麼就更容易滿足設計原則需求。通過分離關注點能夠使軟件的複雜度降到最低,同時可理解性得到提高。
本文將會舉例說明如何同時按照多種方式去分離關注點。因爲本文中的內容都是本人對工作過程中的經驗與總結,不足之處在所難免,還請大家多多提出自己的
意見和建議,錯誤之處,在所難免,請大家批評指出。
三、本章大綱
1、上章回顧。
2、摘要。
3、本章大綱。
4、分離關注點的多種方式。
5、相關設計方法。
6、本章總結。
7、系列進度。
8、下篇預告。
四、分離關注點的多種方式
我的理解是分離關注點(功能點)的方式有以下幾種及每種劃分的原則,下面我們將會講解如何按照不同的方式去劃分一個系統,通過抽象功能點來降低軟件系統的
複雜度,並且提高系統的可理解度。
1、按模型來劃分
這裏的模型劃分分爲概念模型與物理模型:當然這裏的概念模型就是抽象模型,例如我們平時說的功能的分離,我們以B2C的產品管理來說吧,產品管理裏面
至少擁有的功能是選擇產品分類,選擇產品單位,產品的擴展屬性,產品的所屬品牌等相關屬性信息。那麼我們閒來說說概念模型就是說是抽象模型,那麼我們通過圖
形化的方式來描述
能點的分離。
那麼我們在物理模型上如何去實現產品管理的物理模型呢?下面我們來看看。
簡單的解釋就是具體的每個功能點的具體業務設計模型,物理模式是概念模型的實現。
2、按層次來劃分
層次可以簡單的分爲分層分離的方式與橫切分離的方式,那麼來舉例說明,我們都知道橫切和縱切,就是說看待的問題的角度,下面來舉例說明如何以這2種
方式來分離功能點。
當然我們這裏仍然以B2C系統爲例來說明這樣的情況。我們這裏先來看分層分離的方式來處理。
我們的B2C可以簡單按照如下方式進行分層,業務邏輯層與界面通過服務層來調用,這樣可以避免UI層與業務層之間的耦合,而業務
邏輯層通過數據訪問層與數據庫進行交互。當然可能我這裏的部分設計還存在不合理之處,還請大家多多提出寶貴意見,好讓這個設計更加完善。
那麼我們下面來看下橫切分離方式的情況,我們知道,我們系統中可能會對管理員或者任何操作人員的操作的詳細信息進行詳細的記錄,那麼我們
就會通過日誌的方式來處理,橫切的方式就是系統從頭到尾的任何一個功能處都會用到,這是一個橫向分離關注點的過程。那麼我們在設計系統操作日誌時就會記錄相應
的操作日誌或者系統的錯誤日誌等等相關信息。
操作日誌與錯誤日誌貫穿每個分層結構、分離關注點橫向分離的方法實現就是AOP(面向方面編程)。當然我們後面會介紹AOP的具體實現方式細節。
五、相關設計方法
本節將會詳細的闡述分層與橫切分離關注點的二種編程方式的實現,通過編程方法實現關注的不同切面來分析設計方法的實現。這裏介紹的二種編程方法是面向
對象的編程方法實現分層方式的分離關注點與面向切面的編程方法實現橫切分離關注點的方式。
1、面向對象設計
首先、面向對象作爲一種編程思想,我想在園子裏面的大多數同仁都比較熟悉,我這裏也不詳細談面向對象的設計,這裏我們只是談談面向對象設計中的幾個
原則和需要注意的方面。
我們知道面向對象的編程思想是把世界萬物都看作是對象,而複雜的功能可以看作對象與對象之間的關係組成。那麼我們在分離關注點後,那麼每個關
注點可以進一步細化爲多個對象及對象之間的關係。
那麼我們來看看面向對象設計中的幾個基本的原則,並且分別的舉例說明:
a、首先必須先從分離關注點中分析出對象及對象之間的關係。例如我們以B2C系統中的店鋪管理來說。
圖中簡單的描述了對象之間的關係,店鋪信息依賴店鋪等級與店鋪類型信息,店鋪認證信息
依賴店鋪信息。
b、對象分離出來之後,那麼我們先來看看對象對應的類的幾個相關原則:
(1)、(SRP)單一職責原則,簡單來說就是一個類只提供一種功能和僅有一個引起它變化的因素。如果我們發現一個類具有多個引起它變化的因素時就必須想辦
法拆分成單獨的類。下面來舉例說明。我們這裏以ORM中的實體接口層來說。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public interface IEntity { ///
<summary> ///
保存 ///
</summary> ///
<returns>返回影響的行數</returns> int Save(); ///
<summary> ///
刪除 ///
</summary> ///
<returns>返回影響的行數</returns> int Delete(); ///
<summary> ///
寫入日誌信息 ///
</summary> ///
<param name="message">寫入信息</param> ///
<returns>返回影響的行數</returns> int WriteLog( string message); } |
很明顯這裏的寫入日誌與前面的對實體的持久化的操作明顯不搭邊,這樣的可能會造成2中引起類發生改變的因素時就必須分離,那麼就必須把它抽出來,單獨定義一個接口。修改後結果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public interface IEntity { ///
<summary> ///
保存 ///
</summary> ///
<returns>返回影響的行數</returns> int Save(); ///
<summary> ///
刪除 ///
</summary> ///
<returns>返回影響的行數</returns> int Delete(); } public interface Logger { ///
<summary> ///
寫入日誌信息 ///
</summary> ///
<param name="message">寫入信息</param> ///
<returns>返回影響的行數</returns> int WriteLog( string message); } |
(2)、(OCP)開發封閉原則:簡單來說就是不能修改現有的類,而需要在這個類的功能之上擴展新的功能,這時通過開放封閉原則來實現這樣的要求。該原則使我
們不但能夠擁抱變化,同時又不會修改現有的代碼。而這個原則的實現可以簡單來說就是我們將一系列發生變化的類的行爲抽象爲接口,然後讓這些類去實現我們定義
的接口,調用者通過接口進行操作。例如我們以MP3播放器來說。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public interface IMP3 { ///
<summary> ///
播放 ///
</summary> ///
<returns>返回操作是否成功</returns> bool Play(); ///
<summary> ///
停止 ///
</summary> ///
<returns>返回操作是否成功</returns> bool Stop(); } |
定義2個不同的實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
///
<summary> ///
臺電播放器 ///
</summary> public class TD
: IMP3 { #region
IMP3 成員 public bool Play() { return true ; } public bool Stop() { return true ; } #endregion } ///
<summary> ///
惠普播放器 ///
</summary> public class HP
: IMP3 { #region
IMP3 成員 public bool Play() { return true ; } public bool Stop() { return true ; } #endregion } |
通過一個測試類來模擬接口調用,通過依賴注入的方式實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class Test { IMP3
mp3 = null ; public Test(IMP3
mp) { mp3
= mp; } public bool Play() { return mp3.Play(); } public bool Stop() { return mp3.Stop(); } } |
具體的測試代碼我就不書寫了,我想大家都知道了。
(3)、(LSP)替換原則:簡單的來說就是基類出現的地方,擴展類都能夠進行替換,那麼前提就是我們不能修改基類的行爲。也就是說基類與擴展類可以互相相
容。在面向對象中可能會認爲很容易實現,不過我們要注意有時候我們從父類中繼承的行爲有可能因爲子類的重寫而發生變化,那麼此時可能就不滿足前面說的不改變
基類本身的行爲。我們最熟悉的多態其實這樣的情況就不滿足這個原則。需要注意的時,對調用者來說基類與派生類並不相同,我們簡單來說明。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class Test { private int tempValue=0; public void TestA() { tempValue
= 2; } public virtual void TestB() { tempValue
= 9; } } public class Test1
: Test { public override void TestB() { //如果調用該方法,那麼tempValue的值可以和基類中的得到的值是相同的,如果不顯示的調用幾類方法,那麼這個值將丟失 //則不滿足替換原則。 base .TestB(); } } |
通過上面的簡單代碼可知,里氏替換原則中需要注意的地方:當對具有virtual關鍵字和saled關鍵字的類或者方法需要特別注意,因爲這些關鍵字會對繼承類的
行爲造成一定的影響,當然上面的例子中只是說了重寫的情況,還有new的情況,就是把父類中的方法隱藏,同樣都是不滿足里氏替換原則的。本例中我們的
tempValue是私有類型的變量,那麼在基類中可以訪問到,派生類中卻無法訪問,所以我們要注意,在處基類替換時需要注意繼承的成員函數的訪問域,建議的方式是
虛方法訪問的類的成員變量儘量使用保護類型,這樣可以防止丟失的情況。當然基類中有虛方法訪問了基類中定義的私有變量,那麼如果在繼承類中如果想不丟失該基
類中該虛方法對其內部的私有變量的訪問,那麼可以在繼承類中通過“base.(函數名)”的形式來顯示調用基類方法,可以保持基類的行爲。
(4)、(DIP)依賴倒置原則:簡單來說就是依賴於抽象而不應該依賴於實現,這樣的目的就是降低耦合性。簡單的來說就是讓二個類之間的依賴關係通過接口來解
耦,讓一個類依賴一個接口,然後讓另外一個類實現這個接口,通過構造注入或者屬性注入的方式來實現依賴。簡單來說就是抽象不依賴於細節,細節依賴於抽象。下
面我們來舉例說明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
///
<summary> ///
汽車制動系統 ///
</summary> public interface IControl { int UpSpeed(); bool Brake(); } ///
<summary> ///
其他服務 ///
</summary> public interface IServer { bool Radio(); bool GPS(); } |
具體的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
///
<summary> ///
汽車 ///
</summary> public class Car { private IControl
control = null ; private IServer
server = null ; public Car(IControl
con, IServer ser) { control
= con; server
= ser; } public void Start() { control.UpSpeed(); } public void Play() { server.Radio(); } public void Map() { server.GPS(); } } |
上面簡單的舉例說明,並沒有給出具體的實現,只要實現上面的2個接口即可,這裏就不詳細說明了,希望大家能夠明白。錯誤之處還請大家指出。
(5)、(ISP)接口隔離原則:簡單的來說就是客戶不關心細節的東西,他就只關心自己能夠得到的服務,而面向對象的原則我想大家都知道,通過接口的方式來提
供服務。因此我們提供給客戶的是一系列的接口,那麼這樣就具有很好的低耦合性。我們來簡單的舉例說明:以ORM中的簡單的持久化操作來說
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public interface IORM { ///
<summary> ///
保存 ///
</summary> ///
<returns>返回影響的行數</returns> int Save(); ///
<summary> ///
刪除 ///
</summary> ///
<returns>返回影響的行數</returns> int Delet(); ///
<summary> ///
獲取所有列表 ///
</summary> ///
<returns>返回一個DataTable</returns> DataTable
GetList(); } |
這裏我們讓一個實體來繼承實現。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class ORM
: IORM { #region
IORM 成員 public int Save() { return 1; } public int Delet() { return 1; } public System.Data.DataTable
GetList() { return new System.Data.DataTable(); } #endregion } |
業務層的實現
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public class Entity { private IORM
orm = null ; public Entity(IORM
orm1) { orm
= orm1; } public int Save() { return orm.Save(); } public int Delete() { return orm.Delet(); } } |
這裏我就不貼出來測試代碼的實現了,後面我會把代碼貼出來下載,如果有興趣的同仁可以下載看看。當然我這裏只是拋磚引玉,不足之處,還請大家多多
指出。面向對象設計中還有很多的原則,這裏也不會一一複述。這裏只是冰山一角,還希望大家多多提出寶貴意見。
2、面向切面編程
面向切面編程,其實就是面向方面編程,其實這個概念很早之前就提出了,但是並沒有廣泛的流行,這個是比較讓人不解的地方,我平時其實使用的也是比
較少的。不過我們在系統架構中卻是非常有用,特別是在關注點的分離的過程中起到很大的作用。AOP的主要目的呢,是將橫切的關注點與核心的分層形式的或者說是
功能組件的分離。下面我們來看看AOP中的如何實現方面編程。
面向方面編程中的方面並不是直接有編譯器來實現,而是通過某種特殊的方式將方面合併到常規的源代碼中,然後通過編譯器編譯的方式。我們知道一個方
面就是一個橫切關注點,在實現方面的過程中,我們通常需要定義連接點與基於這個連接點上的通知來完成。下面我們來看看AOP的處理源代碼的模型:
AOP一般都是有框架提供注入的功能,而這裏的代碼注入功能與我們在面向對象的依賴注入不同。這裏的注
入是將方面的代碼植入到常規代碼片段中。
下面我們先來介紹AOP中的連接點與通知。
連接點:用來標識某個類型中的植入某個方面的位置,而連接點可以是某個方法的調用,屬性訪問器,方法主體或者是其他。連接點一般用來標識注入某個
方面的類型中的代碼位置。
通知:用來標識注入到類型中植入方面的具體的代碼。簡單來說就是要注入的方面代碼。
目前在.NET中已提供AOP的植入基礎功能。PIAB就是AOP在.NET下的一種實現方式。下面我們來簡單的說說,當然園子裏面不少的大牛也討論過這個
PIAB的相關介紹及用法。大家可以參考這些作者的文章。
一般我們在.NET平臺下有2種注入方面代碼的方式,下面以圖例來說明:
可能具體的實現方案這裏枚舉的並不全面。但是一般採取植入的方式就這2類了,運行期實現的方案較多,編譯期實現則需要有第三方提供方面植入工具,
完成編譯前的代碼植入,並且必須保證植入的代碼是可以編譯通過的。
如果想詳細瞭解PIAB請參考 :大牛 Artech的PIAB系列 《EnterLib PIAB深入剖析》系列博文彙總。
六、本章總結
本章詳細的闡述了軟件設計的規範與原則的實現方式,通過面向對象與面向方面編程來分離實現關注點,並且在實現過程中遵循的原則等。並且分析了分離關注點
中的分離方法與角度,通過多種方式及多角度的分離關注點,當然本文只是拋磚引玉,不足之處,還請大家多多提出寶貴意見。鄙人將在後續文章中持續改進,謝謝!