设计原则与设计模式

   学习笔记(设计模式): 

   糟糕的软件常具有以下缺陷原因,良好的软件系统应具有以下性质:

        缺陷                  目标

过于僵硬rigidity -->可扩展性extensibility

过于脆弱fragility -->灵活性flexibility

粘度过高viscosity -->可插入性pluggability

复用率低immobility

 

     低层次的复用一般体现在 代码复用,数据结构复用,算法的复用。面向对象的设计强调复用的焦点不在于低层次的复用上,而是抽象层次的复用,即模块,稳定的商业逻辑的复用。抽象化,封装,多态可以实现和促进系统的可扩展性,灵活性,可插入性从而可以提高系统的可维护性。可维护性和复用性是系统的俩个特性,它们的关系如下:
设计原则与设计模式

     面向对象的设计提高系统的可复用性,可维护性的原则有如下:

          开闭原则Open-close principle(Ocp)

          里氏替换原则Liskov Substitution principle(Lsp)

          依赖倒转原则Dependency Inversion principle(Dip)

          接口隔离原则Interface segregation principle(Isp)

          组合/聚合复用原则composition/Aggregation principle(Carp)

          迪米特法则Law of demeter(Lod)

      这些原则可以提高系统的可复用性,从而提高系统的可维护性。6大设计原则指导下的设计模式可以分为3类:创建模式,结构模式和行为模式。

 


       开闭原则ocp,是说一个软件系统对扩展开放,对修改关闭。

          a. 已有的系统,可以提供新的行为,满足新的需求,系统具有一定的适应性和灵活性。

          b.已有的系统,特别是重要的抽象层模块不能再修改,这使系统变化中的系统具有一定的稳定性和延续性。

       即不应该更改系统的抽象层,而容许扩展系统的实现层。

       满足则开闭原则的关键是抽象化,在面向对象的Java语言里,可以给系统定义出一个一劳永逸,不在更改的抽象设计,此设计容许有无穷无尽的行为在现实层被实现。通过一个或多个抽象java类或java接口,规定出所有的具体类必须提供的方法特征(signature)作为系统设计的抽象层。这个抽象层预计了所有的可能扩展。因此任何扩展情况下都不会改变。这满足开闭原则的OCP的第二条对修改关闭。

       同时,由于从抽象层导出一个或多个新的具体类可以改变系统的行为,因此系统的设计对扩展是开放的。这满足开闭原则OCP的第一条对扩展开放。

 

         对可变性的封装原则,找到系统的可变因素,将它封装起来。 从工程的角度实现开闭原则OCP。

         a.一种可变性不应该散落在代码的很多角落里,而应该被封装到一个对象里面。同一种可变性的不同表象意味着同一个继承等级里中的具体子类。考虑继承复用。继承应该是看做封装变化的方法,而不应该是从一般对象生成特殊对象的方法。

        b.一种可变性不应该与另一种可变性混合在一起。继承的层次不应太深。

    

        开闭原则OCP与其他设计原则的关系:

        开闭原则是目标,其他设计原则是实现目标的手段。

        里氏替换原则:任何父类可以出现的地方,子类一定可以出现。实现开闭原则的关键步骤就是抽象化。而父类与子类的继承关系就是抽象化的具体体现,所以里氏替换原则是对实现抽象化的集体规范。

       依赖倒转原则:要依赖与抽象,不要依赖于实现。开闭原则是目标,依赖倒转是实现的手段。实现开闭原则就应该依赖于抽象,坚持依赖倒转原则。

       合成/聚合复用原则:尽量使用合成聚合实现复用,而不是继承关系实现复用。  合成/聚合复用原则和里氏替换原则都是实现开闭原则OCP的具体步骤规范。要求有限使用合成聚合复用,其次在考虑继承复用(里氏替换)。

        实体间的通信原则:迪米特原则和接口隔离原则

        迪米特法则:一个软件实体尽可能少的与其他实体发生相互作用。 减少交互容易做到开闭原则OCP的对修改关闭(不修改抽象层).

         接口隔离原则:应该为客户端提供尽可能小的单独接口,而不要提供大的总接口。 接口隔离原则与迪米特法则都是对一个软件实体与其它实体的通信做出限制。接口隔离原则限制的是实体通信的宽度,即通信尽可能的窄。广义的迪米特法则要求尽可能的限制对通信的宽度和深度。

        遵循迪米特法则和接口隔离原则的系统,在扩展的过程的不会将修改的压力传递到其他的对象。

 

         Java语言的接口

         通用的接口是一种逻辑上的抽象,接口是实现构件可插入性的关键,用于系统间通信,实现系统的可扩展性。

        Java的接口是一些方法特征的集合,接口只有方法的的特征,没有方法的实现,接口在不同地方实现时,具有完全不同的行为。Java的接口具有public,static,final的常量属性。 Java的抽象类可具有某些方法的具体实现,涉及到表象,所以Java的接口比抽象类更抽象化。Java的接口常代表一个角色role,它包装与该角色相关的属性和操作,而实现这个接口的类便是扮演这个角色的演员。一个角色可以有不同的演员来演,不同的演员除了扮演共同的一个角色外,并不要求有其它的共同之处。

        Java的对象需要使用其它的对象的一些行为完成一项工作时,需要与其它对象发生相互作用。 如果使用硬编码(直接写死要借用的其它对象),则没有一点扩展性。  要动态的扩展一个具体类的功能,可以考虑使用抽象类,父类声明方法,多个子类去实现。客户端可以动态的决定使用哪一个子类,可提供一定的扩展性。但是Java是单继承的语言,要扩展的具体子类的父类被占用的情况下,只能向上一级移动,直到移动到类等级结构的顶端,这样一来,对一个孙孙辈子类的功能扩展,变成了对整个等级结构中类的修改。 现实情况中某些父类是第三方提供的无源码的包,这种扩展就有困难。java对象的最高父类是java.lang.object。 这是对孙辈的具体子类的使用接口来扩展,影响的只是这个孙辈的类及其子类,不会影响到孙辈类的父类。具有比较灵活的插入性。

       Java设计师应该使用Java接口/抽象类来声明一个类型,如变量的类型声明,参量的类型声明,方法的返回类型声明,以及数据类型的转换等。一个更好的做法是只使用Java接口而不用Java抽象类来完成如上所述的声明。理想情况下一个具体的子类应该只实现Java接口和抽象类中声明过的方法,而不应该给出更多方法。接口声明属性可实现关联属性的可插入性,接口调用方法可实现调用的可插入性。

       Java接口(抽象类)一般作为一个类型等级结构的起点。子类型的继承关系具有传递性,如类型甲是类型乙的子类型,类型乙是类型丙的子类型,类型甲是类型丙的子类型。

      混合类型的类,如果一个类已经有一个主要的父类型,通过实现一个接口,这个类可以拥有另外一个次要的父类型,这种次要的父类型既是混合类型。当一个类处于一个类型等级结构中的时候,为这个具体类定义一个混合类型是保证基于这个类型可插入性的关键。Java接口是实现混合类型最为理想的工具。如TreeMap   

