設(shè)計(jì)場(chǎng)景
1. 有A,B兩組開發(fā)人員進(jìn)行某個(gè)系統(tǒng)的開發(fā),其中A組開發(fā)人員負(fù)責(zé)B/S平臺(tái)的功能設(shè)計(jì)與開發(fā),B組開發(fā)人員負(fù)責(zé)C/S平臺(tái)的功能設(shè)計(jì)與開發(fā)。
2. 在當(dāng)時(shí)的項(xiàng)目背景下,B/S端的項(xiàng)目是先啟動(dòng)的,而A組的開發(fā)人員還沒有意識(shí)到將來需要配合C/S端來做功能協(xié)作,因此產(chǎn)生的問題就是,前期的系統(tǒng)架構(gòu)設(shè)計(jì)沒有過多地考慮以適應(yīng)多個(gè)平臺(tái)下的功能適應(yīng)性。當(dāng)然,從B/S端的設(shè)計(jì)角度上看,系統(tǒng)架構(gòu)還算比較清晰。接著A組的開發(fā)人員就在這樣的情況下,完成了系統(tǒng)功能的實(shí)現(xiàn)。
3. 接著高層領(lǐng)導(dǎo)告訴項(xiàng)目經(jīng)理需要做一套C/S架構(gòu)的軟件來配合B/S端平臺(tái)的使用,而這時(shí)候B/S端的功能實(shí)現(xiàn)已經(jīng)基本完成,B組開發(fā)人員成立。
4. 在B組架構(gòu)人員開始設(shè)計(jì)架構(gòu)的時(shí)候,并沒有衍用B/S端的開發(fā)架構(gòu),很多基礎(chǔ)架構(gòu)(如分層模式、數(shù)據(jù)庫(kù)結(jié)構(gòu)、數(shù)據(jù)實(shí)體類等等)都存在很大的差異(C/S端項(xiàng)目在初期的要求沒有那么高,有的功能能削減掉就削減掉),后來B組架構(gòu)人員發(fā)現(xiàn)需求文檔上的有個(gè)功能和B/S平臺(tái)上的某個(gè)功能是一樣的,于是他和A組架構(gòu)人員進(jìn)行交流,希望負(fù)責(zé)B/S平臺(tái)上這個(gè)功能的開發(fā)人員能夠幫助C/S平臺(tái)幫助完成這一功能。于是A組的Leepy就匆匆忙忙地上陣了。
5. 最初Leepy同學(xué)因?yàn)樵贐/S平臺(tái)上也有大量的任務(wù)需要完成,任務(wù)趕得狠,又收到這樣一個(gè)“功能復(fù)制”的任務(wù),心想:“那么就先把功能復(fù)制一份上去,然后如果B/S平臺(tái)上的功能有更新,就同步修改C/S平臺(tái)就好”。于是打開C/S平臺(tái)的項(xiàng)目,發(fā)現(xiàn)和B/S平臺(tái)項(xiàng)目的差異性比較大,包括數(shù)據(jù)庫(kù)結(jié)構(gòu)和數(shù)據(jù)實(shí)體類等等,更頭疼的是這里采用的是.net framework 2.0進(jìn)行開發(fā),而B/S端采用的是.net framework 3.5進(jìn)行開發(fā),而且從功能上,Leepy使用大量的3.5的屬性。要直接復(fù)用是不可能的,還需要調(diào)整相應(yīng)的代碼。
6. 于是C/S平臺(tái)該功能出來了,運(yùn)行得還行,F(xiàn)在才是郁悶的開始,因?yàn)樵摴δ軐儆谄脚_(tái)的核心模塊,于是B/S平臺(tái)上要時(shí)刻調(diào)整得比較大,所以同步的C/S端的功能也要相應(yīng)的調(diào)整,然后又運(yùn)行完好。于是問題出來了,這樣反復(fù)地修改導(dǎo)致系統(tǒng)(C/S和B/S)維護(hù)成本很高,架構(gòu)間的設(shè)計(jì)耦合度太大。剛開始Leepy抱怨為什么C/S端沒有和B/S端統(tǒng)一架構(gòu),至少底層基礎(chǔ)平臺(tái)能夠設(shè)計(jì)得具有可擴(kuò)展性,光光抱怨無(wú)法解決問題,因?yàn)檫@是項(xiàng)目的人員配置的問題。于是,Leepy想到了必須對(duì)該功能進(jìn)重構(gòu),使用一個(gè)通用的組件進(jìn)行抽象,而實(shí)際實(shí)現(xiàn)的,如C/S、B/S端具體應(yīng)用,只要維護(hù)相應(yīng)的業(yè)務(wù)代碼。
設(shè)計(jì)思路
1. 說完場(chǎng)景,現(xiàn)在說說動(dòng)手的部分。以一個(gè)中學(xué)生教育平臺(tái)591up的網(wǎng)站為例,以及教育平臺(tái)客戶端的輔助軟件。
這一功能實(shí)現(xiàn)一份Word文檔試卷的導(dǎo)入保存并分解文檔中的試題,將試題逐個(gè)保存入庫(kù)(解析出來的試題部分還包括很多屬性,如答案、知識(shí)點(diǎn)、解題關(guān)鍵點(diǎn)等很多屬性),F(xiàn)在B/S平臺(tái)和C/S平臺(tái)都需要這個(gè)功能,但是B/S平臺(tái)和C/S平臺(tái)下的相關(guān)數(shù)據(jù)庫(kù)實(shí)體類,設(shè)計(jì)不很統(tǒng)一,導(dǎo)致維護(hù)系統(tǒng)的成本很高。于是,考慮是否能將解析器的設(shè)計(jì)與業(yè)務(wù)功能分開,將試卷解析器設(shè)計(jì)成通用的組件,而與B/S端和C/S端的業(yè)務(wù)代碼徹底分開,對(duì)于解析的邏輯代碼(基礎(chǔ)代碼)在兩端都可以引用到,而B/S端和C/S端所需要做得就是調(diào)整業(yè)務(wù)代碼,并不需要關(guān)解析的基礎(chǔ)代碼是什么,組件與業(yè)務(wù)代碼解耦。如下圖所示:
2. 現(xiàn)在講講具體設(shè)計(jì)思路,先從試卷解析器基礎(chǔ)組件開始(為了簡(jiǎn)化,該范例是削弱版的),創(chuàng)建一個(gè).net 2.0的類庫(kù)(為了適應(yīng)客戶端.net 2.0的配置)聲明一個(gè)試卷解析器范型接口:
代碼
/// <summary>
/// 試卷轉(zhuǎn)換器泛型接口
/// </summary>
public interface IPaperConvertor<TIn, TOut>
{
/// <summary>
/// 轉(zhuǎn)換方法
/// </summary>
/// <param name="tIn">轉(zhuǎn)換輸入類型</param>
/// <param name="helper">Word處理接口</param>
/// <returns>轉(zhuǎn)換輸出類型</returns>
TOut Convert(TIn tIn, IWordHelper helper);
}
其中TIn類型作為輸入類型,TOut類型作為輸出類型(TIn將來作為業(yè)務(wù)代碼中實(shí)際的輸入類型,如WordInfo類;TOut作為實(shí)際輸出類型,如PaperInfo類;IWordHelper為一個(gè)Word處理接口,這里的實(shí)現(xiàn)是Microsoft.Office.Interop.Word)
考慮到轉(zhuǎn)換器在轉(zhuǎn)換過程Convert中,會(huì)產(chǎn)生一系列的步驟,首先對(duì)于轉(zhuǎn)換這個(gè)過程進(jìn)行細(xì)化,分解成各個(gè)步驟:
代碼
public abstract class BasePaperConvertor<TIn, TOut> : IPaperConvertor<TIn, TOut>
where TIn : class, new()
where TOut : class, new()
{
//成員
/// <summary>
/// 輸出試卷實(shí)體
/// </summary>
protected TOut Paper { get; set; }
/// <summary>
/// 輸入Word條件
/// </summary>
protected TIn WordInfo { get; set; }
#region Word操作實(shí)體屬性
/// <summary>
/// Word操作實(shí)體屬性
/// </summary>
protected IWordHelper WordHelper { get; set; }
#endregion
//公共方法
/// <summary>
/// 轉(zhuǎn)換方法
/// </summary>
/// <param name="tIn"></param>
/// <returns></returns>
public virtual TOut Convert(TIn tIn, IWordHelper helper)
{
WordHelper = helper;
WordInfo = tIn;
Paper = Initialize(tIn);
if (Prepare())
Execute();
Finished();
return Paper;
}
//抽象方法
/// <summary>
/// 初始化
/// </summary>
/// <param name="tIn"></param>
/// <returns></returns>
protected abstract TOut Initialize(TIn tIn);
/// <summary>
/// 預(yù)裝載
/// </summary>
/// <param name="tOut"></param>
/// <returns></returns>
protected abstract bool Prepare();
/// <summary>
/// 執(zhí)行
/// </summary>
/// <param name="tOut"></param>
protected abstract void Execute();
/// <summary>
/// 完成
/// </summary>
protected abstract void Finished();
}
從代碼中,我們可以看到Convert方法中調(diào)用了一系列的抽象方法,首先對(duì)于輸入類型進(jìn)行初始化(Initialize),接著通過輸入類型預(yù)裝載(Prepare),如果預(yù)裝載成功,并開始執(zhí)行。最后完成(Finished)所有的工作。
接著,需要定義一個(gè)包含Word解析邏輯代碼的抽象類,這里使用Microsoft.Office.Interop.Word進(jìn)行Office編程,于是創(chuàng)建名為
OfficeWordPaperConvertor.cs的類:
OfficeWordPaperConvertor
/// <summary>
/// 試卷解析器泛型抽象類
/// </summary>
public abstract class OfficeWordPaperConvertor<TIn, TQuestion, TOut> : BasePaperConvertor<TIn, TOut>
where TIn : class, new()
where TQuestion : class, new()
where TOut : class, new()
{
#region 試卷Word結(jié)構(gòu)信息
/// <summary>
/// 試卷Word結(jié)構(gòu)信息
/// </summary>
protected PaperWordInfo PaperWordInfo { get; private set; }
#endregion
#region Word操作輔助類屬性
private OfficeWordHelper _OfficeWordHelper;
/// <summary>
/// Word操作輔助類屬性
/// </summary>
protected OfficeWordHelper OfficeWordHelper
{
get
{
if (_OfficeWordHelper == null)
_OfficeWordHelper = GetWordHelper();
return _OfficeWordHelper;
}
}
#endregion
#region 預(yù)處理試卷
/// <summary>
/// 預(yù)處理試卷
/// </summary>
/// <param name="tOut"></param>
/// <returns></returns>
protected override bool Prepare()
{
//過濾試卷無(wú)效信息
FilterPaper();
//解析試卷
ParsePaper();
return true;
}
#endregion
#region 執(zhí)行試卷
/// <summary>
/// 執(zhí)行試卷
/// </summary>
/// <param name="tOut"></param>
protected override void Execute()
{
for (int i = 0; i < PaperWordInfo.Count; i++)
{
QuestionWordInfo questionWordInfo = PaperWordInfo[i];
//執(zhí)行試題
ExcuteQuestion(questionWordInfo);
}
}
#endregion
#region 完成時(shí)調(diào)用
/// <summary>
/// 完成時(shí)調(diào)用
/// </summary>
protected override void Finished()
{
//這里進(jìn)行完成時(shí)調(diào)用的實(shí)現(xiàn)
//..
}
#endregion
//虛方法
/// <summary>
/// 過濾試卷無(wú)效信息
/// </summary>
protected virtual void FilterPaper()
{
}
/// <summary>
/// 解析試卷
/// </summary>
protected virtual void ParsePaper()
{
PaperWordInfo = new PaperWordInfo();
//通過計(jì)算 OfficeWordHelper.Document.Text 得到文本中的題目數(shù),這里省去這段邏輯
PaperWordInfo.AddQuestion(new QuestionWordInfo { StartIndex = 0, EndIndex = 0 });
PaperWordInfo.AddQuestion(new QuestionWordInfo { StartIndex = 1, EndIndex = 1 });
PaperWordInfo.AddQuestion(new QuestionWordInfo { StartIndex = 2, EndIndex = 2 });
}
/// <summary>
/// 執(zhí)行試題
/// </summary>
/// <param name="questionWordInfo"></param>
protected virtual void ExcuteQuestion(QuestionWordInfo questionWordInfo)
{
string[] array = OfficeWordHelper.Document.Text.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
//創(chuàng)建試題解析器實(shí)體
TQuestion question = CreateQuestionConvertor(WordInfo, array[questionWordInfo.StartIndex]);
//將試題添加到試卷中
if (question != null) AddQuestion(question);
}
#region 獲取Word工具類
/// <summary>
/// 獲取Word工具類
/// </summary>
/// <returns></returns>
protected OfficeWordHelper GetWordHelper()
{
return WordHelper as OfficeWordHelper;
}
#endregion
//抽象方法
/// <summary>
/// 創(chuàng)建試題解析器實(shí)體
/// </summary>
/// <param name="subject"></param>
protected abstract TQuestion CreateQuestionConvertor(TIn tIn, string wordContent);
/// <summary>
/// 將試題添加到試卷中
/// </summary>
/// <param name="tPart"></param>
/// <param name="tQuestion"></param>
protected abstract void AddQuestion(TQuestion tQuestion);
}
為何這里沒有重寫Initialize方法呢?由于這里需要將Initialize暴露于業(yè)務(wù)代碼中,可以通過業(yè)務(wù)代碼來重寫該方法,如果業(yè)務(wù)組件沒有調(diào)用Initialize,將報(bào)錯(cuò)。
這里Prepare方法主要完成一份Word文檔的信息過濾,并且將文檔中按照試題題號(hào)進(jìn)行拆分試題,形成試題列表。
Execute方法完成一份試卷的執(zhí)行,通過試題列表將題目逐題入庫(kù)。
Finshed方法在Execute之后,可通過事件委托告訴用戶解析已經(jīng)完成。
在后面附加的例子中,我會(huì)引用OfficeWordHelper.Document.Text 等于“1.試題1\r\n2.試題2\r\n3.試題3”的文本字符串來模擬Word文檔中的文字(實(shí)際情況更
加復(fù)雜,Word文檔中包括圖片,符號(hào),OLE對(duì)象等等,一切為了簡(jiǎn)化說明,這里省略該步驟),說明它拆分出來的試題有3道。QuestionWordInfo 類的
StartIndex,EndIndex對(duì)應(yīng)試題所在行數(shù)索引。
接著注意ExcuteQuestion這個(gè)方法,調(diào)用了CreateQuestionConvertor和AddQuestion兩個(gè)抽象方法。該兩個(gè)抽象方法將在業(yè)務(wù)組件中實(shí)現(xiàn)。
試卷解析器基本設(shè)計(jì)實(shí)現(xiàn)了,現(xiàn)在看下試題解析器該如何實(shí)現(xiàn):
聲明一個(gè)試題解析器范型接口:
/// <summary>
/// 試題轉(zhuǎn)換器泛型接口
/// </summary>
public interface IQuestionConvertor<TIn, TOut>
{
TOut Convert(TIn tIn, string wordContent);
}
其中TIn類型作為輸入類型,TOut類型作為輸出類型(TIn將來作為業(yè)務(wù)代碼中實(shí)際的輸入類型,如WordInfo類;TOut作為實(shí)際輸出類型,如QuestionInfo類)
考慮到轉(zhuǎn)換器在轉(zhuǎn)換過程Convert中,會(huì)產(chǎn)生一系列的步驟,首先對(duì)于轉(zhuǎn)換這個(gè)過程進(jìn)行細(xì)化,分解成各個(gè)步驟:
代碼
public abstract class BaseQuestionConvertor<TIn, TOut> : IQuestionConvertor<TIn, TOut> where TIn : class, new()
{
//成員
#region 輸出試卷屬性
/// <summary>
/// 輸出試卷實(shí)體
/// </summary>
protected TOut Question { get; set; }
#endregion
#region 輸入Word實(shí)體屬性
/// <summary>
/// 輸入Word實(shí)體屬性
/// </summary>
protected TIn WordInfo { get; set; }
#endregion
//公共方法
#region 轉(zhuǎn)換方法
/// <summary>
/// 轉(zhuǎn)換方法
/// </summary>
/// <param name="tIn"></param>
/// <param name="helper"></param>
/// <returns></returns>
public virtual TOut Convert(TIn tIn, string wordContent)
{
WordInfo = tIn;
Question = Initialize(tIn);
//解析試題
TOut tOut = Execute(wordContent);
//完成
Finished();
return tOut;
}
#endregion
//抽象方法
#region 初始化
/// <summary>
/// 初始化
/// </summary>
/// <param name="tIn"></param>
/// <returns></returns>
protected abstract TOut Initialize(TIn tIn);
#endregion
#region 執(zhí)行
/// <summary>
/// 執(zhí)行
/// </summary>
/// <param name="tOut"></param>
protected abstract TOut Execute(string wordContent);
#endregion
#region 完成
/// <summary>
/// 完成
/// </summary>
protected abstract void Finished();
#endregion
}
接著,需要定義一個(gè)包含Word解析邏輯代碼的抽象類,這里使用Microsoft.Office.Interop.Word進(jìn)行Office編程,于是創(chuàng)建名為
OfficeWordQuestionConvertor.cs的類:
OfficeWordQuestionConvertor /// <summary>
/// 試題解析器泛型抽象類
/// </summary>
public abstract class OfficeWordQuestionConvertor<TIn, TOut> : BaseQuestionConvertor<TIn, TOut>
where TIn : class, new()
where TOut : class, new()
{
protected override TOut Execute(string wordContent)
{
ParseQuestionContent(wordContent);
ParseDifficultyCode(wordContent);
//...其他解析屬性,這里省略
return Question;
}
#region 解析試題題干
/// <summary>
/// 解析試題題干
/// </summary>
/// <returns></returns>
protected virtual void ParseQuestionContent(string questionText)
{
//通過questionText解析出試題提干,這里省略
string content = questionText;
SetQuestionContent(content);
}
#endregion
#region 解析試題難度
/// <summary>
/// 解析試題難度
/// </summary>
/// <param name="questionText"></param>
/// <returns></returns>
protected virtual void ParseDifficultyCode(string questionText)
{
//通過questionText解析出難度文本,這里省略
string difficulty = "A";
SetDifficultyCode(difficulty);
}
#endregion
//抽象方法
/// <summary>
/// 設(shè)置試題標(biāo)題
/// </summary>
/// <param name="text"></param>
protected abstract void SetQuestionContent(string text);
/// <summary>
/// 設(shè)置試題難度
/// </summary>
/// <param name="difficulty"></param>
protected abstract void SetDifficultyCode(string difficulty);
}
Execute方法通過Word文本內(nèi)容解析相應(yīng)試題的屬性(如題干、難度、是否系統(tǒng)試題等)。
于是這里抽象出了兩個(gè)方法(按照需求來進(jìn)行方法擴(kuò)展),SetQuestionContent和SetDifficultyCode將在業(yè)務(wù)組件中實(shí)現(xiàn)。
3. 現(xiàn)在開始創(chuàng)建其他項(xiàng)目,如下圖所示:
其中WebApp為B/S平臺(tái)項(xiàng)目,WebApp.Lib為B/S平臺(tái)業(yè)務(wù)類庫(kù),兩個(gè)項(xiàng)目均采用.net framework 3.5;WinApp為C/S平臺(tái)項(xiàng)目,WinApp.Lib為C/S業(yè)務(wù)類庫(kù);
注意到,WebApp.Lib和WinApp.Lib在數(shù)據(jù)實(shí)體類上存在差異(實(shí)際情況差異更大,不僅僅數(shù)據(jù)實(shí)體類上,這里為了簡(jiǎn)化),兩個(gè)項(xiàng)目均采用.net framework 2.0;
WordConvertor即為上面說的解析器組件。
以WebApp.Lib為例,實(shí)現(xiàn)業(yè)務(wù)試卷和試題解析器:
WebPaperConvertor .cs:
代碼
/// <summary>
/// Web端試卷解析器
/// </summary>
public class WebPaperConvertor : OfficeWordPaperConvertor<WordInfo, QuestionInfo, PaperInfo>
{
/// <summary>
/// 初始化試卷
/// </summary>
protected override PaperInfo Initialize(WordInfo wordInfo)
{
Paper = new PaperInfo();
Paper.Title = wordInfo.PaperTitle;
return Paper;
}
/// <summary>
/// 創(chuàng)建試題解析器
/// </summary>
protected override QuestionInfo CreateQuestionConvertor(WordInfo wordInfo, string wordContent)
{
WebQuestionConvertor convertor = new WebQuestionConvertor();
return convertor.Convert(wordInfo, wordContent);
}
/// <summary>
/// 增加試題
/// </summary>
protected override void AddQuestion(QuestionInfo tQuestion)
{
if(Paper.QuestionInfoList == null)
Paper.QuestionInfoList = new List<QuestionInfo>();
Paper.QuestionInfoList.Add(tQuestion);
}
//其他業(yè)務(wù)擴(kuò)展...
}
WebQuestionConvertor .cs:
代碼
/// <summary>
/// Web端試題解析器
/// </summary>
public class WebQuestionConvertor : OfficeWordQuestionConvertor<WordInfo, QuestionInfo>
{
/// <summary>
/// 根據(jù)條件初始化試題
/// </summary>
protected override QuestionInfo Initialize(WordInfo wordInfo)
{
QuestionInfo questionInfo = new QuestionInfo();
questionInfo.IsSystem = wordInfo.IsSystem;
return questionInfo;
}
/// <summary>
/// 完成解析后觸發(fā)
/// </summary>
protected override void Finished()
{
}
/// <summary>
/// 設(shè)置試題題干
/// </summary>
protected override void SetQuestionContent(string text)
{
Question.QuestionContent = text;
}
/// <summary>
/// 設(shè)置試題難度
/// </summary>
protected override void SetDifficultyCode(string difficulty)
{
switch (difficulty)
{
case "A":
Question.DifficultyCode = 1;
break;
case "B":
Question.DifficultyCode = 2;
break;
case "C":
Question.DifficultyCode = 3;
break;
}
}
//其他業(yè)務(wù)擴(kuò)展...
}
從類中可以看出,它們分別繼承于OfficeWordPaperConvertor和OfficeWordQuestionConvertor類,這里實(shí)現(xiàn)的只是和平臺(tái)相關(guān)的業(yè)務(wù)邏輯,至于如何對(duì)一份Word文檔解析,交給解析器組件去做,平臺(tái)上無(wú)需知道。
同理,C/S平臺(tái)也用了類似的方法,不同的只是個(gè)別類型通過泛型抽象類得到實(shí)現(xiàn)。并且能夠使B/S平臺(tái)和C/S平臺(tái)擁有各自的業(yè)務(wù)邏輯。
這樣,維護(hù)兩個(gè)平臺(tái)的這個(gè)功能成本降低了,如果解析器組件需要改動(dòng),只要更動(dòng)基礎(chǔ)組件的設(shè)計(jì),而不會(huì)影響業(yè)務(wù)上的邏輯。
這是Leepy同學(xué)在開發(fā)項(xiàng)目的時(shí)候遇到的問題,可以說是提供了一種思路吧,也可以算是經(jīng)驗(yàn)之談吧:)
在591up以及客戶端的功能效果如下圖所示:
591up 客戶端軟件
最后附上該范例的Demo