设计模式6大原则杂谈

1,927 阅读15分钟

  总原则:开闭原则(Open Close Principle)

 1、单一职责原则

 2、里氏替换原则(Liskov Substitution Principle)

 3、依赖倒置原则(Dependence Inversion Principle)

 4、接口隔离原则(Interface Segregation Principe)

 5、迪米特法则(最少知道原则)(Demeter Principle)

 6、合成复用原则(Composite Reuse Principle)


类的划分: 从类组成来看、划分为属性的操作,行为的操作。

设计模式六大原则的须知:


一、单一职责原则

1、1在某些时候不能遵守单一职责时,那就放心地破坏。记住,教条是死的,生活是多变的。


违反单一职责原则的例子: 单例模式

为何违反了单一职责原则: 单例类的模式的职责过重(在一个方法中进行了创建类和提供类对象的操作)

单例模式的职责: 1、 某一个类只有一个实例。

2、它必须自行创建这个实例。

3、它必须向整个系统提供这个实例。

单例模式的实例(静态内部类):


public class Singleton
{
       private  Singleton()
       {
       }

       private static class HolderClass
       {
              private final static Singleton  instance = new Singleton();
       }

       public static Singleton getInstance()
       {
              return HolderClass.instance;
       }
}

执行过程解释

第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。 通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能。


1、2 使用单一职责原则的好处:

1)降低了类的复杂度;

2) 提高类的可读性,提高系统的可维护性;

3)降低变更引起的风险(降低对其他功能的影响)。


1、3 违反单一职责原则的弊端:

1)当一个类有多于一个的职责,多个职责耦合到一起,一个职责的变化可能会影响到其他的职责。

2)一个类的职责越多,复用能力越弱。

3)单一职责原则提出了一个编写程序的标准,用"职责"或"变化原因"来衡量接口或类是否优良,但"职责"和"变化原因"都是不可度量的,因项目、环境而已。


1、4 单一职责原则的使用范围:

单一职责原则适用于接口、类、同时也适用于方法。即一个方法尽可能做一件事情。


1、5 至于如何识别破坏了单一职责原则,可以从以下方面考虑:

1、类拥有太多依赖(构造器太多参数)

2、方法拥有太多参数

.......

1、6 什么情况下违反单一职责原则才合适?

1、一个类中方法比较简单,无论你是代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。但是因为实际中的类通常比较复杂,一旦发生职责扩散而需要修改类时,除非这个类非常简单,否则还是遵循单一职责原则的好。


1、7 单一职责原则的重构:

业务规则和持久化两个职责应该分开:业务规则往往频繁变化,而持久化的方式却不会如此频繁的变化,并且变化的原因完全不同。

当一个类的职责过多,可以将类中不同的部分抽取出来分成两个类。再由一个父类进行管理。

处理接口职责过多也可以才去上述方式。


违反SRP原则的重构可采取设计模式: 外观模式、代理模式、数据访问对象。

这些设计模式在后期的文章中都会写到。


1、8 单一职责原则的注意点:

1)单一职责原则最难划分的就是职责。

2)单一职责原则剔除标准:用职责和变化原因来衡量接口或者类设计的是否优良,但是职责和变化原因都是不可度量的,因项目、环境而异。

3)接口一定要做到单一职责原则,类的设计尽量做到只有一个原因引起变化。



下面我们来谈谈单一职责原则的实际应用范围:

     在我们平常编写程序时,我们将程序分为多层,实体层、业务层、逻辑层、还有工具类、业务类等等。

   实体层:实体层只提供get、set方法,私有属性。 符合使用单一职责原则。

    业务层:因为业务层经常会变化,包含了很多种方法来操作各种属性和类。不符合单一职责原则。

     逻辑层: 单个逻辑层的类提供操作某一类型业务类的属性和状态的职责,尽量使用单一职责原则来明确区分开来,不然一旦职责扩散,可能会导致功能的紧耦合,降低代码复用能力和不利于代码的维护。

     工具类: 工具类提供一些与业务无关的数据与操作数据的方式,不依赖于业务类对象,不进行职责的扩展,为了工具类的扩展与松耦合,使用单一职责原则是非常合适的。

1、9 单一职责原则可以使用在创建型的设计模式。

至于具体的例子,在这里放上链接:单一职责原则



2、1里氏替换原则


2、2 里氏替换原则的严格表达是:

         如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。

       换而言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类和子对象的区别。


以下的实例违反了里氏替换原则:


public class A{
    public int func1(int a,int b){
       return a-b;
}

public class B extends A{
    public int func1(int a, int b){
       return a+b;
}
    public int func2(int a, int b){
       return func1(a,b)+100;
}

public class Client{
   public static void main(String[] args){
       B b = new B();
       System.out.println("100-50" + b.func1(100,50));
}
}

以上的伪代码输出结果为:

100-50=150

在这个例子中,因为子类覆盖了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,当父类替换子类时行为会发生改变。在实际编程中,我们常常会重写父类的方法来完成新功能,这样虽然简单,但是整个继承体系的可用性会变差,特别是多态比较频繁的时候,程序出错的几率大。


2、3 里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但是不能改变父类原有的功能,包含的含义是:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法形参)要比父类方法的输入参数要宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即)返回值要比父类严格。


当各个子类都有不同的实现时,最好在父类定义抽象方法,在子类具体实现。这样整个继承体系的可用性会比较好。

在JDK的类库中,HashMap就是一个违背了里氏替换原则的例子。HashMap重写了hashCode方法,replace,remove。在这里HashMap是最终实现类,所以违背里氏替换原则也没有关系。如果需要自己实现有不同功能的Map,最好是继承AbstractMap。