java.lang.Object
  extended by java.util.AbstractMap<K,V> implements  Map<K,V>,
      extended by java.util.TreeMap<K,V>  implements Serializable,Cloneable,NavigableMap<K,V>,SortedMap<K,V>

      AbstractMap是主要的父类型,Serializable,Cloneable,NavigableMap<K,V>,SortedMap都是它的次要类型,次要类型扩展了TreeMap的行为和功能。

      Java的接口有:单方法接口如Runnable,只有一个run方法。

                                标识接口如java.io.Seriable,java.rmi.Remote

                                常量接口如:ObjectStreamConstants,Calendar

 

       Java抽象类:可以有实例变量,及一个或多个构造方法,但是不可实例化.同时拥有抽象方法和具体方法,仅提供一个类型的部分实现,虽然拥有构造方法但是不可实例化,构造方法是用于子类调用的,从而使一个抽象类的所有子类可以有一些共同的实现。而不同的子类在此基础上可以有自己其它的实现。抽象类和子类的这种模式实际上是模板方法模式的应用。抽象类是拥有商业逻辑实现的对象模板,用来继承的。在一个以继承关系形成的等级结构里,树叶节点都应该是具体类,树枝节点都应该是抽象类。在一个以抽象类到具体类的继承关系中,共同代码应该尽量移动到抽象类中,以提高复用率。与代码移动的方向相反的是,数据移动的方向是从继承结构的高端向低端移动,即从抽象类到具体类。即抽象类中具体的属性尽量少。

       针对抽象编程,不要针对具体编程。依赖倒转原则指出了抽象类对代码复用的一个重要作用。

       优先使用合成复用,其次继承复用. 这是组合/聚合复用原则指出的。

       模板方法模式的根本就是关于继承的模式。

       继承代表 ’一般化/特殊化‘ 的关系。父类代表一般,子类代表特殊。子类将父类特殊化或扩展化。

       里氏替换原则是可否使用继承关系的准绳(父类和子类替换使用原则)。扩展的讲,也就是要满足以下条件才应该使用继承关系:

          a.子类是父类的一个特殊种类,而不是父类的一个角色。也就是‘Is-a’关系才符合继承。‘Has-a’关系应当使用聚合关系描述。

          b.永远不会出现需要将一个子类替换为另外一个子类的情况。如果不是很确定一个类会不会再将来会成为另一个类的子类的话。就不应该将这个类设计为当前父类的子类。

          c.子类具有扩展父类的责任。而不是置换(Override)或者注销掉(Nulllify)父类的责任。如果子类需要大量置换掉父类的行为,那么这个子类不应该成为这个父类的子类。

          d.只有在分类学上有意义时才使用继承。不要从工具类继承。工具类必定具有自己的行为特性,一个封装了商业逻辑的商业类型,不可能是工具类型的一种。

 

         开闭原则OCP指出面向对象的设计的重要原则是创建抽象化,并且从抽象化导出具体化。具体化可以给出不同的版本,每一个版本都有不同的实现。从抽象化到处具体化要使用继承关系, 需要遵循里氏替换原则。里氏替换原则的严格定义:如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的多有程序p在所有的对象o1都替换为o2时,程序p的行为没有发生变化,那么T2类型为T1类型的子类型。换言之,一个软件实体使用的是一个父类的话,那么一定适用于其子类,程序P不会觉察出父类和子类的区别。

       里氏替换原则是父类和子类替换的原则

       里氏替换原则是继承复用的基础。只有当衍生的子类可以替换掉父类,软件单位的功能不受影响时,父类才能真正的被复用,子类才能在父类的基础上添加新的行为。遵循里氏替换原则的子类需要具备父类型的所有接口,而且可能比父类的接口范围更宽。

       里氏替换原则是通过Java编译器的编译检查来保证的。描述一个物体的大小的量有精度和准确度俩种属性。所谓精度就是这个数字的量有效数字是多少位。所谓精度是这个量与真实的物体大小相符合到什么程度。一个量可以有很高的精度,但是却无法与真实物体的情况 相吻合。Java的编译器能检查的,仅仅是相当于精度的属性而已,它无法检查这个量与真实物体的差距。换言之,里氏替换原则不能检查一个系统在实现和商业逻辑上是否满足里氏替换原则。

       策略模式Strategy体现了里氏替换原则,如果有一组算法,那么就将每一个算法封装起来,使得它们可以互换。所有的算法封装后可以实现互换。则需要将所有的具体策略角色放到一个类型等级结构中,使它们拥有共同的接口。如AbstractStrategy s = new AbstractStrategy();

      合成模式Composite体现了里氏替换原则,合成模式通过树结构描述整体与部分的关系,从而将单纯元素和复合元素同等看待。由于单纯元素和复合元素都是抽象元素角色的子类,所以都可以替代抽象元素出现在任何地方。

       代理模式Proxy体现了里氏替换原则,代理模式给某一个对象提供一个代理对象,并有代理对象提供对元对象的引用。代理模式能够成立的关键,就在于代理模式与真实主题模式都是抽象主题角色的子类。客户端只知道抽象主题,而代理主题可以替换抽象主题出现在任何需要的地方。将真实主题隐藏在幕后。

 

       里氏替换原则将的是父类与子类的关系,只有具备父子关系时,里氏替换原则才成立。反之不成立。

       正方形是不是长方形的子类? 正方形是长宽都相等的长方形。  但是长方形是具体类,从设计角度讲要尽量从抽象类继承,而不从具体类继承。 一般而言,如果有俩个具体类A和B有继承关系,那么一个最简单的修改方案是建立一个抽象类C,让A和B都成为C的子类。  这是一个重构方法。
                                   设计原则与设计模式
      

       依赖倒转原则Dependency Inversion principle:要依赖与抽象,而不要依赖与实现编程。反转的含义是来源于与传统的编程方法的比较。 Corba,EJB,JavaBean等构建设计模型就是依据于依赖倒转原则。要依赖于宏观的商业逻辑,战略决策,而不是具体的实现细节代码。 提倡复用抽象的宏观部分。因为宏观部分是比较稳定和持久的。而实现细节是多变的。

      3中耦合关系:

      a.零耦合Nil Coupling:俩个类直接没有交互

      b.具体耦合Contrete Coupling:俩个可实例化的类之间,经由一个类对另一个类的直接引用造成。

      c.抽象耦合Abstract Coupling:一个具体类和一个抽象类/接口直接的耦合,具有较大的灵活性。

      不要依赖实现编程的含义是指:不应当使用具体的类进行变量类型声明,参量类型声明,方法的返回类型声明,以及数据类型转换等。

       要保证做到这一点,一个具体的Java类应该只实现Java接口和抽象Java类中声明的方法,而不应当给出多余的方法。 这样的系统实体之间的关系具有灵活性。

       只要一个被引用的对象存在抽象类型,就应当在任何引用次对象的地方使用抽象类型,包括参量的声明类型,方法返回类型声明,属性变量的声明类型等。

       以抽象类型耦合是实现依赖倒转原则的关键。

       通过抽象类型的耦合虽然具有灵活性,但是也带来额外的复杂性。如果一个具体类发生变化的可能性非常小,使用具体耦合反而会更好。

       Java的实例化对象的方法要求必须通过new 具体对象的构造方法实现,无法实现依赖抽象编程的原则,工厂方法模式,回避了这个问题。

       模板方法模式支持依赖倒转原则。

       依赖倒转原则的缺点:a.由于依赖关系倒转的缘故,对象的创建很可能要使用对象工厂,避免对具体类的直接引用,这样会产生大量的辅助类。 b.依赖倒转原则假定所有的具体类都是会变化的,这也不总是正确的,有些具体类是相当稳定的,不会发生变化的,所以客户端消费者可以依赖这个具体类,而不必为此类构造一个抽象类。

       

      

       抽象类和接口:

       a.抽象类的优点是加入一个新的具体方法,所有的子类都可以得到这个方法。接口做不到这点。

       b.Java的单继承没有接口的扩展性强大。

       由于抽象类具有提供默认实现的优点,接口具有扩展的优点。联合俩者使用是一个好的选择。首先,声明类型的工作有接口承担。但同时提供一个Java抽象类,为这个接口提供一个默认的实现。子类可以继承抽象类也可以实现接口。 继承抽象类可以自动得到方法默认的实现。实现接口需要自己实现这些方法。

      如果需要向接口中加入一个新方法的话,同时向抽象类中加入一个这个方法的具体实现即可。所有继承自这个抽象类的子类会自动从抽象类得到这个具体方法。 这就是缺省适配模式Default Adapter。 Java的API中也是用这种方式,抽象类的名为Abstract + 接口名。如接口Collection,抽象类为AbstractCollection。联合使用接口和抽象类俩者的优点,客服它们的缺点。

   

       接口隔离原则ISP:使用多个专一接口比使用单一个总接口要好。换言之,一个类对另外一个类的依赖应建立在最小的接口上的。

        接口:广义的接口是一个类型所具有的方法特征的集合,是一种逻辑上的抽象。狭义的接口是指Java语言中定义的Interface结构。广义的接口划分就直接带来类型的划分。一个接口相当于剧本中的一种角色,而此角色具体在一个舞台上由哪一个演员来演则相当于接口的实现。因此一个接口应当简单地代表一个角色,而不是多个角色。如果系统涉及到多个角色的情况,每个角色都应当有一个特定的接口来代表。划分角色的原则叫做角色隔离原则。

       将接口理解为狭义的Java接口,接口隔离原则讲的就是为同一个角色提供宽,窄不同的接口,以应对不同的客户端消费者。这种做法在服务行业叫做定制服务Customized Service。  这是适配器模式的应用。如右图:              设计原则与设计模式

       由于每个接口都代表一个角色,实现一个接口的对象,在它的整个生命周期中都扮演这个角色。因此将角色区分清楚就是系统设计的一个重要工作。因此,一个符合逻辑的推断,不应当将几个不同的角色都交给同一个接口,应该交给不同的接口。否则就是对接口的污染Interface Contamination。
        接口隔离原则与迪米特法则的关系:迪米特法则要求任何一个软件实体,除非绝对需要,不然不要与外界通信。即使必须进行通信,也应当尽量限制通信的广度和宽度。定制服务拒绝向客户提供不需要的提供的行为,符合迪米特法则。

        例子:全文查询引擎的系统设计: 用户通过一个或数个关键词进行全网站的搜索。在数据库等数据源的数据被crud的同时,查询引擎的索引文件也需要相应的更新。根据接口隔离原则,应该划分为:搜索器角色,索引生成器角色,搜索结果集角色。

        备忘录模式Memento Pattern的用意是在不破坏封装的条件下,捕捉一个对象的状态并将之外部化,从而可以在将来合适的时候把这个对象还原到原来储存起来的状态。 简略的类图如下:
