来源:互联网 发布:网络音乐地址mp3 编辑:程序博客网 时间:2024/05/17 00:06
 
揭示在软件工程中,水能载舟,亦能覆舟的道理
文/雷扬
摘要
本文将以分析一个不适当的三层架构的应用引起的弊端作为例子提出在软件工程中度的概念,再将以设计模式中Strategy和Template模式的应用作为度概念的延伸,从而再次揭示这个度概念的存在。
 
将适当的方法应用于适当的地方,才能真正体现出其效应,再好的方法应用得不恰当,往往收不到臆想的结果,或者收到相反的结果是本文中度概念存在的意义。
概述
水能载舟,亦能覆舟。这本来是一个人生的道理,但是在软件工程中这一信哲同样存在。一种方法在适当的时间,适当的方位能够使得事情事半功倍,而如果把再好的方法运用于不适当的地方轻则不能达到预期的效应,甚至可能适得其反。
 
在下面的章节中我将分别分析几个软件工程中大家公认为很好的方法,在一个不适和的地方所产生的弊端,从而阐述我的论点。
三层架构 vs. 单层结构
三层架构,现在甚至成为了“软件高手”所专有的名词,也成为了人才市场上程序员面试中越来越多的考试题目。这里我将就当前最流行的三层结构中的Domain Model[1],甚至被称为面向对象的直接表现方式,结合所谓“软件低手”的单层结构分析它的弊端。
 
之所以从弊端讲起我的目的不在于否定Domain Model三层架构本身,而是想从弊端中看到不适合运用三层架构的地方,从而描绘我所定义的度的概念,之后我同样会分析三层架构将带来的益处。
 
其实就Martin Fowler提出的三层架构除Domain Model有很多的方式,包括单层结构也可以被划分为三层架构的一种形式,在这里之所以把Domain Model作为三层结构的定义,原因在于在很多程序员的理解三层架构就是Domain Model,面向对象也就是Domain Model。在以下的章节中,所有我提及的三层架构都被泛指成为了Domain Model结构的三层架构,希望大家不要在这里产生歧义。
 
三层架构把一个软件体系从纵向切分为三块独立的模块,其处理原理采用向下依赖方式。如图表1所示。表现层对业务逻辑层直接的依赖,逻辑层对数据访问层直接依赖。所有和数据库形成交换信息的操作都被归属于数据访问层,业务逻辑层和表现层都不能直接操作数据库。
图表 1:三层体系架构
 
为了明了我的论点,现在我假设一个在实际数据库项目中再普遍不过的功能性需求,虽然是一种假设,但这种需求可以说随处可见,如图表2所示。
 
功能性需求:
1.       ……
2.       需要通过系统对学生信息进行添加,学生信息包括学号和姓名,其中学生学号是一个学生信息的唯一标识。其中学号和姓名都为不可空信息。
3.         ……
图表 2:假设的功能性需求
 
经过一番分析,与客户的协商,形成如图表3所示的数据库表和图表4所示的界面,最终用户将通过界面录入的信息,将数据添加到数据库表。
图表 3:数据库表
下面我将分别描述用VS.Net 2005搭建该功能性需求分别采用三层架构和单层架构的搭建方式,之后则根据实际的开发方式进行分析和比较。
图表 4:最终用户界面
1.1    三层架构的搭建
应用三层架构,其搭建方式需要建立各个层次的类满足前面所述三层结构各个环节的构造,由于篇幅的关系我免去了实际的程序代码,采用的UML图简化描述三层架构的架构方式。最终,我们将会形成如图表5所示的架构图。
图表 5:三层架构图
 
如前面所描述的构造原则,对Student表操作将以SQL语句或者存储过程的方式集中于数据访问层类StudentsTableAdapter。在逻辑层中采用Domain Model的方式建立Student业务逻辑对象,并添加控制学生信息的逻辑对象StudentController该类中的方法AddNewStudent将完成添加学生的时候数据信息的传递,表现层将通过这些逻辑对象类和数据访问层形成信息交换。表现层是一个简单的数据获取装置,我用.Net所提供的aspx页面文件。
 
