2006年03月09日

经常感觉很多人对于架构和框架分得不是很清楚,随便写一点关于这方面的东西吧。如果以后对于这方面的认识更加深刻的话,再补充好了。

    1. 架构和框架的设计层次不同

    类似于硬件设计,软件设计也分为不同的层次。典型的软件设计层次如下图:

design-level.BMP

    在这个图中我们可以看到,Framework处于Micro-architectures和Application Level之间。Deisgn Patterns是Micro-architectures级的设计,Framework由多个Design Pattern和其他微架构设计元素形成。而Object&classes、Micro-architectures和Framework被称为 Micro Level设计。就是说,从Objects&classes到Framework,还没有发生质变。

    对于Application Level到Global/Industry Level的设计来说,就都是Architecture的范畴了。从Application Level来说,它是由零到多个Framework组成的独立的应用程序,会考虑诸如UI之类的重要问题。System Level由多个应用组成,每个应用在整个系统中代表不同的角色,这些应用共同组成一个系统工作环境。Enterprise Level又是由System Level组成,通常跨越整个企业(这里是广义的企业)中的多个组织机构。Global/industry Level由多个企业通过Internet和商业市场组成一个相当大范围内的系统应用,B2B就是这样的Global Level设计的应用系统。

    显然Framework和Architecture在这里的差别是巨大的,哪怕和Application Level相比。当一个应用中只使用了一个Framework时,我们可以把它叫做Architecture吗?事实上,Framework仅仅是 Architecture中的一个微不足道的部分。在Achitecture中,我们考虑技术视图时,会选择J2EE或.NET,然后才会考虑:是否要用 Spring、Hibernate?从这里可以看出,Framework只是技术视图的一个设计决策。

    2. 架构和框架的Design Forces不同

    其实,仅仅上面的描述也应该能够让大家清楚的认识到Architecture和Framework的区别了。但我还想在另外的方面更进一步说明。

    Design Forces我不知道怎么翻译,只能解释一下了:Design Forces指设计主要针对的问题、领域和能力。勉强可以翻译成“设计针对”吧。

    Framework的Design Forces主要是功能性、复杂性和性能。从Framework的定位和设计层次来说,它主要目的是帮助开发人员完成公共的、系统的功能,这些功能在大多数应用中都需要,差别在于多少而已。对于一个Framework,完成系统的功能,隐藏并尽量简化这些系统功能的复杂性,同时提供可接受的性能,这就是它的设计目标了。

    而对于Architecture,其最主要的Design Force就是变更。其次,所有可能的问题都是要在Architecture中考虑:复杂性、功能、性能、技术、所有非功能需要等等。。。

    3. OOP、AOP还是FOP(Framework oriented Programming)?!

    做Java有一个好处,就是各种框架技术层出不穷,使用方便。但这不见得就是好事情。感觉很多人非常专注于框架的使用和研究,对框架注意力甚至超过了对OO、Java、J2EE规范等基本的东西的注意力。倒是有些象当初Windows下面,用VB、VC、Delphi,还是 CBuilder?似乎有些人把OOP、AOP变成了FOP。

    在我看来,框架这个东西,用用是可以的,研究就不必了;自己写个框架也是可以的,但也不应该停留在框架这个层次。不要在框架上浪费精力,把这个事情交给毕业生做好了。

    4. 慎用Framework!

