简单总结面向对象设计原则之SOLID

面向对象设计原则

面向对象设计原则比较出名的便是SOLID,分别是SRP,OCP,LSP,ISP,DIP

其实设计模式最主要的便是想要在各个情况下使代码始终遵循上面的面向对象设计原则,因此在学习设计模式之前一定要清楚各个设计原则的重点,为什么要有这些设计原则?遵循这些设计原则有什么好处?这样才能在以后编写业务逻辑的时候,通过设计模式来达到遵循这些设计原则的目的。


单一职责原则(SRP)

单一职责原则(Single responsibility principle)是由Robert Cecil Martin 在《敏捷开发,原则,模式和实践》一书中的一篇名为<面向对象设计原则>所提出来的。—-维基百科

定义

单一职责原则,说简单一点 ,便是一个类,一个方法,只应该有一个改变的原因,如果你能够想到多于一个的动机去修改某个类,那么这个类便具有多于一个的职责。

比如在MVC开发的网站中,页面代码的变化不会引起业务逻辑代码的变化,因此MVC模式是符合单一职责原则的,如果将业务逻辑层和页面代码糅合在一起,那么可能某天需要修改一个按钮的样式的时候,却可能引起业务逻辑的bug。

为什么需要单一职责

保持一个类专注于单一功能单能够使得类更加健壮,更加容易维护和扩展。

在大话设计模式中,举了手机和专业相机的例子:手机集多个功能与一体,能听歌,打电话,拍照等,而专业相机却只能拍照,但是专业相机的拍照功能却比手机好的多。这就是单一职责带来的好处,因为专一,所以专业。

个人认为,引入单一职责原则,主要在一下几点:

  • 遵循单一职责的代码复杂度和耦合度低

    再说MVC中,因为MVC模式遵循单一职责,因此在视图层需要更换一整套样式的时候,业务逻辑却基本不用动,并且视图和业务逻辑分离开来,业务逻辑层可以由专门写业务逻辑层的人写,视图层可以由专门的视图的人写,互不影响

  • 遵循单一职责的代码容易阅读,并且可复用

    由于一个类/方法只做一件事,因此这个类很容易被复用,并且也不会给引入这个类的人带来不必要的功能

  • 遵循单一职责的代码容易扩展

    由于类、方法只做一件事,因此在扩展的时候不会“不小心”修改到其他的代码,不容易引起不必要的代码

单一职责应该注意的地方

理论很容易,但是实践比较难

  • 不必分离总是同时变化的职责

    如果在某个业务逻辑中,应用程序的变化总是会导致这两方便同时变化,那么就不必分离他们,如果分离开来,还有可能代码不必要的复杂度

  • 不要预先设计

    对比非主要功能,仅仅在真实发生变化的时候,再针对变化设计才具有实际意义。在没有提出真正的需求之前,过渡设计一些不会使用的变化,返回会引起不必要的复杂度。


开闭原则(OCP)

开闭原则(Open-closed principle)是由Bertrand Meyer在《面向对象软件构造》一书中提出 —-维基百科

定义

开闭原则认为,一个类的实现只应该因为错误而修改,新的或则改变的特性应该新建不同的类来实现。

说简单一点就是一个类在编写完后,就不应该去修改它,如果需要扩展功能,就应该新建其他的类来完成。

为什么需要开闭原则

开闭原则是我认为最重要的一个原则,几乎所有的设计模式都是为了达到开闭原则而设计。

正如平时的笑话所说,自己写的代码,一个月以后照样不认识。同样的道理,一个模块,如果某天突然需要增加一个功能,那还得捋一捋逻辑,看一看这块代码应该怎么写,原本的逻辑是怎么样的等等,要是忽略了一个地方,那么增加功能的同时,很可能就会带来bug。

这个时候,开闭原则的好处就体现了,符合开闭原则的代码,在进行扩展的时候,是不需要修改原本的代码的,这至少保证了在扩展 功能后,不会影响到原本的业务逻辑。

如何做到开闭原则

说起来比较简单,但是想做到代码符合开闭原则,还是比较难的

首先应该明白的一点:无论代码设计的多么完善,在面对各种各样的需求的时候,都可能存在一些无法封闭的变化。

因此,在设计某个类的时候,需要首先明确系统功能,找到系统的核心关键点,然后找到其容易发生变化的地方进行设计,然后使它遵循OCP原则。

比如:对于一个计算器来说,计算就是它的核心功能,因此对于计算功能,应该就是其容易变化的地方,比如刚开始只有加减乘除功能,后续可能会增加开方等。所以应该对功能这块代码进行OCP设计。

而对于如何做到OCP,最重要的便是抽象

比较经典来说,想要做到OCP,依赖倒转(DIP)是手段,里斯替换(LSP)是基础,在稍后所说的原则中,这两个原则便是需要做到OCP最重要的原则。

开闭原则需要注意的地方

开闭原则虽然比较好,但是想要写出符合开闭原则的代码需要一定的水平,并且如果过渡注重开闭原则,有可能带来的后果便是过渡设计,这样反而可能会增加系统的复杂度,因此最好便是明确需求,预测变化。


里斯替换原则(LSP)