由于我免去了代码的表示,添加了如图表6所示的表现层,逻辑层,数据访问层和数据库进行信息互交的时序图以帮助大家理解,成其为所谓的“高手解决方案”。
图表 6:三层架构时序图
1.2    单层结构的搭建
在这里,我将继续描述采用VS. Net 2005搭建该功能时候采用的最简单的方法。之后再对这两种方法进行分析。在这里我不是在为Microsoft作广告,我相信在其他任何一种语言都可以以这种简单的方式实现该功能。
 
方法非常简单,直接在AddStudent.aspx中放入SqlDataSource,然后采用Micrsoft提供的方便向导配置方式,形成相关属性设置,动手写一行再简单不过的代码就可以完成。相信看过VS.Net 2005基本入门级相关教材的程序员都可以轻松地做到,也就成其为所谓“低水平程序员”的解决方案。其程序如图表7所示。这里我为了节省篇幅略去生成表单的html代码,只保留SqlDataSource和添加事件的相关代码。
 
<script runat="server">
    protected void btnAdd_Click(object sender, EventArgs e)
    {
        this.sdsAddNewStudent.Insert();
    }
</script>
<!--省略的html内容à
        <asp:SqlDataSource ID="sdsAddNewStudent" runat="server" ConnectionString="<%$ ConnectionStrings:Balance_DemoConnectionString %>" InsertCommand="INSERT INTO [Students] ([StudentID], [StudentName]) VALUES (@StudentID, @StudentName)" >
            <InsertParameters>
                <asp:ControlParameter Name="StudentID" Type="String" ControlID="txtStudentID" />
                <asp:ControlParameter Name="StudentName" Type="String" ControlID="txtStudentName" />
            </InsertParameters>
        </asp:SqlDataSource>
图表 7:单层结构程序代码
 
本想继续对该方案再进行一些描述,但是我始终没有再找到可以用来添加的材料,因为大家在看到这份代码以后就已经对这个方案再了解不过了,不需要借助任何其他语言或者图形来帮助大家对它进行更深入的了解。
 
下面我将就两种方案进行对比,从大家都看得到的方面,由于表现层和数据库的定义已经比较清晰,而且在这两种方案中没有不同之处。所以这里免去了这些部分的对比。
 
就可读性而言,虽然也是一个可支持我的论点的题目,但是我在这里还是选择忽略,相信有经验的程序员可以很直接地得到这个问题的答案。
1.3    对比开发效率和代码质量
对于开发效率我的主要的依据直接来自代码量的多少,代码量多的程序自然开发效率就比代码量少的低。虽然从这个很小的例子上来看,三层结构的代码量不会比单层结构超出许多,顶多也就是20行代码左右,但是如果在一个项目中有50个大同小异的功能点,代码量的增加就可以达到一个需要关注的程度。
 
且不说如果采用三层架构是否需要考虑出现详细的结构性设计文档,才能给维护程序员提供以足够的信息读懂程序结构,就只关系到代码而言一个显然的结果。三层架构的代码量大于单层结构的代码量。
 
两种方案给最终用户相同的一个结果,但是代码量不同。所以在这个例子中三层结构的开发效率低于单层结构的开发效率。
 
代码质量的问题也随代码量的增加产生,问题产生的关键在于“没有人能够写出没有bug的代码,bug的数量和代码量成正比”,因为代码量的增加,会增加程序员犯错误的机会,代码越多bug越多。从这一点的推论:在这个例子中,程序员容易犯的错误比单层结构多,三层结构比单层结构质量差。
 
很多人会在这里提出了O/R Mapping,提及O/R Mapping可以帮助我们生成一系列的相对稳定代码,这样的生成的结果可以基本保证就目前来说的三层结构和单层结构的代码量相同,在这里我先暂时保留对这部分的分析,在陈诉了可维护性和可扩展性的比较之后,我再对它进行描述。
1.4    对比可维护性和扩展性
可维护性,可扩展性可谓是针对我这个论点中大家可能会提出的最多的反驳意见,所以我在这里将再次扩展这个实际的假设例子让大家有更多的体验。
 
客户的需求总是在变化的,相信各位从事多年的程序员大哥大姐都会深有体会。再次假设,在项目进行了第一次UAT之后产生了如下的修改报告:
客户测试反馈意见报告
总体意见:
系统完善,正常,基本适合我们的需求……
修改意见:
……
学生信息添加功能需要除学生学号和姓名之外需要增加性别信息。
……
图表 8:客户测试反馈意见报告
 
