设计场景
1. 有A,B两组开发人员进行某个系统的开发,其中A组开发人员负责B/S平台的功能设计与开发,B组开发人员负责C/S平台的功能设计与开发。
2. 在当时的项目背景下,B/S端的项目是先启动的,而A组的开发人员还没有意识到将来需要配合C/S端来做功能协作,因此产生的问题就是,前期的系统架构设计没有过多地考虑以适应多个平台下的功能适应性。当然,从B/S端的设计角度上看,系统架构还算比较清晰。接着A组的开发人员就在这样的情况下,完成了系统功能的实现。
3. 接着高层领导告诉项目经理需要做一套C/S架构的软件来配合B/S端平台的使用,而这时候B/S端的功能实现已经基本完成,B组开发人员成立。
4. 在B组架构人员开始设计架构的时候,并没有衍用B/S端的开发架构,很多基础架构(如分层模式、数据库结构、数据实体类等等)都存在很大的差异(C/S端项目在初期的要求没有那么高,有的功能能削减掉就削减掉),后来B组架构人员发现需求文档上的有个功能和B/S平台上的某个功能是一样的,于是他和A组架构人员进行交流,希望负责B/S平台上这个功能的开发人员能够帮助C/S平台帮助完成这一功能。于是A组的Leepy就匆匆忙忙地上阵了。
5. 最初Leepy同学因为在B/S平台上也有大量的任务需要完成,任务赶得狠,又收到这样一个“功能复制”的任务,心想:“那么就先把功能复制一份上去,然后如果B/S平台上的功能有更新,就同步修改C/S平台就好”。于是打开C/S平台的项目,发现和B/S平台项目的差异性比较大,包括数据库结构和数据实体类等等,更头疼的是这里采用的是.net framework 2.0进行开发,而B/S端采用的是.net framework 3.5进行开发,而且从功能上,Leepy使用大量的3.5的属性。要直接复用是不可能的,还需要调整相应的代码。
6. 于是C/S平台该功能出来了,运行得还行。现在才是郁闷的开始,因为该功能属于平台的核心模块,于是B/S平台上要时刻调整得比较大,所以同步的C/S端的功能也要相应的调整,然后又运行完好。于是问题出来了,这样反复地修改导致系统(C/S和B/S)维护成本很高,架构间的设计耦合度太大。刚开始Leepy抱怨为什么C/S端没有和B/S端统一架构,至少底层基础平台能够设计得具有可扩展性,光光抱怨无法解决问题,因为这是项目的人员配置的问题。于是,Leepy想到了必须对该功能进重构,使用一个通用的组件进行抽象,而实际实现的,如C/S、B/S端具体应用,只要维护相应的业务代码。
设计思路
1. 说完场景,现在说说动手的部分。以一个中学生教育平台591up的网站为例,以及教育平台客户端的辅助软件。
这一功能实现一份Word文档试卷的导入保存并分解文档中的试题,将试题逐个保存入库(解析出来的试题部分还包括很多属性,如答案、知识点、解题关键点等很多属性)。现在B/S平台和C/S平台都需要这个功能,但是B/S平台和C/S平台下的相关数据库实体类,设计不很统一,导致维护系统的成本很高。于是,考虑是否能将解析器的设计与业务功能分开,将试卷解析器设计成通用的组件,而与B/S端和C/S端的业务代码彻底分开,对于解析的逻辑代码(基础代码)在两端都可以引用到,而B/S端和C/S端所需要做得就是调整业务代码,并不需要关解析的基础代码是什么,组件与业务代码解耦。如下图所示:
2. 现在讲讲具体设计思路,先从试卷解析器基础组件开始(为了简化,该范例是削弱版的),创建一个.net 2.0的类库(为了适应客户端.net 2.0的配置)声明一个试卷解析器范型接口:
代码
/// <summary>
/// 试卷转换器泛型接口
/// </summary>
public interface IPaperConvertor<TIn, TOut>
{
/// <summary>
/// 转换方法
/// </summary>
/// <param name="tIn">转换输入类型</param>
/// <param name="helper">Word处理接口</param>
/// <returns>转换输出类型</returns>
TOut Convert(TIn tIn, IWordHelper helper);
}
其中TIn类型作为输入类型,TOut类型作为输出类型(TIn将来作为业务代码中实际的输入类型,如WordInfo类;TOut作为实际输出类型,如PaperInfo类;IWordHelper为一个Word处理接口,这里的实现是Microsoft.Office.Interop.Word)
考虑到转换器在转换过程Convert中,会产生一系列的步骤,首先对于转换这个过程进行细化,分解成各个步骤:
代码
public abstract class BasePaperConvertor<TIn, TOut> : IPaperConvertor<TIn, TOut>
where TIn : class, new()
where TOut : class, new()
{
//成员
/// <summary>
/// 输出试卷实体
/// </summary>
protected TOut Paper { get; set; }
/// <summary>
/// 输入Word条件
/// </summary>
protected TIn WordInfo { get; set; }
#region Word操作实体属性
/// <summary>
/// Word操作实体属性
/// </summary>
protected IWordHelper WordHelper { get; set; }
#endregion
//公共方法
/// <summary>
/// 转换方法
/// </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>
/// 预装载
/// </summary>
/// <param name="tOut"></param>
/// <returns></returns>
protected abstract bool Prepare();
/// <summary>
/// 执行
/// </summary>
/// <param name="tOut"></param>
protected abstract void Execute();
/// <summary>
/// 完成
/// </summary>
protected abstract void Finished();
}
从代码中,我们可以看到Convert方法中调用了一系列的抽象方法,首先对于输入类型进行初始化(Initialize),接着通过输入类型预装载(Prepare),如果预装载成功,并开始执行。最后完成(Finished)所有的工作。
接着,需要定义一个包含Word解析逻辑代码的抽象类,这里使用Microsoft.Office.Interop.Word进行Office编程,于是创建名为
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结构信息
/// <summary>
/// 试卷Word结构信息
/// </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 预处理试卷
/// <summary>
/// 预处理试卷
/// </summary>
/// <param name="tOut"></param>
/// <returns></returns>
protected override bool Prepare()
{
//过滤试卷无效信息
FilterPaper();
//解析试卷
ParsePaper();
return true;
}
#endregion
#region 执行试卷
/// <summary>
/// 执行试卷
/// </summary>
/// <param name="tOut"></param>
protected override void Execute()
{
for (int i = 0; i < PaperWordInfo.Count; i++)
{
QuestionWordInfo questionWordInfo = PaperWordInfo[i];
//执行试题
ExcuteQuestion(questionWordInfo);
}
}
#endregion
#region 完成时调用
/// <summary>
/// 完成时调用
/// </summary>
protected override void Finished()
{
//这里进行完成时调用的实现
//..
}
#endregion
//虚方法
/// <summary>
/// 过滤试卷无效信息
/// </summary>
protected virtual void FilterPaper()
{
}
/// <summary>
/// 解析试卷
/// </summary>
protected virtual void ParsePaper()
{
PaperWordInfo = new PaperWordInfo();
//通过计算 OfficeWordHelper.Document.Text 得到文本中的题目数,这里省去这段逻辑
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>
/// 执行试题
/// </summary>
/// <param name="questionWordInfo"></param>
protected virtual void ExcuteQuestion(QuestionWordInfo questionWordInfo)
{
string[] array = OfficeWordHelper.Document.Text.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
//创建试题解析器实体
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>
/// 创建试题解析器实体
/// </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暴露于业务代码中,可以通过业务代码来重写该方法,如果业务组件没有调用Initialize,将报错。
这里Prepare方法主要完成一份Word文档的信息过滤,并且将文档中按照试题题号进行拆分试题,形成试题列表。
Execute方法完成一份试卷的执行,通过试题列表将题目逐题入库。
Finshed方法在Execute之后,可通过事件委托告诉用户解析已经完成。
在后面附加的例子中,我会引用OfficeWordHelper.Document.Text 等于“1.试题1\r\n2.试题2\r\n3.试题3”的文本字符串来模拟Word文档中的文字(实际情况更
加复杂,Word文档中包括图片,符号,OLE对象等等,一切为了简化说明,这里省略该步骤),说明它拆分出来的试题有3道。QuestionWordInfo 类的
StartIndex,EndIndex对应试题所在行数索引。
接着注意ExcuteQuestion这个方法,调用了CreateQuestionConvertor和AddQuestion两个抽象方法。该两个抽象方法将在业务组件中实现。
试卷解析器基本设计实现了,现在看下试题解析器该如何实现:
声明一个试题解析器范型接口:
/// <summary>
/// 试题转换器泛型接口
/// </summary>
public interface IQuestionConvertor<TIn, TOut>
{
TOut Convert(TIn tIn, string wordContent);
}
其中TIn类型作为输入类型,TOut类型作为输出类型(TIn将来作为业务代码中实际的输入类型,如WordInfo类;TOut作为实际输出类型,如QuestionInfo类)
考虑到转换器在转换过程Convert中,会产生一系列的步骤,首先对于转换这个过程进行细化,分解成各个步骤:
代码
public abstract class BaseQuestionConvertor<TIn, TOut> : IQuestionConvertor<TIn, TOut> where TIn : class, new()
{
//成员
#region 输出试卷属性
/// <summary>
/// 输出试卷实体
/// </summary>
protected TOut Question { get; set; }
#endregion
#region 输入Word实体属性
/// <summary>
/// 输入Word实体属性
/// </summary>
protected TIn WordInfo { get; set; }
#endregion
//公共方法
#region 转换方法
/// <summary>
/// 转换方法
/// </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 执行
/// <summary>
/// 执行
/// </summary>
/// <param name="tOut"></param>
protected abstract TOut Execute(string wordContent);
#endregion
#region 完成
/// <summary>
/// 完成
/// </summary>
protected abstract void Finished();
#endregion
}
接着,需要定义一个包含Word解析逻辑代码的抽象类,这里使用Microsoft.Office.Interop.Word进行Office编程,于是创建名为
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>
/// 设置试题标题
/// </summary>
/// <param name="text"></param>
protected abstract void SetQuestionContent(string text);
/// <summary>
/// 设置试题难度
/// </summary>
/// <param name="difficulty"></param>
protected abstract void SetDifficultyCode(string difficulty);
}
Execute方法通过Word文本内容解析相应试题的属性(如题干、难度、是否系统试题等)。
于是这里抽象出了两个方法(按照需求来进行方法扩展),SetQuestionContent和SetDifficultyCode将在业务组件中实现。
3. 现在开始创建其他项目,如下图所示:
其中WebApp为B/S平台项目,WebApp.Lib为B/S平台业务类库,两个项目均采用.net framework 3.5;WinApp为C/S平台项目,WinApp.Lib为C/S业务类库;
注意到,WebApp.Lib和WinApp.Lib在数据实体类上存在差异(实际情况差异更大,不仅仅数据实体类上,这里为了简化),两个项目均采用.net framework 2.0;
WordConvertor即为上面说的解析器组件。
以WebApp.Lib为例,实现业务试卷和试题解析器:
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>
/// 创建试题解析器
/// </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);
}
//其他业务扩展...
}
WebQuestionConvertor .cs:
代码
/// <summary>
/// Web端试题解析器
/// </summary>
public class WebQuestionConvertor : OfficeWordQuestionConvertor<WordInfo, QuestionInfo>
{
/// <summary>
/// 根据条件初始化试题
/// </summary>
protected override QuestionInfo Initialize(WordInfo wordInfo)
{
QuestionInfo questionInfo = new QuestionInfo();
questionInfo.IsSystem = wordInfo.IsSystem;
return questionInfo;
}
/// <summary>
/// 完成解析后触发
/// </summary>
protected override void Finished()
{
}
/// <summary>
/// 设置试题题干
/// </summary>
protected override void SetQuestionContent(string text)
{
Question.QuestionContent = text;
}
/// <summary>
/// 设置试题难度
/// </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;
}
}
//其他业务扩展...
}
从类中可以看出,它们分别继承于OfficeWordPaperConvertor和OfficeWordQuestionConvertor类,这里实现的只是和平台相关的业务逻辑,至于如何对一份Word文档解析,交给解析器组件去做,平台上无需知道。
同理,C/S平台也用了类似的方法,不同的只是个别类型通过泛型抽象类得到实现。并且能够使B/S平台和C/S平台拥有各自的业务逻辑。
这样,维护两个平台的这个功能成本降低了,如果解析器组件需要改动,只要更动基础组件的设计,而不会影响业务上的逻辑。
这是Leepy同学在开发项目的时候遇到的问题,可以说是提供了一种思路吧,也可以算是经验之谈吧:)
在591up以及客户端的功能效果如下图所示:
591up 客户端软件
最后附上该范例的Demo