里斯替换原则是由芭芭拉·利斯科夫(Barbara Liskov)在1987年在一次会议上名为“数据的抽象与层次”的演说中首先提出—-维基百科

什么是里斯替换原则

其实其实替换原则在Java中,是应用的最多的原则,在刚开始学习面向对象的时候,就明白一个概念叫多态,其实,多态便是里斯替换原则的一种体现。

说简单一点,里斯替换原则便是:派生子类必须能够替换掉他们的基类类型(父类)。

就好像在Java中

public static void test(Object o){
    System.out.println(o.hashCode());
}

这里,虽然说需要传入的类型是Object类型,但是我们知道如果某个类型继承自Object类型,那它一定包含hashCode()方法,因此,这里我们可以传入任意Object的子类。

为什么需要里斯替换原则

其实,只要接触过面向对象的原则的程序员,都使用过里斯替换原则,并且能够说出它的好处:可复用。

正因为子类能够替换基类,这样写出来的方法才能被复用。

里斯替换原则需要注意的地方

实现里斯替换原则比较简单,基本的语言的语法都遵循里斯替换原则,但是在编写业务逻辑的时候,有几点需要注意:

  • 每个子类所实现的方法对外来看,应该差不多。
    interface Add{
      int add(int a,int b);
    }
    public class AddImpl1{
      @Override
      pubic void int add(int a,int b){
         return a+b; 
      }
    }
    
    public class AddImpl2{
      @Override
      pubic void int add(int a,int b){
         return a-b; 
      }
    }
    

    可以看到,AddImpl1AddImpl2虽然都实现了add方法,在编译过程中完全正确,但是在逻辑上来看AddImpl2是不能替换AddImpl1的。

  • 里斯替换原则虽然好,但是在实现的时候,只有真正是is-a才能使用继承实现里斯替换,如果是has-a,则应该使用组合,可以参见另外一条设计原则:组合优先于继承原则


接口隔离原则(ISP)

接口隔离原则(interface-segregation principles)是指客户应该不依赖于它不使用的方法。接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。这种缩小的接口也被称为角色接口(role interfaces)—–维基百科

什么是接口隔离原则

说简单点,接口隔离原则便是 不应该强迫客户依赖于它们不用的方法。

也就是说如果一个客户端程序依赖一个比较庞大的类,那么从客户端的角度来说,无疑增加了客户端的使用难度

更重要的是,如果其他客户程序却确实要使用该客户不使用的方法,那么当其他客户要求这个类改变时,就很可能会影响到这个客户程序。

为什么需要接口隔离原则

因为接口具有强迫性,实现接口的类,都必须完整的实现接口中的所有方法,因此如果设计的接口中包含过多的接口,那会给所有实现该接口的类带来额外的负担

如何实现接口隔离原则

  • 可以使用委托分离接口,比如适配器模式。
  • 也可以是用多重继承分离开接口

通常优先使用第二种方法。

接口隔离原则注意事项

使用接口隔离原则的时候,需要主要分离接口的粒度,不能太大,也能不太小。并且在分离的时候需要考虑用户是如何使用该接口的。如果某两个方法用户需要同时使用,这个时候就比较麻烦。


依赖倒转原则(DIP)

依赖反转原则(Dependency inversion principle,DIP)由罗伯特·C·马丁提出,是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象—–维基百科

依赖倒转原则是什么

说简单点,依赖倒转原则主要有两点:

1) 高层模块不依赖于底层模块,两者都应该依赖于抽象

2)抽象不应该依赖于细节,细节应该依赖于抽象

什么意思呢,也就是传统我们的类中所包含的属性有可能是固定的:

public class Car{
    private String tyre="square";
}

比如我们创造的第一辆车,轮胎是正方形的。于是有了以上的代码,后来发现,圆形的轮胎更加省力,于是修改代码:

public class Car{
    private String tyre="roundness";
}

这里便就违背了开闭原则,若在后面,又发现了橡胶轮胎比木头轮胎更加省力,还得再修改代码。但是,如果我们的代码遵循依赖倒转原则:

public class Car{
    private Ityre tyre;

    public Car(Ityre tyre){
        this.tyre=tyre;
    }
}

将所依赖的属性作为一个参数传入,而所需要的属性是以接口形式依赖,这个时候,无论轮胎发展成什么样,Car这个类都不用修改。这便是依赖倒转原则。

熟悉Spring的同学应该都知道,Spring的一大特性之一便是方便实现依赖注入。

依赖倒置原则应用广泛,在面向对象程序框架设计中(是核心原则)、架构系统中、在社会活动构建组织等方面,都发挥重要作用。

总结


本文简单总结了面向对象原则中SOLID原则,而面向对象原则远远不止这五个,比如还有迪米特法则,合成复用原则等等。

这些原则虽然看似难以理解,但是我们只用明白,开闭原则 是最终的目的,因为只要不修改,就不会导致难以维护,而里斯替换原则 是做到开闭原则的基本,接口隔离,依赖注入,单一职责便是设计系统的建议。

这些原则都是前人经过不断的总结得来的,理解这些原则,能够更好的理解设计模式。因为设计模式便是前人通过总结,写出的在不同的情况下,利用其他原则,最终能够达到开闭原则,从而写出可扩展,可复用,灵活,易维护的特性的代码。