如此这样的需求变化,大家都见过也处理过,这种修改性需求将贯穿于所有整个软件的生命周期,如果这种修改不存在,软件维护就失去了其存在的意义。
 
客户是上帝,大家都可以选择拒绝修改,但是拒绝带来的最有可能的直接结果就是引起客户的满意度下降,项目经理对你的工作态度产生意见,最直接地影响你的升职,影响你的加薪。
 
既然按照客户意见进行修改成为了必然的结果,数据库表结构和用户界面的修改是不可避免的修改的过程,访问数据库的存储过程或者Sql语句的变化也是在两种方式中不可避免,所以我同样在这里免去了这部分的内容。我们从这里来观察对于两种结构对你修改这样的功能性需求产生的开销。
 
三层架构中,首先我们可以看到Domain Model中逻辑层实体类Student类需要进行变化,最起码地增加一个性别属性,修改构造函数的函数签名,形成如图表9所示的静态类图。
图表 9:修改后的Student类
 
其次,数据访问层同样面临变化,AddStudent函数的数字签名的修改也成为了必然的结果,增加新的Gender参数,修改后的类图如图表10所示。SQL语句或者存储过程的修改也是必然的一个过程。
图表 10:修改后的数据访问层
 
对于单层结构,需要修改的地方就比较简单,把aspx中的SQL语句进行适当的修改,增加一个参数,再在VS.Net 2005的SqlDataSource添加一个参数和界面上新的输入控件挂接,就可以完成修改。这里我也略去了这部分的详细描述。
 
每一个代码的修改点都会产生成本上的开销,修改点越多需要消耗的时间越多,效率越低,维护时产生的效率问题的答案就不言而愈了。质量问题和开发过程中相同的推论,修改点越多,容易犯的错误越多。因而,这个例子中,维护任务中操作效率和质量,三层结构都比单层结构的低。
 
习惯于瀑布型开发的人会提出,这些都是针对需求变化的时候产生的问题,而如果我们能够控制需求不变,这些问题自然就不会存在了。答案是肯定的,但是在这里我不得不提醒大家,这里是说如果,大家都可以去回顾你们所经历过的项目,是否这种完美的需求真实地存在?
1.5    O/R Mapping
O/R Mapping将会成为异议者反驳我的一个论点,现在市面上有各种O/R Mapping工具可以帮助程序员很方便的形成逻辑层和数据访问层的代码。
 
这里我不打算对O/R Mapping工具进行质量上的评价,这里只从另外一个层面上分析O/R Mapping所带来的问题。在面对维护、扩展的时候,自动生成的代码同样是代码,同样存在维护的问题,这是不可否认的,那么当出现类似上一章节形式的维护的时候,维护的开销将会同样出现。
 
曾经见过一个应用O/R Mapping工具进行开发的项目,在开始的时候根据O/R Mapping生成了相关的代码,随之根据项目的变化,所有的东西都开始变化,于是项目组开始一次又一次地生成代码。由于采用的O/R Mapping工具的某些局限性,所生成的代码在每一次生成之后都需要进行适当的修改,才能满足实际架构三层的需要。这种修改跟随每一次代码生成器运行而执行,这样的开销同样成为了程序员代码的开销。在项目进行了一段时间以后,所采用的O/R Mapping工具作了一次全面的升级,两个版本生成的代码有较大的差异,导致了项目的维护在某种程度上说形成了瘫痪。
 
在这点上我想增加额外的一个观点,所有的代码,不管是程序员写的还是动态生成的,不管是C#, Basic, Pascal, Java, C++, 汇编,还是html, xml, Ini文件代码,都是代码,这些代码都有开发和维护的代价,这些维护中造成的时间开销也是降低效率的一个因素,错误的发生也是造成代码质量下降的一个原因。
1.6    小结
对于分层结构的小结的开始,我先打算面对很多人可能产生的反驳,“没有项目会类似你假设的功能那么简单。”
 
我不否认这一点的事实性,但就我假设的需求来说,单层结构是比较于三层结构带来了太多的效益。那么这里我可以大胆地做出一个结论,如果你的项目中有一百个功能点,其中五十个功能点的需求类似我的假设,那么起码这五十个功能点的设计和实现就不一定适合三层结构,最起码三层结构会带来效率上和质量上的问题。当然这也能成其为绝对的判断标准,还有其他一些因素会决定整个结构的构造。
 
