架构那点事系列一 - 设计模式前章

                                        ----能够正视失败和错误,而不回避和掩盖,正是职业架构师的宝贵素养!!!

      首先,我们需要明白的是何为架构,以我看来,架构更多关注的是高层设计(所谓的high-level Design),它是一组有关如何确定软件系统的组织机构的重要决策。感兴趣的朋友可以查看这篇文章来深入理解一下架构的概念(http://baike.baidu.com/view/1147116.htm?fr=ala0_1)。那么,如何达到传说中的“架构之美”呢?我把它归为:软知识+想象力+创造力。软知识的积累过程中,必然少不了设计模式的学习。自己写这篇博文的目的,是希望和大家一同分享设计模式的美感,进行有效地模式挖掘。同时,在开源软件的今天,建造属于自己的轮子(我想这就是想象力加创造力最完美的结合吧) 

       正如题目所言,这篇博文是设计模式学习的前章。作为引子,我们必须精通OO理念,也为后续设计模式的学习,打下良好的基础。 

       我们都知道,面向对象设计的5大原则是指开闭原则(Open-Close Principle),单一职责原则(Single-Responsibility Principle),接口隔离原则(Interface Isolation Principle),里氏替换原则(Liskov Substitution Principle),依赖倒置原则(Dependence Inversion Principle),可是,我们当中又有多少人真正能领会到这5大原则之间不是相互独立的呢?真正领会到:其中的一个原则是另一个原则的基础或是加强。违反其中的任何一个原则,都有可能同时违反了其余的原则。而把面向对象设计理念运用到极致的就是我们所熟悉的GOF的设计模式,它是将OO思想运用到具体的软件编程开发实践中的最佳范例。下面,随我一起,走入OO世界吧。

 
       让我们首先看看开闭原则的一些基本概念,而后,我们会看到,在真实的项目中,我对它的理解与应用。随后的几大原则,我也打算用这种模式去叙述,同时将自己的感悟写下来,为了不误人子弟,希望大家带着批判的思维去审视我的设计。毕竟,敢于怀疑,才能获得真正的进步嘛,而我也很是需要这样的朋友。


       开闭原则(Open for extension. Closed for modification)中的Open,具体含义是:软件实体应该是可扩展的。Close则告诉我们,当变化来临时,我们应该避免对其进行修改。恩,避免对其修改?那怎么能面对变化,对其进行扩展呢,呵呵,这就是思想最为精妙之处。细细品来,它是说:软件设计人员应该拥抱变化,封装变化。不用修改原有实体(之所以我用实体来表述这个概念,是因为我觉得这个词的抽象层次很高,它包括小到类,大到模块等概念)就能对其进行扩展。具体点就是应用此原则,我们可以在不用修改代码(源代码,二进制可执行代码等)的前提下,对现有系统进行扩展和升级。想想看,在现实世界中,这样的例子真的又有多少呢?比方说,我们最为熟悉的机顶盒,使用它,我们可以将模拟电视扩展为数字电视,而不用对电视机内部结构进行调整,这就意味着,你大可不必去购物商场对你的视觉进行二次投资,省钱省心。再比如说,计算机主板上的内存扩展槽,呵呵,这个就不用我啰嗦了吧。总之,开闭原则其实就是对“可变性”的封装,遵守该原则,一方面要在软件系统中找出各种可能的“可变因素”,将它们封装起来;另一方面,在具体的编程实现中,一种可变性因素不应当散落在多个不同代码模块中,我们应当将它们封装到一个对象里。好,下面,我们看看,为了能够满足或者说遵守此原则,作为第一线的我们,又会采取什么措施呢?恩,在这里,我们通常采用面向接口编程或者是利用模板方法。


       面向接口编程,其实也遵从了依赖倒置原则。由于接口是相对稳定的,当需求发生变化时,可以提供该接口的新的实现类,以求适应变化。由于接口没有变化,所以依赖于该接口的客户端代码不需要被动的发生变化。这里面的设计技巧遍布Java Web的三大框架,最为经典是的Spring的Inversion of Control了,在更为一般的IOC设计理念中,通常采用Dependency Injection,或者是Service Locator,而Spring运用了前者,即我们所熟知的依赖注入(保留抽象的接口,让组件依赖于抽象接口,当组件要与其它实际的对象发生依赖关系时,可以通过抽象接口注入其所依赖的实际对象)。 

   

       利用模板方法模式编程。其实呢,模板方法模式中的模板类是一个抽象类。由于模板方法模式依赖于一个固定的模板类,因此它对于修改是关闭的。同时,通过从这个抽象类派生,也可以对此模板类的行为进行扩展。

 
       好,为了省事期间,我就没有附上诸多实例代码,毕竟,对于大多数人来说,如果有代码配合,可能理解的会更深。送走了第一个准则,我们迎来了单一职责。恩,单一职责,有点味道,这不是要求我们不要越俎代庖吗?呵呵,是啊,道理很相近。所谓SRP,它要求应用系统中的一个具体的类只完成某一类功能,并且尽可能避免出现一个“胖”(在一个类中完成多个不同的功能)功能的类。按照此原则设计应用系统中的类时,对于某个具体的类,应该仅有一个引起它变化的原因。很显然,如果我们严格遵守单一职责原则,那么就可以避免相同的职责分散到不同的类中,同时还可以避免一个类承担过多的职责。(注意:单一职责原则揭示了系统设计中“内聚性”和“耦合性”之间的正反关系。如何做到恰到好处的内聚,避免软件实体之间过高的耦合性,单一职责原则给出了我们最佳答案。呵呵,在工程实践中,这也取决于需求和软件设计师的经验)


       在应用系统的持久层设计中,我们通常将其拆分为数据实体类,数据访问逻辑,数据连接逻辑(通常情况下,我们使用XML文件或者是属性文件进行配置,好处自不待言)。为什么要这样设计呢?呵呵,我想每个编程老手都经历过一个蜕变的过程,在早期,它们都写过这样的代码:一个类文件,包含了对数据库操作的全部语句,比方说,数据库连接,数据库CRUD操作,数据库连接释放等。记得,在刚开始接触JDBC编程时,自己曾有过做万能连接的想法(主要是方便自己手动的配置数据库连接而已!),呵呵,当时也实现了部分数据库的部分操作(MS SQL 2000,Mysql,Access等)。简简单单一个类,可读性也不差,可扩展性,可维护性也都还好(尽管如此,但我还是不推荐这样的做法,要知道,多花点时间在前期的设计上,这样的投资绝对值,它会给是你的后期维护变得不那么举步维艰!)。呵呵,可是后来发现很多持久层框架,都已经为我们做了这些工作,不仅如此,它们通常还提供数据库连接池功能,并且使用了配置文件。其实背后都蕴藏着单一职责的设计原则,只是当时才疏学浅,未能吃透。呵呵,慢慢来嘛,大师曾经都是小工的!值得我们注意的时,单一职责原则在模板方法模式中的具体应用主要体现在其模板类的设计上,通常它定义出所有子类的共同行为,然后由它的各个子类单独完成各自的行为。模板方法模式解决问题的思路为“通用的功能我来实现,特殊的需求交由你处理”!


       利用面向对象技术中的继承机制,能够减少代码的重复编程实现,从而实现系统中的代码复用。如何合理而正确地进行继承,Liskov替换原则给出了我们良好的指导。在进行派生类的设计时,我们要保证它能与基类相容。具体说,就是基类的抽象方法都要在其子类中实现,并且一个具体的派生类只实现其抽象类中声明的方法,而不应当给出多余的方法定义或实现,这样可以实现动态绑定。(经验之谈:只有派生类完全替换基类,才能保证系统在运行期动态进行类的类型识别,也就是可以实现动态多态形式的调用)。


       再啰嗦几句吧!由于面向对象编程技术中的继承在具体的编程中过于简单,在许多系统的设计和编程实现中,我们并没有认真地,理性地思考应用系统中各个类之间的继承关系是否合适,派生类是否能正确地对其基类中的某些方法进行重写等问题。因此经常出现滥用继承或者错误的进行了继承等现象,给系统的后期维护带来难以估计的麻烦。所以我们应当在自己的编程中,不断思考,不断提炼,这样才能做出真正的OO产品来。

 
       好了,让我们看看什么是接口隔离原则吧!其主要思想为:一个类对另一个类的依赖关系应当建立在最小接口上,使用多个专门的接口比使用单一复合总接口要优越。可以看出,接口隔离原则其实是单一职责应用于接口设计上的细化准侧。那么,我们如何做,才能更好地遵守此原则呢?我觉得,应当将完成一类相关功能的各个方法放在同一个接口中,形成高内聚的职责。大接口可以根据功能分类分割成若干个不同的小接口,具体可以应用接口的多重继承来分离接口(可以看看Spring框架的设计大师们是怎么做的)。


      传统面向过程的软件系统设计方法是“自顶向下,逐步细分”的,这种设计方法的核心思想是将一个大的业务功能分解为一些小的功能。从具体的实现来看,设计者往往用一个main函数概括出整个应用程序需要完成的各个功能模块(子函数),各个功能模块的实现又可以细化为更小的子函数。函数之间通过调用关系来实现系统的整体功能。但是这样的设计思想和设计方案将导致整个应用系统的核心逻辑依赖与其外延的细节,程序本应该是比较稳定的核心逻辑,但因为依赖于易于变化的部分,变得不稳定,一个细节上的功能实现的小小改动,也有可能在依赖关系上引发一系列的变动和修改。这种依赖关系也是面向过程程序设计不能良好地处理变化的原因之一,而一个合理的依赖关系,应该是倒过来的,即细节实现依赖与核心逻辑,如何达到这样的设计目标呢,好,该是依赖倒置原则出场的时候了。

 
       依赖倒置原则是指应用系统中的高层模块不应该依赖于底层模块,抽象不应该依赖于细节。为了使应用系统中的各个类在设计时能够依赖于抽象而不是依赖于具体的实现,我们应该针对接口编程,而不是针对具体的实现类进行编程。具体说来,消除两个模块间的依赖关系,应该在这两个模块之间定义一个抽象的接口,上层模块调用抽象接口中定义的方法,下层模块则具体实现该接口。 


       依赖倒置原则的本质就是使用抽象来使应用系统中的高层模块独立于系统中的低层模块,以实现应用系统中高层模块的自由复用。由于接口是属于高层的,而低层要实现高层提供的接口。因此,我们现在的设计思想是低层依赖于高层,是依赖关系和接口所有权的倒置。


       我们知道,Java EE平台中有许多轻量级的框架系统,在设计初期,它们就很好地考虑到了自身的扩展问题。从满足依赖倒置原则的角度来看,需要框架的核心功能是不应依赖于框架具体使用者的不同功能需求(正所谓“抽象不依赖于实现”)。比方说Struts中的PlugIn接口,Spring中的众多模板方法模式的应用,所有这些都很好的诠释了本原则。 
       由于抽象类具有提供默认实现的优点,而接口具有松散的扩展等优点,因此我推荐大家综合使用面向对象语言中的接口和抽象类,那么如何才能更好地结合两者以设计出更为优秀的代码呢?首先,让接口负责方法的定义,但同时给出一个默认实现(使用抽象类,类似于适配器模式中的适配器类),其它同属于这个抽象类的具体类可以选择实现这个接口,或者选择继承这个抽象类(这两种实现方式有区别吗?你是怎么理解的?可以给我留言,写下自己的观点,呵呵)。如果需要对系统中的功能进行扩展,则只需要向接口中加入新的方法,同时向这个抽象类中添加方法的具体实现即可,因为继承自这个抽象类的子类都会从这个抽象类中得到具体的方法。

 
       但是,如果我们严格遵守依赖倒置原则,则可能导致在整个系统中产生大量的接口,此外,由于依赖倒置原则假定所有具体类都是可能变化的,这在实际系统开发过程中也不总是正确的。如果所依赖的某个具体类是比较稳定的,那么直接使用或者依赖于这个具体类也不会给系统带来不良后果。这一点,不是正如我们对Java平台中JDK API的依赖吗?(或者说对.Net平台的SDK API依赖)


       好了,我们的OO之旅暂且告一段落,还等什么,赶紧拿起上述准则,对原有系统进行重构吧!呵呵,如果你能将5大准则铭记于心,并能灵活运用,那么我要恭喜你了(明天的OO大师就是你),呵呵!然而,要想成为真正的大师,还是有很长的路要走的。乐观固然是好事,但是要正视眼前的困难。同样,要想设计出多层架构,松耦合的系统,还有很多很多要学,下面,我就将自己在设计模式上的学习经验,毫无保留地拿出来与大家分享,希望能够抛砖引玉!


      在某个具体的软件项目中我们可以不使用门面模式,不使用桥模式,不使用观察者等其它模式,就能够完成系统功能的实现,只不过可能会在代码实现方面出现一定的复杂性。但是,我们必须将现实世界的实体和业务功能进行抽象,并分配各个类的具体职责。而类和对象的设计,关联关系的确定以及具体的职责分配又依据什么呢?是否有一定的设计准则来指导软件设计人员呢?经典的通用职责分配软件模式(General Responsibility Assignment Software Pattern),可以给予我们方向性的指导。


      GoF设计模式是一种高效,灵活的设计准则,但问题是在具体应用此模式之前,设计人员必须先设计和确定本应用系统中应该有哪些类,并确定类与类之间的关联关系,类职责的分配等,才能应用GoF设计模式来优化实现的代码。所以我想在讲述GoF设计模式之前先来回顾一下GRASP模式。通用职责分配软件模式是描述如何把职责分配给不同对象的有效经验和基本准则,它包含5个基本模式(信息专家模式,创建者模,高内聚模式,低耦合模式和控制器模式)和4个扩展模式(多态模式,纯虚构模式,中介模式,受保护变化模式)。

 
       软件系统的设计人员在完成类职责的设计时,如果发现某个类拥有完成该职责需要的所有信息(数据),那么这个职责就应该分配给这个类,这个类就是相对于这个职责的信息专家(Information Expert)类。在实际的软件开发中,我们还要辨别类的职责是协作完成还是应该独立完成(即需要一个信息专家还是需要多个不同的信息专家协同完成)。

 
       再来看看创建者模式,创建者模式的主要作用可以概括为如下两点:封装创建逻辑的细节和封装创建逻辑的变化。我个人的理解是:GoF设计模式中的单例模式,工厂方法模式,抽象工厂模式,原型模式,生成器模式都属于创建者模式的具体实现。别急,关于这些模式的理解,我会在后面娓娓道来。

 
       至于低耦合和高内聚模式,我就不多做解释了。我觉得这里面更多是经验的总结。如何做到低耦合,如何做到高内聚,这本身就是个很难解答的问题(一切服从于需求嘛),我们要做的就是不断从实践中提取设计准则,所谓的最佳实践,无非是前人大量经验的总结。在这,我给大家一个指导性的方针,不过在实际应用中,大家要慎重!好了,准则如下:对于那些对系统稳定性要求比较高且系统不容易发生变化的需求,我们完全可以将其设计成紧耦合的。这样可以提高效率,提高代码的内聚性,减少类的数量。反之,我们在设计上,尽量考虑松耦合。而降低类与类之间耦合度的一个常用技术手段就是应用抽象层次。

 
      再啰嗦几句,在应用系统的类设计中,为了达到松耦合的设计目标,我们经常采用的设计策略一般包括:依赖倒置原则,控制反转等。

 
      但是,请注意:依赖倒置原则描述的是类与类之间代码级的依赖关系。如果应用系统是基于某种框架系统开发的,则该应用系统中的类对目标框架的依赖关系就会更强。这时,控制反转的思想应运而生。那么控制反转又为何物呢?它是说将控制权(原本由程序来控制对象之间的依赖关系)由应用程序转移到了外部容器。利用控制反转能够减少对象请求者对服务提供者的特定实现的逻辑依赖,因为组件类不需要查找或者实例化它们所依赖的组件类。


      好了,今天就先啰嗦到这吧~


参考文献:

1.http://www.ibm.com/developerworks/cn/education/java/j-patterns201/index.html

你可能感兴趣的:(架构之魂,设计模式,编程,扩展,持久层框架,数据库连接池,spring)