冬Blog

醉心技术、醉心生活
  博客园  :: 首页  :: 新随笔  :: 订阅 订阅  :: 管理

描述与实现——系统构建的顺序

Posted on 2006-10-28 08:58  冬冬  阅读(1706)  评论(6编辑  收藏  举报

 

1引言

很多人说在做一个系统的时候不知道从何下手,就是有了需求也不知道该怎么做。通常的思路是分析,设计,然后编码。分工也就是你进行数据库的开发,我做GUI,有些人写存储过程。这种方式有一个前提,就是分析和设计必须做的很好,构架很健壮,而且设计做得很细,甚至有些时候要求到代码级。

但是即便如此,项目仍然可能是失败的,原因是因为代码其实也是设计1,会对构架等有反作用,很多分析和设计时期根本想不到的问题,只有到编码的时候才能被发现。

那么,面对一个项目,应该从哪里下手呢?

2永远不够的设计

设计,当然是设计。没有设计哪儿来的编码?就算是极限编程、TDD4等。其实都是先有设计的。当然,设计的意义随着项目的不同而不同。

越大的项目、其设计工作应该做的越细致一些。这和建筑学很相似5:相信三峡大坝的设计工作一定是做得非常细致,而且是经过了充分的验证以后的。但是如果是我给我们家的狗狗搭个窝,我相信没有必要用AutoCAD去画图。

以上观点又有引起了另一个问题:多少设计才够?我的观点是:没有设计是足够的,做到你做不下去的时候就可以了。

所谓的做不下去了,一种情况是缺乏实践的支持,不知道所做的设计是不是符合要求、能不能实现。这个时候需要进行一些编码工作,搭建一个快速原型进行验证。或者干脆从别的方面下手,把进行不下去的地方先放一放。比如说数据库的设计不知道该做什么,不妨去明确以下业务逻辑,客户业务条理不清晰,就去搞搞GUI,摆摆控件。要知道系统是一个整体,任何一部分对其他部分都是有联系的。对系统其他部分的思考或许会帮你打开思路,攻克难关。但是需要注意的是,搞设计需要有一个主线,不能东一下西一下。大体是要沿着需求—〉业务逻辑—〉GUI设计—〉数据持久化这么个顺序。

还有一种做不下去的情况是人员的心理原因。有个时候程序员更喜欢敲代码、而不是写文档。这也是很正常的。如果说非要程序员去拧着性子讲究敬业精神去写文档而不去写代码,是一种不科学的做法。做软件是脑力劳动,开发人员的活力和激情是很重要的。所以有的时候设计做不下去了,转而去编码,可以缓解很大压力,调动积极性,同时也是对设计的一种检验。可以说是一举两得。

3用代码描述系统——业务逻辑

3.1什么是业务逻辑

很多人说:我知道要做设计,但是我不知道怎么做设计。

做设计没有一个固定的套路,不然也就称不上设计了。编码也是设计,因为编码也没有什么套路,不然算法生成早就淘汰了所有的程序员。

虽然套路没有,但是思路还是有的。构架的设计顺序上面我已经说过了:需求—〉业务逻辑—〉GUI设计—〉数据持久化,这其实也就是先规定想要做什么,然后再考虑怎样去做。

从需求出业务逻辑是最重要,也是最难的一个环节。

什么是业务逻辑?业务逻辑,是对系统行为的定义和描述,即用代码或其它设计性的语言描述系统的行为,规定系统应该做什么。只是这么说太抽象了,我们下面举个例子。

依旧是我的FtpSearch搜索引擎。需求我就不说了,就是普通的搜索引擎:抓取、索引、查询等,详细情况可以参考我的另一篇文章http://yuandong.cnblogs.com/archive/2006/06/26/436395.html。本文的重点不是搜索引擎技术,而是系统的实现顺序。先看一下其Solution中Project的层次结构(图一和图二):


(图一:Solution中的Project)

(图二:系统的依赖关系,箭头表示依赖)

一眼望去,相当复杂,但是我想要描述的不是一个已经实现的系统,而是系统实现的顺序。以上的这些包,是按照什么样的顺序设计和实现的呢?答案是系统各部分的依赖的顺序。

3.2系统各部分的依赖顺序

所谓的依赖说白了就是调用csc编译器时Ref参数后面的东东,在Vs.net中就是Add Reference中添加的DLL。从图中可以很明显的看出项目中包的依赖关系,即:实现依赖于逻辑,UI依赖于系统的其他所有部分。


(图三:系统的顶层依赖关系)

按照依赖的关系,系统可以分为逻辑、实现、表现三大部分。FtpSearch系统中逻辑层包含:Kernel、SearchBusiness、QueryBusiness。表现层包含:SearchApplication(Console控制台程序)、WebSite。实现层包含其它的包。

先说Kernel,这是这个比较特殊的包,它被系统的所有部分依赖。其内容的是系统对现实世界的映射和抽象,非常的简单。如下图:


(图四:Kernel包的内容)

Kernel包中一共有五个类。Server表示的是要被搜索的Ftp服务器。FtpItem是抽象类。表示Server上的一个项目。FtpDir和FtpFile继承于FtpItem,分别表示Server上的目录和文件。FileSize是一个辅助类,用于计算文件大小的便于阅读的表示。

SearchBusiness和QueryBusiness即是对抓取逻辑和查询逻辑的定义,也就是系统的核心逻辑,描述了系统应该实现的两个大模块的功能。其内容如下:


(图五:SearchBusiness包的内容)


(图六:QueryBusiness包的内容)

SearchBusiness包含了四个事件(参数),三个需要被实现的接口,一个枚举,一个具体类。QueryBusiness包含了一个需要备实现的接口,Query类用于封装查询信息,QueryResultResultFile类用于封装查询结果。

以上就是业务逻辑,即用代码描述的需求。其中最重要的就是四个接口,他们描述了这个系统的实现部分应该具有的功能。系统的实现部分就是实现这些接口的类。而表现层则根据这些接口调用实现类。另外,单元测试也是根据这些接口构建脚手架的。接口同时具有定义和解耦合两个作用。

以下是两个模块中四个接口的定义。


(图七:接口的定义)

从图上可以看出,接口IRobot定义了抓取机器人的功能,包括启动的方法和可能触发的事件。IQueryServices比较简单,定义的查询服务只要实现Search这个方法就可以了。其它的两个接口则是为他们服务的。可以看出,这四个接口规定了系统的功能。

在设计和构造一个系统的时候,最首要的问题也就使描述系统的逻辑。这些逻辑直接来源于需求和设计者对是系统的认识。这些认识包含系统的规模、构架、要采用的技术、模块间的耦合等。

用代码描述的逻辑,相对于文档、UML等,更具有表现力,也更明确、清晰。一个好的系统设计必有一个好的描述,它起到了整个系统指引方向的作用。

如何用代码描述逻辑是一件需要不断学习和锻炼的事情。很多人,特别是从Asp等系统的开发过来的人,往往不知道该怎么分层,或者一般就是分为两层,表现层调用数据访问层。这种情况下,很多逻辑就散落在系统的很多角落里。这种系统没有一个良好的抽象,没有完整的定义,同时也是紧密耦合、不便于测试的。这种设计不能满足大型项目的需要,同时对维护来说也是一种噩梦。

3.3根据逻辑实现功能

一旦有了明确的逻辑定义,可以说系统的实现就是水到渠成的事情了。

相对于业务逻辑来说,系统的实现是细节,细节依赖于抽象,是理所应当的事情。例如数据持久化、用户界面等,都属于细节。如果我们在改变一个按钮在界面上的位置会导致访问数据的操作进行修改,那显然是很可笑的。同样,修改数据库的同时可能会修改逻辑操作的代码,这也是不对的。对于逻辑来说,它定义并拥有接口,规定了系统的其他部分应该怎样运转。其原因就是业务逻辑是对需求的体现。需求变,则逻辑变,逻辑变则系统皆变。

另外,对于细节,一般推迟实现。比如,做构架的时候不会很具体的考虑到数据库中有多少个表,每个表格包含哪些字段。因为业务逻辑还不明确,更重要的事,系统不因为这些因素而变化。这些细节处于从属地位。

回到我们FtpSearch上来。

实现SearchBusiness的包是SearchEngineDataAccessEdtFtpAccess。其中SearchEngine中的Robot类实现了SearchBusinessIRobot接口,视为系统的核心。同样,SearchEngine中的IFtpAccessorIFtpAccessorFactory这两个接口定义了它需要的服务,EdtFtpAccess是一个代理模式,它依赖于SearchEngine和第三方的APIEdtFtpNetEdtFtpAccess通过实现IFtpAccessorIFtpAccessorFactory接口为SearchEngine提供服务。这是更低一层次的“描述与实现”的关系。

以下是这部分的图示:包含SearchEngineEdtFtpAccess两个包的内容。


(图八:
Search功能的实现)

Query部分就更简单了。只是单单提供了一个查询操作。由于系统的索引采用的是Lucene,所以查询的实现简单的用Proxy模式封装以下就可以了,以至于没有某个包是为了实现QueryBusiness而存在的。数据持久层就全权代理了。

3.4数据持久化与业务逻辑的封装

系统的设计和开发进行的这里,我们才开始真正的着手考虑数据持久层。数据持久层怎样实现?用AccessSqlServerLucene?还是自己写?都可以!反正只要能实现业务逻辑规定的接口,从理论上来说,系统就可以运作,不同之处是哪一种实现方式更合适一些。

这里我们不讨论搜索引擎技术,只是说用Lucene显然更合适。不过不管用什么,都不应该让你的数据持久化的方式入侵到你的业务逻辑。比如说我这里用了Lucene,而且我知道这个著名的开源索引肯定会出新的版本。难道说当它出新的版本的时候,我的整个系统都要跟着它改?当然不行!所以,依赖于它是错误的。我们需要将第三方的组件对系统的影响减少到最小。Proxy模式3是最合适的选择,它将Lucene对整个系统的影响完全局限的在DataAccess一个包中。对于系统的其他部分,他们根本不知道、也没有必要知道数据持久化的细节。查询操作亦是如此。Proxy模式的作用在这里发挥得淋漓尽致。

以下是DataAccess包的内容:


(图九:
DataAccess包的内容)

可以看出来,DataAccess中的类完全是为了实现接口而设计的。

除了DataAccess,还有一个包:SearchFacade,故名思义,这个包的作用是封装整个搜索逻辑,对表现层隐藏了大量的细节。这个包的主要内容如下:


(图十:
SearchFacade包的内容)

可以看到,这个包中最主要的类就是SearchManager类。表现层需要了解的就是这一个类。这就大大地简化了表现层的复杂度。

以上就所有系统的实现。但是只有实现还是不够的,还需要把他们整合并驱动起来,这也就是表现层的作用。

3.5表现层

表现层最主要的两个作用有两个:整合和驱动。

表现层的方式可以是多种多样的:命令行、窗体、Website……。同样,表现层也应该依赖于业务逻辑,因为表现层的职责是就在用户和业务逻辑之间建立一座桥梁。

回到我们的FtpSearch上来。这个系统的用户有两方面:一是管理员,负责更新数据;二是普通用户,使用系统查询数据。数据的更新显然应该是自动化的,所有采用命令行的方式。而前台采用Web的形式则是必然的。

表现层需要整合整个系统,下面是一段初始化代码,从中可以直观的看到对业务逻辑的初始化:

 

IFtpAccessorFactory ftpAccessorFactory = new EdtFtpAccessorFactory();

 

LuceneDataAccess fileServices = new LuceneDataAccess(workPath);

 

ISearchItemDataServices searchItemServices = new OucSearchItemServices();

 

SearchManager searchManager = new SearchManager(ftpAccessorFactory, fileServices, searchItemServices);

 

if (!isSilence) searchManager.RobotNewFileFound += new EventHandler<NewFileFindEventArgs>(searchManager_RobotNewFileFound);

 

searchManager.FileSaved += new EventHandler<FtpSearch.SearchFacade.Event.FileSaveEventArgs>(searchManager_FileSaved);

 

以上代码可以总结为:使用满足业务逻辑定义的接口的类,给业务逻辑进行构造。也就是对整个系统的整合。

所谓的驱动,就是根据用户的请求,调用初始化了的业务逻辑。用户的请求可能是Web Request,命令行参数、鼠标的点击、时钟事件等等。表现层应将其转化为业务逻辑的消息,并在此过程检查输入输出是否合法。同时,表现层需将业务逻辑给出的结果反馈给用户。

3.6依赖的方向

系统实现的顺序就是系统依赖的顺序,先描述后实现的顺序体现了从已知到未知的探索过程,同时也是不稳定依赖于稳定的过程。

系统依赖的方向应该是不稳定的部分依赖于稳定的部分,稳定的部分先于不稳定的部分被设计和实现,不受不稳定部分的影响。这样的系统才是健壮的、灵活的。

在一个系统中,什么是稳定的?逻辑,因为逻辑直接来源于需求;什么是不稳定的?数据库设计、用户界面等,因为左右他们的因素会有很多。

系统某一部分的稳定程度反映在它的抽象程度上。接口、抽象类的抽象程度高于具体的实现类。业务逻辑包含的大部分是接口和抽象类,而具体的类大都位于实现部分。

FtpSearch系统中。业务逻辑是指KernelSearchBusinessQueryBusiness。这三个包,其中SearchBusinessQueryBusiness显然是以接口为主的,抽象程度较高,稳定性强。Kernel包比较特殊,全部是具体类。但是Kernel是对现实世界对象的映射,现实世界是不容易改变的,所以这个包虽然抽象程度不高,但是稳定性却比较高。所以依赖于这三个包,即依赖于业务逻辑,是向稳定的方向依赖,是正确的。

4耦合

4.1单元测试

在前面我已经提到过,接口同时起到了定义和解耦合两个作用,这个进行单元测试时尤为重要。因为对于某个类的测试,我们必须为其构造一个环境,将这个类分离出来。如果这个类直接调用系统的其他部分,那么系统就是紧耦合的。这种浑然一体的系统是无法一一攻破的。我们继续用FtpSearch的例子进行分析。

比如我们要对核心类Robot进行测试,这个类依赖于接口IFtpAccessorFactory。我们可以用一个实现该接口的模拟类对Robot进行测试。UML图如下:


(图十一:
Robot的单元测试)

图中,RobotTest调用MockAccessorFactoryRobot进行构造,然后执行Robot的方法进行测试。MockAccessorFactory类会模拟各种访问服务器的行为,甚至是模拟一些异常情况,比如服务器超时、访问的目录不存在、数据非法等。这样的好处是可以很容易实现测试的状况覆盖,而不用真的使用服务器引发某个错误。

TDD的一个好处就是,如果你先写Robot的测试RobotTest。那么你自然而然的就会得到上面的结构。

这个接口是便于测试的,请注意图中描述与实现的关系,IFtpAccessorFactory是描述,而MockAccessorFactoryEdtFtpAccessorFactory都是实现,而且还可以有其他的实现。不同的实现满足不同的需求,这样系统具有很大的灵活性,是松耦合的。

反过来看,Robot是高层次的,它定义了IFtpAccessorFactory接口,描述了所需要的逻辑,但是它不需要了实现的细节。MockAccessorFactoryEdtFtpAccessorFactory通过实现IFtpAccessorFactory接口满足不同的要求,视为不同的实现方式。

4.2第三方API

第三方的组件是不断更新、不可靠,同时也是不可控制的。可以说是系统中最不稳定的部分。从防御性编成6的思想来看,要把不可靠的因素隔离起来。从描述和实现的角度来看,第三方组件服务于实现,是细节,应该依赖于抽象,为实现服务。所以在采用第三方组件的时候,一半将其影响限定在一个类或一个包中。这个时候,Proxy模式和Façade3模式都是很有用处的。

FtpSearch系统中采用了两个第三方的开源组件,分别是EdtFtpNetLucene。都使用了Proxy模式进行封装隔离。

 

以上就是系统构建的顺序。

系统构建的顺序就是系统各部分的依赖顺序。系统逻辑、实现、表现三部分的依赖关系是:实现依赖于逻辑、表现依赖于逻辑和实现。其中业务逻辑是整个系统的核心,业务逻辑即使用代码描述的系统行为,它从需求出发,规定了系统各部分的协作关系和运行方式。良好的业务逻辑抽象是系统健壮、灵活的保证。

参考文献

1.  《源代码就是设计》 Jack Reecves

2.  《敏捷软件开发 Robert C. Martin

3.  《设计模式》,4GOF

4.  《测试驱动开发》,Kent Beck

5.  《代码大全2》,Steve McConnell

6.  《程序员修炼之道》,Andrew Hunt