因素会非常的具体,完全取决于所要进行开发的实际项目,其中Martin Fowler在他的企业级架构一书中有较为详细的描述。
 
l         多种表现形式并存
结合Martin Fowler在他的企业级架构中的描述,就我刚才所描述的假设再次作为示例,如果我们的项目需要存在两个表现层,例如Windows Form程序和Console程序并存,我们需要从一个控制台程序和窗口程序同时产生录入学生信息的功能,那么在这里至少,数据访问层的分离就成为了一种必要开销。原因只有一个,因为SQL脚本代码也是代码,我们不能考虑在Windows程序和控制台程序中去重复所需要的SQL语句,这样会带来维护的时候的各种弊端。那么让两个表现层共享同一个数据访问层会带来的好处,远远超过了代码复制在程序中会产生的弊端,如下图所示。
图表 11:多种表现形式并存
 
l         有数据库类型转换的需要
项目在开始的时候就有从小型数据库向大型数据库转换的考虑,这种转换尤其适用于需要长期维护发布新型版本的产品软件。例如需要从Access转型到Oracle,作数据库开发的人员都会比较清楚,Access和Oracle的数据库SQL语法有一定的区别,当这种移植发生的时候,有数据访问层的控制会比把所有的SQL语句都写在窗口中进行数据访问带来了太多维护时的益处。
 
l         复杂的业务逻辑
在项目中的某些功能点,需要在程序中处理复杂的计算功能的时候,实体与实体之间形成有形的顺序、流程控制,那么面向对象在逻辑层的出现所能形成的各种益处将会带来这些计算功能的方便处理。
 
l         开发队员的各项技能参差不齐
有些队员比较熟悉SQL语句开发,有些队员比较熟悉html,还有些比较熟悉Java或者C#等编程语言,而且补齐这种差异需要付出更大的代价。那么按照不同层次的编程方法也是一条可选之路。
 
相信这种场景是的确很多,但是如此多的场景不能代表三层结构存在的绝对必要,就如我上面所描述的示例,三层结构反而降低了开发的效率和质量,因为在我的示例中,这些因素都不存在。
 
对于大多数最终用户和开发项目而言,他们需要的就是一个完善运行,相对稳定,可以方便的帮助他们提高工作效率的一个系统,是否采用三层对他们来说并非是个必要的产品,那么三层的是否选择就成为了一个你自主选择的战略计划。
 
这里就产生了一个度的道理,三层结构或者以之衍生的多层结构,在某些情形下是一个对软件工程实际开发产生利大于弊的结果,而在某些项目中会产生避大于利的效应,是否该采用三层,或者采用Domain Model关键的参考依据是在于你的项目或者就你需要面对的功能点和实际项目的情况,如果采用了该结构能提高项目的开发、维护效率,增加软件的质量,那就应该采用;相反,那么大可不必去证明自己的所谓“高手解决方案”。
类泛滥 vs. 清晰职责
这一个题目来自设计模式[2]中的两个基本模式,Strategy [2]和 Template[3]。严格的说,Strategy其实是Template的一个衍生或者变化。而Strategy可以解决Template的一个类泛滥问题,这是Strategy较Template而言所产生的益处之一。而往往很多程序员在设计的时候就会采用Strategy去代替Template模式,达到一个莫须有的减少类泛滥的要求。
 
这里我打算提及到大师Anders Hejlsberg的两个产品,我是一名从Delphi转变为C# .net的程序员。在感受这两个产品的时候发现了一个小小的问题。用过Delphi和.Net的人都知道,输入框控件在Delphi中叫TEdit,.Net中叫做TextBox。而他就对这两套不同的开发方式采用的不同的设计方式给予了用户,也就是我们,不同的体验。
 
某种程度上来说,在Delphi中他应用Template模式提供了一系列的输入框控件给我们使用。如下图所示。
图表 12Delphi中的输入框
 
而在.Net当中只有一个TextBox就可以同时实现如同上图中TEdit和TDBEdit能给我们带来的功能。某种程度上来说Anders Hejlsberg在这里采用了Strategy模式设计了该组件的构造。
 
我不需要去了解为什么Anders Hejlsberg为什么要以这两种不同的方式实现这个功能。而在我作为使用者,我的Delphi和.Net程序作为它的Client一段时间中我有两种不同的感受。
 