设计原则与设计模式

 不破坏封装是一个关键。为了做到这一点,备忘录模式需要向外界提供双重接口,即一个宽接口和一个窄接口。宽接口是为发起人角色准备的,因为这个备忘录角色所存储的状态就是属于这个发起人角色的。而且这个角色需要访问备忘录角色所存储的的信息以便恢复自己的状态。窄接口是为包括负责人角色在内的所有其它对象准备的,因为它不需要,也不应该读取备忘录角色所存储的信息。换言之,发起人角色和负责人角色就相当于备忘录角色的不同客户端。这种为不同客户端提供不同服务的方式就是定制服务的概念。

      迭代子模式Iterator:集合对象向不同的客户端提供了不同的接口,一个是宽接口,提供给迭代子对象,另一个是窄接口,提供给客户端使用。客户端只需要迭代集合内部的数据,而迭代子对象不但需要访问集合内部的数据,而且需要知道集合对象的内部结构等信息。而集合对象的内部结构信息客户端是不需要知道的。

     ‘看人下菜碟’,从接口隔离原则讲,就是一种定制服务。


       合成/聚合复用原则Composite/Aggregate reuse principle,又叫合成复用原则。就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。另一种表述:要尽量使用合成/聚合,尽量不要使用继承。

       合成Composite和聚合Aggregate都是关联Association关系的特殊种类。聚合表示一种拥有关系,整体与部分的关系,整体与部分的生命周期彼此独立。组合表示一种强拥有关系,整体和部分具有一致的生命周期,即组合对象对组成部分的内存分配,内存释放具有绝对的责任。

       合成复用的好处:a.新对象存取成分对象的唯一方法是通过成分对象的接口。

                                    b.这种复用是黑箱复用,因为成分对象内部的细节是新对象所看不见的。

                                    c.这种复用支持封装。

                                    d.这种复用所需的依赖较少。

                                    e.每个新的类可以将焦点集中在一个任务上。

                                    f.这种复用可以在运行期动态进行,新对象可以动态地引用于成分对象类型相同的对象。

       一般而言,如果一个角色得到了更多的责任,那么可以使用合成关系将新的责任委派到合适的对象。

        合成复用的缺点:会有较多的对象需要管理。

        合成复用可以使用的场景要不继承复用广泛的多。继承复用只适合某些场景。

        继承复用的子类通过增加新的属性和方法来扩展父类的实现。继承复用是类型的复用。继承复用的优点: a. 实现较容易,父类的大部分功能可以通过继承进入子类。

        b. 修改或扩展继承而来的实现较为容易。

        继承复用的缺点:a.继承复用破会封装。父类的内部细节常对子类是透明的,是白箱复用。

                                     b.如果父类的实现发生变化,子类的实现也必须跟着改变。

                                     c.从父类继承而来的实现时静态的,不可能在运行期改变,没有足够的灵活性。

        

          对违反里氏替换原则的设计进行重构时,一般有俩种方式:一是加入抽象父类;一是将继承关系改成合成关系。

        区分Has-A和Is-A,Is-A是严格的分类学上的定义,意思是一个类是另一个类的一种。而Has-A则不同,它表示某一角色具有某一项责任。

        里氏替换原则是继承复用的基础。Java的API中也有违反里氏替换原则的案例。如:Stack被不当设计为Vector的子类,Properties被不当设计为HashTable的子类。

 

        迪米特法则Law of Demeter(LOD),又叫最少知识原则:一个对象应当对其他对象有尽可能少的了解。如果俩个类不必彼此直接通信,那么这俩个类就不应当发生直接的相互作用。确定交互对象的条件:

       a.当前对象this

       b.以参量形式传入到当前对象中方法中的对象

       c.当前对象实例变量直接引用的对象

       d.当前对象的实例变量如果是一个集合,那么集合中的元素也都是朋友

       e.当前对象所创建的对象

       满足上述条件的就是‘朋友’,反之则是‘陌生人’。‘陌生人’不应该发生直接关联。

       某人调用朋友的方法,朋友调用陌生人的方法,通过这种调用转发,实现某人对陌生人的低耦合。

       狭义迪米特法则的缺点:会在系统里构造出大量的小方法,散落在系统的各个角落。这些方法只是传递间接的调用,与系统商务逻辑无关。遵循迪米特法则会使一个系统局部简化,因为每一个对象都不会和远距离的对象发生直接的关联。这也会造成系统不同模块间通信效率的降低,使得模块之间的协调趋于复杂。

       门面模式Facade:子系统外部的对象和内部对象不应该直接通信,而应当通过双方都认可的的朋友,也就是门面对象进行通行。门面模式创造出一个门面对象,将客户端所涉及的属于一个子系统的协作伙伴的数目减少到最少,使得客户端与子系统内部的对象的相互作用被门面对象所取代。门面模式是实现迪米特法则的一个强大武器。

          调停者模式Mediator:一些对象形成一个中等规模的‘朋友圈’,可以通过创造出一个大家共有的朋友对象,大家都通过这个共有朋友对象发生相互作用,而将相互之间的互相作用省略掉。

 

      广义的迪米特法则:是对对象之间的信息流量,流向以及信息的影响的控制。一个模块设计的好不好的主要标记是多大程度上将自己内部数据和其它实现有关的细节隐藏起来,即封装的好不好。信息的隐藏非常重要的原因在于解耦,从而可以使模块独立的使用和修改。即模块化开发。信息的隐藏可以促进软件的复用。迪米特法则主要的用意在于控制信息的过载。在系统设计时应该注意以下几点:

      a.在类的划分上应当创建弱耦合的类。类之间耦合越低,就越利于复用。

      b.在类结构的设计上,每一个类都应当尽量降低成员的访问权限。Accessibility。

      c.在类的设计上,只要有可能,一个类应当设计成不变类。

      d.在对其它对象的引用上,一个对象对其它对象的引用应当降低到最低。

      对于内部状态不会变化的类,优先设计为不变类。

      对于访问权限为protected的类,任何其它包内的子类都可以访问它。

      被标记为Serializable的类内部结构不能再有变更,否则无法反序列化。

      限制局部变量的有效范围。

      迪米特法则是设法使一个软件系统的不同对象彼此之间尽量“老死不相往来”,降低系统维护成本。

 

    
     

 

 

 

你可能感兴趣的:(设计模式)