小标题夸张一点,是为了提醒大家。一般情况下,使用Framework可以大大减少工作量,使开发变得容易。通常使用框架应该是值得鼓励的。但是也要注意:

    • 不要滥用Framework。不要在一个不是很大的项目中使用过多的Framework,不然维护会受到影响。
    • 尽量不要同时使用几个功能上有交叉的Framework。这会使项目开发的管理更加复杂,同样会导致维护问题。
    • 在Enterprise Level的应用中使用Framework时,要对Framework进行严格的评估,确保其Design Forces不和Enterprise Level的应用冲突!Framework在设计时刻通常不会考虑到Enterprise Level的问题,你不能想当然的认为它一定可以适合你的Enterprise应用!


        关于架构和框架,可以用一首诗来做结:

        横看成岭侧成峰,远近高低各不同。
        不识庐山真面目,只缘身在此山中。

    (http://www.blogjava.net/feelyou/archive/2005/11/22/ArchitectureVSFramework.html)

    2006年02月19日

            分离关注( Separation of Concerns : SOC)是Ioc模式和AOP产生最原始动力,通过功能分解可得到关注点,这些关注可以是 组件Components, 方面Aspects或服务Services。

      从GoF设计模式中,我们已经习惯一种思维编程方式:Interface Driven Design 接口驱动,接口驱动有很多好处,可以提供不同灵活的子类实现,增加代码稳定和健壮性等等,但是接口一定是需要实现的,也就是如下语句迟早要执行:

      AInterface a = new AInterfaceImp();

      AInterfaceImp是接口AInterface的一个子类,Ioc模式可以延缓接口的实现,根据需要实现,有个比喻:接口如同空的模型套,在必要时,需要向模型套注射石膏,这样才能成为一个模型实体,因此,我们将人为控制接口的实现成为“注射”。

      Ioc英文为 Inversion of Control,即反转模式,这里有著名的好莱坞理论:你呆着别动,到时我会找你。

      其实Ioc模式也是解决调用者和被调用者之间的一种关系,上述AInterface实现语句表明当前是在调用被调用者AInterfaceImp,由于被调用者名称写入了调用者的代码中,这产生了一个接口实现的原罪:彼此联系,调用者和被调用者有紧密联系,在UML中是用依赖 Dependency 表示。

      但是这种依赖在分离关注的思维下是不可忍耐的,必须切割,实现调用者和被调用者解耦,新的Ioc模式 Dependency Injection 模式由此产生了, Dependency Injection模式是依赖注射的意思,也就是将依赖先剥离,然后在适当时候再注射进入。

    Ioc模式(Dependency Injection模式)有三种:

    第一种类型 从JNDI或ServiceManager等获得被调用者,这里类似ServiceLocator模式。 1. EJB/J2EE
    2. Avalon(Apache的一个复杂使用不多的项目)
    第二种类型 使用JavaBeans的setter方法 1. Spring Framework,
    2. WebWork/XWork
    第三种类型 在构造方法中实现依赖 1. PicoContainer,
    2. HiveMind

      有过EJB开发经验的人都知道,每个EJB的调用都需要通过JNDI寻找到工厂性质的Home接口,在我的教程EJB是什么章节中,我也是从依赖和工厂模式角度来阐述EJB的使用。

      在通常传统情况下,为了实现调用者和被调用者解耦,分离,一般是通过工厂模式实现的,下面将通过比较工厂模式和Ioc模式不同,加深理解Ioc模式。

    工厂模式和Ioc

      假设有两个类B 和 C:B作为调用者,C是被调用者,在B代码中存在对C的调用:

    public class B{
       private C comp;
      ……
    }

      实现comp实例有两种途径:单态工厂模式和Ioc。

    工厂模式实现如下:

    public class B{
       private C comp;
      private final static MyFactory myFactory = MyFactory.getInstance();

      public B(){
        this.comp = myFactory.createInstanceOfC();

      }
       public void someMethod(){
        this.comp.sayHello();
      }
      ……
    }

    特点:

    • 每次运行时,MyFactory可根据配置文件XML中定义的C子类实现,通过createInstanceOfC()生成C的具体实例。

    使用Ioc依赖性注射( Dependency Injection )实现Picocontainer如下,B类如同通常POJO类,如下:

    public class B{
       private C comp;
      public B(C comp){
        this.comp = comp;
       }
       public void someMethod(){
        this.comp.sayHello();
       }
      ……
    }

    假设C接口/类有有一个具体实现CImp类。当客户端调用B时,使用下列代码:

    public class client{
       public static void main( String[] args ) {
        DefaultPicoContainer container = new DefaultPicoContainer();
        container.registerComponentImplementation(CImp.class);
        container.registerComponentImplementation(B.class);
        B b = (B) container.getComponentInstance(B.class);
        b.someMethod();
       }
    }

      因此,当客户端调用B时,分别使用工厂模式和Ioc有不同的特点和区别:

      主要区别体现在B类的代码,如果使用Ioc,在B类代码中将不需要嵌入任何工厂模式等的代码,因为这些工厂模式其实还是与C有些间接的联系,这样,使用Ioc彻底解耦了B和C之间的联系。

      使用Ioc带来的代价是:需要在客户端或其它某处进行B和C之间联系的组装。

      所以,Ioc并没有消除B和C之间这样的联系,只是转移了这种联系。
      这种联系转移实际也是一种分离关注,它的影响巨大,它提供了AOP实现的可能。

    Ioc和AOP

      AOP我们已经知道是一种面向切面的编程方式,由于Ioc解放自由了B类,而且可以向B类实现注射C类具体实现,如果把B类想像成运行时的横向动作,无疑注入C类子类就是AOP中的一种Advice,如下图:

      通过下列代码说明如何使用Picocontainer实现AOP,该例程主要实现是记录logger功能,通过Picocontainer可以使用简单一行,使所有的应用类的记录功能激活。

    首先编制一个记录接口:

    public interface Logging {

      public void enableLogging(Log log);

    }

    有一个LogSwitcher类,主要用来激活具体应用中的记录功能:

    import org.apache.commons.logging.Log;
    public class LogSwitcher
    {
      protected Log m_log;
      public void enableLogging(Log log) {
        m_log = log;
        m_log.info("Logging Enabled");
      }
    }

    一般的普通应用JavaBeans都可以继承这个类,假设PicoUserManager是一个用户管理类,代码如下:

    public class PicoUserManager extends LogSwitcher
    {

      ….. //用户管理功能
    }
    public class PicoXXXX1Manager extends LogSwitcher
    {

      ….. //业务功能
    }
    public class PicoXXXX2Manager extends LogSwitcher
    {

      ….. //业务功能
    }

    注意LogSwitcher中Log实例是由外界赋予的,也就是说即将被外界注射进入,下面看看使用Picocontainer是如何注射Log的具体实例的。


    DefaultPicoContainer container = new DefaultPicoContainer();
    container.registerComponentImplementation(PicoUserManager.class);
    container.registerComponentImplementation(PicoXXXX1Manager.class);
    container.registerComponentImplementation(PicoXXXX2Manager.class);
    …..

    Logging logging = (Logging) container.getComponentMulticaster();

    logging.enableLogging(new SimpleLog("pico"));//激活log

      由上代码可见,通过使用简单一行logging.enableLogging()方法使所有的应用类的记录功能激活。这是不是类似AOP的advice实现?

      总之,使用Ioc模式,可以不管将来具体实现,完全在一个抽象层次进行描述和技术架构,因此,Ioc模式可以为容器、框架之类的软件实现提供了具体的实现手段,属于架构技术中一种重要的模式应用。J道的JdonSD框架也使用了Ioc模式。

    2006年02月17日

          模式和极端编程(XP)都为软件设计、开发者提供了无法用金钱衡量的帮助。但是迄今为止XP大量关注于重构(refactoring),而对模式只字不提。在这篇文章中,我问“为什么”,并且最终描述出模式怎样以XP的方式更好地实现、以及XP怎样因为包含对模式的使用而变得更好。

    致谢

      非常感谢Kent Beck、Martin Fowler和Ward Cunningham,他们为这篇文章提出了友善的评论。

      仍在所知不多的时候我们就开始了自己的程序设计生涯,生产出的软件也反映出了我们的缺乏经验:我们创建的代码臃肿、错误百出、脆弱、难以维护、难以扩展。随着时间的流逝,我们成为了更好的软件设计者:我们从技术作家、专家那里学习,我们从自己的错误中学习。现在我们编写具有高度灵活性的软件,它适应广泛而且坚固。当被请求编写一个新的系统时,我们知道查明当前和将来的需求,这样我们可以设计软件来处理当前和将来的需要。

      在软件开发生涯的这个阶段,极端编程告诉我们,我们经常对软件过分设计(over-engineer)了。我们从自己的错误中学到了太多,我们不希望重复这些错误,所以我们在系统生命周期的早期做了大量的努力来创造灵活而坚固的设计。不幸的是,我们没有认识到:如果这个系统永远不需要这个程度的灵活性和坚固性,那么我们所有的工作就都没有意义了。我们过分设计了。

      我也曾经过分设计过。说实话,与其他设计者坐在一间房间里考虑如何设计软件来适应许多当前和将来的需求,这的确是一种乐趣。我们把自己学到的所有东西——尤其是那些最好的经验——应用在设计中。我们常常知道需求的列表会改变,但用户或客户总是改变需求。不过,我们认为我们可以足够聪明地设计软件,使软件足够灵活,使它能应付所有的需求变化。

      今天,极端编程将告诉你这是多么愚蠢的做法。XP说,我们必须让设计自己显现出来,而不是去预测设计将是什么样子。XP说,“做可能起作用的最简单的事”,因为“你将不再需要它”。另外,Kent Beck说:

      你需要在一个强调沟通、简单、反馈和勇气的价值系统中选择最好的工作方法,这样你才能勇敢的脱离过分设计。[Beck1 00]

      同意。但是,现在我必须提到我的朋友Norm Kerth。Norm在软件开发领域有丰富的经验和见识。一年以前我问他“对XP有什么想法”。他说:

      我喜欢XP里的每样东西。我关心的是:还有什么不在XP中。[Kerth 99]

      当时,我只认为Norm是一个保守派。但现在我不能确定了。XP明显缺少的就是使用模式的经验。尽管一些XP的创始人帮助建设了模式社团,但没有哪一个坚定清楚的说明模式如何适应XP。

      一开始,这还没有让我感到迷惑。但现在,我的确感到迷惑。

      我感到迷惑,因为我在XP和模式上的经验让我相信:在XP的场景中模式会工作得更好;并且当XP包含模式时,XP也会工作得更好。

      这需要一些解释。我将从描述我自己使用模式和XP的一些经验开始。

      从1995年开始,我开始沉浸入模式之中。我学习模式文献、主办了一个每周一次的模式学习组、使用模式设计和开发软件、并进行UP(一个关于使用模式的国际学术会议)的组织和运转工作。说我“热衷于模式”实在是一种保守的说法。

      当时,就象很多第一次学习模式的人一样,我有一点过分渴望使用它们。这不是一件好事,因为它会让你的设计比需要的更复杂。但我没有意识到这一点,直到我开始学习重构。

      大概在1996年,我第一次接触到了重构。我开始实证它并很快观察到重构带我离开了我在模式学习中学到的某些原则。

      举个例子,那本里程碑式的书——《设计模式:可复用面向对象软件的基础》——中的一个原则是:

      针对接口编程,而不是针对实现编程。[GHJV1 95]

      《设计模式》的作者们做了相当精彩的工作来解释为什么我们需要遵循这条建议。几乎在所有的模式中,都讨论了当你针对某个特定实现编程时你的软件如何变得缺少灵活性和可修改性。几乎每一次都是接口过来帮忙。

      但如果我们不需要灵活性和可修改性,情况又是怎样?为什么我们要在开始设计时预料一些可能永远不会出现的需要?这是我的一次觉悟。所以随后我记录下了下面这个JAVA技巧:

    不要分离类和接口

      我曾经习惯于在我的接口名字后面加上一个“I”。但当我继续学习更多的重构技术时,我开始看到一种明智的做法:把类名和接口名设计成一样。下面是原因:在开发过程中,你知道你可以使用一个接口来让某些东西变得灵活(使实现多样化),但可能你现在根本不需要让实现多样化。所以,放下预测太多的“过分设计”吧,你仍然保持简单,仍然把东西放在一个类中。在某个地方你会写一个方法语句来使用这个类的对象。然后,几天、几星期、几个月之后,你明确“需要”一个接口。因此你就将原来的类转换成一个接口,再创建一个实现类(实现新的接口),并且让你原来的语句保持不变。[Kerievsky 96]

      我继续学习类似于重构的课程,逐渐的,我使用模式的方式开始改变了。我不再预先考虑使用模式。现在,我更加明智了:如果某个模式可以解决某个设计问题,如果它提供一种方法来实现一个需求,我就会使用它,但我将从可以编码出的模式的最简单实现开始。晚些时候,当我需要增加或修改时,我将让这个实现更加灵活、稳固。

      这种使用模式的新方法是一种更好的方法。它节约了我的时间,并让我的设计更简单。

      由于我继续学到更多关于XP的知识,我很快开始考虑这样一个事实:那些清楚介绍“XP是什么”和“XP如何工作”的人毫不提及模式。看起来,焦点已经全部从开发转向了重构。构造一点,测试一点,重构一点,然后再重复。

      那么,模式怎么了?

      我收到的一般的答案是:模式鼓励过分设计,而重构保持事情简单、轻量级。

      现在,我和其他任何人一样喜欢重构——我回顾了Martin Fowler的书的关于这个主题的两份手稿,然后知道重构将成为一个标准。但我仍然喜欢模式,我发现模式在“帮助人们学会如何设计更好的软件”方面是无价之宝。所以,XP怎么能不包括模式呢?!

      我小心的在Portland Pattern Repository上写下了我的不安。我问:是否完美的XP模式应该由完全不知道模式的程序员和指导者组成,是否他们应该完全依赖重构来“让代码去它该去的地方”。Ron Jeffries,世界上最有经验的XP实践者,与我争论了这个主题,并且这样写:

      一个初学者不能倾听代码所说的话。他需要学习代码质量的模式(在一般意义上)。他需要看好的代码(以及,我猜,差的代码),这样他才能学会写出好的代码。

      一个问题——我的意思是一个可能的问题——是,现在的模式是否被用于帮助提高代码的质量。我想Beck的Smalltalk Best Practice Patterns会有帮助,因为那些都是非常小型的模式。我想设计模式都更值得怀疑,因为模式和讨论有时变得相当大,而且它们可能造成看起来合理的庞大解决方案。Martin Fowler的精彩的分析模式也有同样的危险:在可以选择一个小规模解决方案的时候选择了大规模的解决方案。[Jeffries 99]

      一个非常有趣的关于模式的观点。尽管我已经看到模式可以被明智的实现、使用,但Ron看起来却认为它们是危险的,因为它们“让庞大的解决方案看起来合理”。在其他地方,Ron观察了一件经常发生的事情:第一次学习模式的人们如何过分渴望使用它们。

      我无法不同意后面这个观察结果。就象任何新事物——甚至是XP——一样,人们可能会过分渴望使用它们。但模式真的鼓励在可以使用小规模解决方案时使用大规模解决方案吗?

      我想这主要取决于你如何定义、使用模式。举个例子,我观察了许多模式的初级使用者,他们认为一个模式与它的结构图(或类图)是完全相同的。只有在我向他们指出“模式可以根据需要以不同的方式实现”之后,他们才开始发现这些图只是表示实现模式的一种方式。

      模式的实现有简单的也有复杂的。诀窍是:发现模式针对的问题,将这个问题与你当前的问题进行比较,然后将这个模式最简单的实现(解决方案)与你的问题进行比较。当你这样做时,你就不会在可以使用小规模解决方案的时候使用大规模解决方案。你获得了解决问题最好的平衡。

      当人们没有受过模式的良好训练时,困难就可能出现。Ron提到人们使用模式的方式是“现在构成的”——这就是说,他们如何与现在的作者沟通。我同意模式文献有一些缺点。关于模式的书很多,你可以花一些时间来理解模式解决的问题,这样你就可以聪明的根据自己的特定需要选择模式。

      这种选择是极其重要的。如果你选择了错误的模式,你可能过分设计或仅仅把你的设计揉在一起。有经验的模式使用者也会犯错误,并且经常看到这样的结果。但这些专家有其他的模式作为装备,这些模式可以帮助他们面对自己的错误。所以他们最终经常把自己真正需要的模式换成了不那么理想的模式。

      那么,你将怎样成为一个有经验的模式使用者呢?我发现除非人们投身于大量模式的学习中,否则他们就有可能陷入误解它们、过分使用它们以及用它们过分设计的危险之中。

      但这是避免使用模式的一个原因吗?

      我想,不。我发现模式在如此多的项目中如此有用,以至于我无法想象不使用它们来进行软件设计和开发。我相信对模式的彻底的学习是非常值得的。

      那么,XP对模式保持沉默是因为感觉到它们将被误用吗?

      如果情况是这样,也许问题已经变成:我们怎样使用模式中的智慧,而避免模式在XP开发场景中的误用呢?

      在这里,我想我必须回到《设计模式》。在“结论”一章、“设计模式将带来什么”一节、“重构的目标”小节中,作者写道:

      我们的设计模式记录了许多重构产生的设计结构。在设计初期使用这些模式可以防止以后的重构。不过即使是在系统建成之后才了解如何使用这些模式,它们仍可以教你如何修改你的系统。设计模式为你的重构提供了目标。[GHJV2 95]

      这就是我们需要的观点:重构的目标。这就是重构和模式之间的桥梁。它完美的描述了我自己在如何使用模式方面的进步:从简单开始,考虑模式但将它们保持在次要地位,小规模重构,只有在真正需要模式的时候才把重构转移为模式。

      这个需要训练和仔细判断的过程将很好的适应XP所包含的最好的习惯。

      而且这个途径很明显与“故意不知道或不使用模式而只依赖重构来改善设计”的方法非常不同。

      只依赖重构的危险是:没有目标,人们可能使设计小小进步,但他们的全面设计将最终受损害,因为这种方法缺乏顺序、简单性和效力,而聪明的使用模式则可以让开发者拥有这些。

      引用Kent Beck自己的话:模式生成体系结构。[Beck2 94]

      但模式不保证有纪律的使用。如果我们在设计中过多、过早的使用它们,我们就又回到了过分设计的问题。因此,我们必须回答这个问题:“在设计的生命周期中,何时引入模式是安全的?”请回忆上面对《设计模式》的引用:

      在设计初期使用这些模式可以防止以后的重构。

      这是一个聪明的主张。如果我们不知道“何时配置一个模式”的基本规则,那么我们就很容易在设计周期的早期就陷入过分设计。

      再一次,问题又全部集中在一起:如何将项目中的问题与一个合适的模式相匹配。

      在这里,我必须讲述我为不同行业开发软件得到的经验。

      有一家客户要求我和我的团队用JAVA为他们的网站构造软件,这将是一个很酷的交互式版本。这个客户没有任何JAVA程序员,但仍然要求能在他们需要的任何时候、任何地方修改软件的行为,而不必做程序的修改。多么高的要求!

      在对他们的需要做了一些分析之后,我们发现Command模式将在这个设计中扮演一个非常重要的角色。我们将编写命令对象,并让这些命令对象控制软件的整个行为。用户将可以参数化这些命令、将它们排序、并选择命令运行的时间和地点。

      这个解决方案工作得很完美,Command模式正是成功的关键。所以在这里,我们不会等到重构的时候才使用Command模式。相反,我们预先看到了使用它的需要,并从一开始就用Command模式来设计软件。

      在另一个项目中,系统需要作为独立应用程序和WEB应用程序运行。Builder模式在这个系统中发挥了巨大的作用。如果没有它,我不敢想象我们会拼凑出一个多么臃肿的设计。Builder模式的作用就是解决“多平台、多环境运行”这样的问题。所以在设计早期就选择它是正确的。

      现在,我必须声明:即使在设计的早期引入了模式,但一开始仍然应该按照它们最原始的样子来实现它们。只有在晚些时候,当需要附加的功能时,模式的实现才能被替换或升级。

      一个例子会让你更清楚这一点。

      上面提到的由命令对象控制的软件是用多线程的代码实现的。有时候两个线程会使用同一个宏命令来运行一系列命令。但一开始我们并没有被宏命令的线程安全问题困扰。所以,当我们开始遇到线程安全造成的莫名其妙的问题时,我们必须重新考虑我们的实现。问题是,我们应该花时间构造宏命令的线程安全吗?或者有没有更简单的方法来解决这个问题?

      我们用更简单的方法解决了这个问题,并且避免了过分设计:为每个线程提供一个独立的宏命令实例。我们可以在30秒内实现这个解决方案。请把这个时间与设计一个线程安全的宏命令所需的时间做一下比较。

      这个例子描述了XP的哲学怎样在使用模式的情况下保持事情简单。没有这种简单化的驱动,过分设计的解决方案——就象线程安全的宏命令——很容易出现。

      因此,简单化和模式之间的关联是很重要的。

      当程序员需要做出设计决策时,很重要的一件事就是:他们应该试图保持设计简单,因为简单的设计通常比庞大而复杂的设计更容易维护和扩展。我们已经知道,重构意味着将我们保持在简单的路上:它鼓励我们以小而简单步骤逐渐改进我们的设计,并避免过分设计。

      但是模式呢?难道它们不是帮助我们保持简单吗?

      有些人会说“不”。他们认为模式尽管有用,但容易造成复杂的设计。他们认为模式会造成对象快速增加,并导致过分依赖对象组合。

      这种观点是由于对使用模式的方法的错误理解。有经验的模式使用者会避免复杂的设计、对象的快速增长和过多的对象组合。

      实际上,在使用模式的时候,有经验的模式使用者会让他们的设计更简单。我将再用一个例子来说明我的观点。

      JUnit是一个简单而有用的JAVA测试框架,它的作者是Kent Beck和Erich Gamma。这是一个精彩的软件,其中满是精心选择的简单的模式。

      最近一些人要求我对JUnit进行DeGoF,也就是说,将JUnit中的设计模式移除掉,以观察没有模式的JUnit是什么样子。这是一次非常有趣的练习,因为它让参与者认真考虑应该在什么时候在系统中引入模式。

      为了描述他们学到的东西,我们将对JUnit 2.1版中的一些扩展进行DeGoF。

      JUnit中有一个叫做TestCase的抽象类,所有的具体测试类都派生自它。TestCase类没有提供任何多次运行的方法,也没有提供在自己的线程中运行测试的方法。Erich和Kent用Decorator模式很优雅的实现了可重复测试和基于线程的测试。但是如果设计团队不知道Decorator模式呢?让我们看看他们会开发出什么,并评估一下他们的设计有多简单。

      这是Test Case在JUnit框架1.0版本中的样子(为了简化,我们忽略了注释和很多方法):

    public abstract class TestCase implements Test {
    private String fName;
    public TestCase(String name) {

    fName= name;

    }

    public void run(TestResult result) {

    result.startTest(this);

    setUp();

    try {

    runTest();

    }

    catch (AssertionFailedError e) {

    result.addFailure(this, e);

    }

    catch (Throwable e) {

    result.addError(this, e);

    }

    tearDown();

    result.endTest(this);

    }

    public TestResult run() {

    TestResult result= defaultResult();

    run(result);

    return result;

    }

    protected void runTest() throws Throwable {

    Method runMethod= null;

    try {

    runMethod= getClass().getMethod(fName, new Class[0]);

    } catch (NoSuchMethodException e) {

    e.fillInStackTrace();

    throw e;

    }

    try {

    runMethod.invoke(this, new Class[0]);

    }

    catch (InvocationTargetException e) {

    e.fillInStackTrace();

    throw e.getTargetException();

    }

    catch (IllegalAccessException e) {

    e.fillInStackTrace();

    throw e;

    }

    }

    public int countTestCases() {

    return 1;

    }

    }

      新的需求要求允许测试重复进行、或在它们各自的线程中进行、或以上两者。

      没有经验的程序员通常在遇到这样的新需求时进行子类型化。但是在这里,因为知道TestCase对象将需要能够在同一个线程中重复运行、或在各自独立的线程中重复运行,所以程序员知道:他们需要考虑得更多。

      一种实现方法是:将所有的功能都添加给TestCase本身。许多开发者——尤其是那些不了解设计模式的开发者——将会这样做,而不考虑这会使他们的类变得臃肿。他们必须添加功能,所以他们将功能添加到任何可以添加的地方。下面的代码可能就是他们的实现:

    public abstract class TestCase implements Test {
    private String fName;
    private int fRepeatTimes;
    public TestCase(String name) {
    this(name, 0);
    }
    public TestCase(String name, int repeatTimes) {
    fName = name;
    fRepeatTimes = repeatTimes;
    }
    public void run(TestResult result) {
    for (int i=0; i < fRepeatTimes; i++) {
    result.startTest(this);
    setUp();
    try {
    runTest();
    }
    catch (AssertionFailedError e) {
    result.addFailure(this, e);
    }
    catch (Throwable e) {
    result.addError(this, e);
    }
    tearDown();
    result.endTest(this);
    }
    }
    public int countTestCases() {
    return fRepeatTimes;
    }
    }

      请注意run(TestResult result)方法变大了一些。他们还为TestCase类添加了另外的构造子。到目前为止,这还不算什么大事。并且,我们可以说:如果这就是所有必须做的事情,那么使用Decorator模式就是多余的。

      现在,如果要让每个TestCase对象在其自己的线程中运行又怎样呢?这里也有一个可能的实现:

    public abstract class TestCase implements Test {
    private String fName;
    private int fRepeatTimes;
    private boolean fThreaded;
    public TestCase(String name) {
    this(name, 0, false);
    }
    public TestCase(String name, int repeatTimes) {
    this(name, repeatTimes, false);
    }
    public TestCase(String name, int repeatTimes, boolean threaded) {
    fName = name;
    fRepeatTimes = repeatTimes;
    fThreaded = threaded;
    }
    public void run(TestResult result) {
    if (fThreaded) {
    final TestResult finalResult= result;
    final Test thisTest = this;
    Thread t= new Thread() {
    public void run() {
    for (int i=0; i < fRepeatTimes; i++) {
    finalResult.startTest(thisTest);
    setUp();
    try {
    runTest();
    }
    catch (AssertionFailedError e) {
    finalResult.addFailure(thisTest, e);
    }
    catch (Throwable e) {
    finalResult.addError(thisTest, e);
    }
    tearDown();
    finalResult.endTest(thisTest);
    }
    }
    };
    t.start();
    result = finalResult;
    } else {
    for (int i=0; i < fRepeatTimes; i++) {
    result.startTest(this);
    setUp();
    try {
    runTest();
    }
    catch (AssertionFailedError e) {
    result.addFailure(this, e);
    }
    catch (Throwable e) {
    result.addError(this, e);
    }
    tearDown();
    result.endTest(this);
    }
    }
    }
    public int countTestCases() {
    return fRepeatTimes;
    }
    }

      唔,这看起来开始变得更坏了。为了支持两个新的特征,我们现在拥有了三个构造子,而且run(TestResult result)方法的大小迅速的膨胀起来。

      即使不管所有这些新代码,我们这些程序员还没有满足这些需求:我们仍然不能在各自的线程中重复运行测试。为了这个目的,我们必须添加更多的代码。算了,我就放过你吧。

      重构可以帮助这些代码减小尺寸。但是只需要稍做思考:如果再接到一个新的需求,我们要怎么办?现在JUnit 3.1支持四种不同的TestCase修饰器,它们可以轻松的随意组合以获取所需的功能。同时,JUnit的实现仍然简单——没有混乱的代码。这种设计保持TestCase类的简单、轻量级,用户只需要在需要的时候对TestCase对象进行装饰即可,而且可以选择任何组合顺序。

      很清楚,这是一个模式帮助简化设计的例子。这个例子也说明了缺乏经验的开发者怎样改善他们的设计——如果他们知道模式指出的重构目标。

      使用模式来开发软件是聪明之举,但如果你缺乏使用模式的经验,它也可能是危险的。出于这个原因,我极力提倡模式学习组。这些学习组让人们在同伴的帮助下稳步前进而精通模式。

      当人们了解模式并以受过训练的方式使用它们时,模式是最有用的——这种受过训练的方式就是XP的方式。以XP的方式使用模式鼓励开发者保持设计的简单、并完全根据需要对模式进行重构。它鼓励在设计早期使用关键的模式。它鼓励将问题与能帮助解决问题的模式相匹配。最后,它鼓励开发者编写模式的简单实现,然后根据需要发展它们。

      在XP的场景中,模式的确更有用;而在包含对模式的使用时,XP开发则更有可能成功。

    参考书目

    [Beck1 00] Beck, Kent. Email on extremeprogramming@egroups.com, January 2000.

    [Beck2 94] Patterns Generate Architectures, Kent Beck and Ralph Johnson, ECOOP 94

    [GHJV1 95] Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. 中译本:《设计模式:可复用面向对象软件的基础》,李英军等译。

    [GHJV2 95] Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides. Pages 353-354 中译本:《设计模式:可复用面向对象软件的基础》,李英军等译,第6章。

    [Jeffries 99] Jeffries, Ron. Patterns And Extreme Programming. Portland Pattern Repository. December, 1999

    [Kerth 99] Kerth, Norm. Conversation, circa March, 1999.

    [Kerievsky 96] Kerievsky, Joshua. Don’t Distinguish Between Classes And Interfaces. Portland Pattern Repository. Circa 1996

    2006年01月13日

    【总的思想】
    1、以业务层(领域模型)为核心
    2、高内聚、低耦合

    【特点】
    1、将DAO接口置于业务层;
    2、在数据访问层实现DAO接口,PO服务于DAO接口

    【分层】
    显示层-业务层-数据访问层(下面简称数据层)
    业务层:细分为事务层-操作层-实体层-DAO接口,DAO接口进一步分为读接口和写接口
    数据层:DAO实现+PO
    显示层:这个比较简单,用MVC框架,然后调用业务层即可

    【业务层与数据层的关系】
    数据层服务于业务层,业务层定义数据访问的接口(DAO接口),数据层来实现,数据层的PO是为了持久化的目的设计的,这也是PO唯一的存在价值,PO不属于业务层范畴,不具备反映业务逻辑的职责,它的活动范围被严格限制在数据层之内,换句话说,无论是业务层还是显示层都不能引用PO。

    是否设计PO与所使用的持久化技术有关,如果业务实体能够直接拿来持久化,而且性能没有问题,那么就不需要单独设计PO。总之PO属于数据层的实现细节,业务层的设计可以不考虑PO的任何问题,只管提DAO接口就是了。当然DAO接口只能用业务实体做参数,而不能有PO。

    【业务层的设计原则】
    事务层:包含事务的业务逻辑。必须定义接口。如果用Spring,则在此层绑定事务
    操作层:不包含事务的业务逻辑。必须定义接口
    实体层:包含简单的、操作自身和聚合边界内其它实体的业务逻辑。接口定义不是必须的。可变的、不稳定的业务逻辑通过在操作层、事务层应用策略模式等方式解决。
    DAO接口:接口的语义仅限于反映实体的持久化操作,比如create、update、delete、findByXXX,DAO接口不能带有业务语义。不遵守这个原则会造成业务逻辑与数据逻辑的混淆,不符合高内聚的最高原则

    【依赖关系】
    总的依赖方向:显示层->业务层<-数据层
    显示层可以引用:事务层、操作层、实体层和读的DAO接口,不可以引用写的DAO接口
    事务层可以引用:事务层、操作层、实体层和读写的DAO接口
    操作层可以引用:操作层、读的DAO接口和实体层
    实体层不可以引用其它任何层的接口,包括DAO,实体的操作限于其聚合边界内

    显示层还有一个有趣的技术就是,用Struts的话,将业务实体直接放到FormBean里面作为它的一个JavaBean属性,而不是根据页面定义一大堆String属性,页面通过x.x.x访问对象图的方式绑定数据,这样可以大大简化显示层的开发。(可能很多人都是这么用的,只是我们才刚刚醒悟过来)。至于有人提出的业务层一变,显示层会跟着变的问题,这就看你想让谁依赖谁了,你总要确定一个依赖方向,不可能谁也不依赖谁(变通的办法是再加一层,然后都依赖它,如果你愿意的话……),最开始就讲了,架构总的原则是以业务层为核心,这就决定了其它层都必定是依赖与业务层的,我实在是想不出为什么不能依赖业务层。

    关于Service,我不太喜欢这个概念(还有Facade),它真的能起到应有的作用吗?它究竟是让接口变简单了呢还是增加了系统整体的复杂程度?jdk那么复杂,也没什么Service层啊!我们用jdk或者Hibernate的时候就看它的api和reference,那么显示层用业务层的时候,也去看业务层的api和reference不结了?

    —————————————————

    包结构如下(按字母序):
    ###.daoimpl
    ###.daoimpl.po
    ###.domain.dao.readers
    ###.domain.dao.writers
    ###.domain.entities
    ###.domain.operations
    ###.domain.transactions
    ###.web

    —————————————————