3、1 依赖倒置原则:

    定义:依赖倒置原则(Dependence Inversion Principle)就是要依赖于抽象,不要依赖具体。不要面向实现编程,而要面向接口编程。

下面介绍与之相关的概念:


依赖倒置原则(DIP):一种软件架构设计的原则(抽象概念)。

控制反转(IoC): 将设计好的对象交给容器控制,而不是传统地在你的对象内部直接控制。(DIP是一种软件设计思想)

依赖注入(DI):IoC的一种实现方式,用来反转依赖(Ioc的具体实现方式)。

IoC容器:依赖注入框架,用来映射依赖,管理对象创建和生命周期(DI框架)。


3、2 Bromon的blog上对IoC与DI浅显易懂的讲解

3、2、1  IoC(控制反转)

  首先想说说IoC(Inversion of Control,控制反转)。这是spring的核心,贯穿始终。所谓IoC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。这是什么意思呢,举个简单的例子,我们是如何找女朋友的?常见的情况是,我们到处去看哪里有长得漂亮身材又好的mm,然后打听她们的兴趣爱好、qq号、电话号、ip号、iq号………,想办法认识她们,投其所好送其所要,然后嘿嘿……这个过程是复杂深奥的,我们必须自己设计和面对每个环节。传统的程序开发也是如此,在一个对象中,如果要使用另外的对象,就必须得到它(自己new一个,或者从JNDI中查询一个),使用完之后还要将对象销毁(比如Connection等),对象始终会和其他的接口或类藕合起来。

  那么IoC是如何做的呢?有点像通过婚介找女朋友,在我和女朋友之间引入了一个第三者:婚姻介绍所。婚介管理了很多男男女女的资料,我可以向婚介提出一个列表,告诉它我想找个什么样的女朋友,比如长得像李嘉欣,身材像林熙雷,唱歌像周杰伦,速度像卡洛斯,技术像齐达内之类的,然后婚介就会按照我们的要求,提供一个mm,我们只需要去和她谈恋爱、结婚就行了。简单明了,如果婚介给我们的人选不符合要求,我们就会抛出异常。整个过程不再由我自己控制,而是有婚介这样一个类似容器的机构来控制。Spring所倡导的开发方式就是如此,所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。

3、2、2  DI(依赖注入)

  IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢? Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。


3、3 DIP的几种写法

接口声明依赖对象: 在接口的方法中声明依赖对象

构造函数传递依赖对象: 在类中通过构造函数声明依赖对象(Spring的构造器注入)

Spring的注入方式:

1、构造器注入

2、set注入

3、注解方式注入(使用反射来进行注入)




  3、4 依赖倒置原则与开闭原则的关系:

    1、开闭原则(OCP)是面向对象设计原则的基础也是整个设计的一个终极目标,而依赖倒置原则(DIP)则是实现OCP原则的一个基础。

    2、在面向对象软件设计的过程中不遵循依赖倒置原则是很难开发出符合开闭原则的软件。


  3、5 依赖倒置原则的含义:

  • 模块之间的依赖通过抽象类发生,实现类之间不发生直接的依赖关系
  • 接口或抽象类不依赖于实现类
  • 实现类依赖于接口或抽象类


高层模块不依赖于底层模块,而底层模块依赖于高层模块的接口(高层模块定义接口,底层模块负责实现)。

高层模块(接口):抽象  底层模块(实现接口):实现 ==>两者应该依赖于抽象,抽象(高层)不依赖实现(底层),实现(底层)依赖于抽象(高层)。



3、6 在使用DIP需要注意到一下几点:

1、继承自高层接口中的类要实现所有接口中的方法。

2、子类中除了接口的方法,在用接口声明的对象调用的地方是无法被调用到的,但是直接调用自诶是违背DIP的。

3、DIP是实现OCP的重要原则,一般违背了DIP很难不违背OCP。

4、LSP是实现DIP的基础,多态给实现DIP提供了可能。

5、每个类尽量都要有接口或抽象类,或者抽象类和接口都有:依赖倒置原则的基本要求,有抽象才能依赖倒置

6、变量的表面尽量是接口或者抽象类

7、任何类都不应该从具体类派生

8、尽量不要重写基类已经写好的方法(里氏替换原则)

9、结合里氏替换原则来使用:结合里氏替换原则和依赖倒置原则我们可以得出一个通俗的规则,接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。




4、接口隔离原则

         根据接口隔离原则拆分接口,必须首先满足单一职责原则。

     4、1 定义

       客户端不应该依赖它不需要的接口

       一个类对另一个类的依赖应该建立在最小的接口上


     4、2定义解读

  • 一个类对另一个类的依赖应该建立在最小的接口上
  • 一个接口代表一个角色,不应该将不同的角色都交给一个接口,因为这样会形成一个臃肿庞大的大接口。
  • 不应该强迫客户依赖它们从来不用的方法。

在单一职责原则中,一个接口可能有多个方法,提供给多个不同的调用者调用,但是它们始终完成同一种功能,因此它们符合单一职责原则,却不符合接口隔离原则,因为这个接口存在着多种角色,因此可以拆分成更多的子接口,以供不同的调用者所调用。 比如,项目中我们通常有一个Web服务管理类,接口定义中,我们可能会将所有模块的数据调用方法都在接口中进行定义,因为它们都完成同一种功能: 和服务器进行数据交互;但是对于具体的业务功能模块来说,其他模块的数据调用方法它们从来不会使用,因此不符合接口隔离原则。


4、3 为什么要使用接口隔离原则?

     在项目开发中,依赖几个专用的接口要比依赖一个综合的接口更灵活。通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。