在使用Delphi的过程中,我写的第一个程序,当我面对需要从数据库中读取数据以输入框的形式表现在界面上的时候,我就采用了TDBEdit,使用它作为直接从数据库中读取数据而不需要写代码的工具,配置相应的属性它就帮我自动把数据库里面的字段读取并填充。
 
而在.Net中,我采用了另外一种方式,从数据库访问组件中获得数据值,然后把他通过程序赋到TextBox控件中去,在面对两三次这样的开发过程后,我甚至产生了自己封装一个DBTextBox的想法。
 
很多.Net的程序员会在这里告诉我,在.Net中也可以和Delphi中一样,通过设置属性就可以完成该项功能,我现在也了解这种方式,但是在我从事.Net开发半年以后。
 
在使用Delphi开发的时候,我的第一判断依据来自对这个类/组件的名称,它的名字让我清晰地知道,这个类/组件将可以用作与数据库字段的方便操作;在.Net中,在缺乏对msdn上关于该组件的详细了解的情况,我采用了一个不太好的方法处理这个问题,于是我至少会在一段时间之内会采用不太好的方法,又或者我自己构造一个多此一举的DBTextBox。
 
那么这就带来了Template模式在某种情形下也会带来的好处。它让它的使用者能够很清晰地知道这个类存在的意义,它能够完成的功能,也就如我题目上所提及的清晰职责。
 
图表 13:Template调用方式
 
如图表13所示,Client端调用输入框系列组件的时候,如果采用Template模式,客户端能够很清晰的了解到系列中每个功能的使用,因为对于TEdit和TDBEdit对于他来说都是直接可见的。
 
而对于如图表14所示的Strategy模式,Client调用该系列组件的时候,因为对于其多态体系结构呈现非直接调用,这样会造成Client对Component本身的体系结构的了解不透彻而产生的误用。如图表14所示,如果在这个Strategy中ABehavior是默认的委托项的话,Client可能就会在一段时间采用该委托项,而忘记了其他委托项的存在,正如我发生的问题。
图表 14:Strategy调用方式
 
有人也许会加以评论,这样的误用是Client,或者其搭建者的问题,那么我所想到的回答是: Component或者类抽取出来作为一个共用的目标开发,它的调用者不去调用其他的功能点,这些功能点是否还具有存在的意义?
 
当然在这里我同样不是想表达Template一定优越于Strategy的目的。在某种程度上它们是可以互换,那么该采用哪一种方式,我同样把他归结为一个度的问题。Template带来的清晰职责还会有其他的益处同样可以和Strategy的避免类泛滥益处相抗衡,在我们,特别是在构造一些可公用的组件的时候,我们就需要去仔细地分析各自带来的效应,选取一个带来最大效应的结果还是选择它们两者的依据。
结束语
度这个概念除我所描述的这两点比较,在软件工程更为普遍的存在,在此提及一本大家都熟知的老书,重构[3],细心的重构读者会发现在重构中反复出现了几个由A重构到B,又从B方法重构到A的例子,这里Martin Fowler的描述其实也包含了这个度的概念。
 
事务是变化的,正如需求是变化的一样,在项目发展到某种适当的情形下A方法会比B方法更适用,情形再次变化B优胜于A的情况同样会发生。当这种情况发生,重构也就随之发生,这也是Martin Fowler所描述的重构的最重要的意义之一,在适当的时候选择适当的方法来处理中的编码或者设计,而不是采用一种方法就可以包治百病。
 
软件工程中有很多的方法,这些方法就算贵为大师所著,就算在别的项目中得到了成功,但是它不一定适应于你的项目,把一个适合的方法用在一个适合的地方才能真正使得事半功倍。而如果再好的方法用在一个它不适合的地方,往往会带来相反的效应。
 
就本文中提及的两个关于度的观点,其实没有找到相应的适合于全局的解决方案,在哪些情况下需要用三层,哪些情况下需要用单层,哪些情况下适合用Strategy,哪些情况适合Template。还没有总结出确切的定义,通过大家的总结和经验可以更快地找到答案。
参考文献
1.         Martin Fowler; Patterns of Enterprise Application Architecture; 企业级架构模式; Addison Wesley, Inc; 中国电力出版社;2003
2.         Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides; Design Pattern; Addison Wesley; 1995
3.         Martin Fowler; Refactoring; 重构; Addison Wesley; 中国电力出版社; 2003
原创粉丝点击