程序设计几大原则

一、单一职责原则(SRP)

单一职责原则(SRP)用于指导我们,在对功能划分到具体的类中的时候,要保证具有高内聚性。对于SRP的一个很好的描述是:就一个类而言,应该仅有一个引起它变化的原因

想要使用好SRP,一个首先要搞清楚的问题是:什么是职责?每一个职责都是变化的一个轴线,职责被定义为"变化的原因"。

这里对于职责的定义个人感觉比较清楚明确。我们要知道,SRP中的职责并不是一个类中的函数,而是一个变化的轴线,具体到不同层次的类,其职责有大有小。如对于一个Activity来说,其负责的是展示一个完整的界面,那么界面的内容获取势必从他里面发起,界面的数据处理势必在其里面完成,一个Activity里面做的事情,调用的方法有很多,但是站在其层次的角度来考虑,一个Activity的职责就是负责好他所对应的页面,而其他页面的事情对它来说就是多余的职责。

即SRP告诉我们,当一个类中某几个功能常常衍生出新的场景、新的实现方式时,那么应该考虑将他们各自进行独立的职责封装,而不是在当前类中不断的改来改去、加来加去来使得当前类变得臃肿、难以维护。

1、一个例子

举一个例子,假设需要装一台电脑,以下是配置:

public class Computer {
    private String CPU() {
        return "最贵的最好的CPU";
    } 
    private String board(){
        return "最实惠的、型号对应的主板";   
    }
    
    private void build() {
        String cpu = CPU();
        String board = board();
        return "电脑配置为"+cpu+board;
    }
}

当前来看这个类很单纯,就是组装一台电脑嘛,完全可以胜任,目前的方案是在CPU上花钱,主板挑一个过得去的就行;但是,不同的人需求不一样,有的人追求够用即可在CPU上也追求实惠,有的人土豪一个,在主板上也追求最贵最好。这样的需求变动不得不使我们在Computer类中添加对应的方法来满足以上需求,这样就导致了Computer类的臃肿和难以维护。

上述的问题在于,在可预见的未来,用户选择CPU的方案和选择board的方案都会产生很多变化,即Computer类变化的轴线有两个:CPU和board,这违反了SRP,所以方案就是将CPU的选择方案封装成一个单独的类CPU,将board的选择方案封装成一个单独的类Board。同时,这个例子也体现了单一职责也是分层次的,Computer的职责是负责组装电脑,CPU的职责是负责确定哪款CPU,Board的职责是负责确定哪款主板。

另外,在上述问题中CPU的选择方案与主板之间还存在一种约束关系(型号对不上),因此,如果不将职责进行拆分的话,build方法还可能因为耦合关系,出现运行错误的情况,而这种情况当代码量很大时是很难察觉的。如果进行了职责的拆分,Computer类只负责CPU和board方案的匹配校验工作,那么这种错误发生的概率就会降低很多。

2. 小结

通过上述例子我们可以粗略大胆的认为当一下情况出现的时候,你的类违背了SRP,并且到了需要拆分的时候:

  1. 当前类包含了多个子功能的具体实现;
  2. 在应用场景中,上述子功能分别会有多种可能的实现方案。

二、开放封闭原则(OCP)

开闭原则(OCP):软件实体应该是可扩展的,但是不可修改的。

按照OCP设计出的模块具有两个特征:

  1. 对扩展开放:模块的行为是可以扩展的;当应用的需求改变时,可以对模块进行扩展来满足新的行为。
  2. 对更改封闭:对模块进行扩展时,不必改变模块的源码。

上面的两个特征看起来有些矛盾,不改变源码怎么进行扩展?实际上OCP要求我们使用抽象来定义行为,使用具体类来实现行为,扩展也就是说使用新的具体类来扩展行为;封闭也就是说在使用该行为时使用抽象类对象来囊括不同具体行为,这样一来就不用了更改源码了(实际上由于多了个具体类,那么最起码初始化该具体类对象的地方还是相当于修改了源码,这是没法避免的)。

同时,还有一个重点在于,在使用OCP来定义行为的时候,一定要选择程序中呈现频繁变化的那些部分进行抽象;如若不然,那么滥用OCP也会带来很高的维护成本

小结

通过上面的说明,我们对OCP可以有一个大概的认知,即具有如下结构的应用:

  1. 定义类时采用抽象类,实例化时采用具体类,即所谓的左类型与右类型。

三、里式替换原则(LSP)

上述的OCP核心思想在于:利用抽象来定义行为,利用具体类来实现和实施不同种的行为。但是,在面向对象开发中还有另一种形式的继承机制,即父类并不是抽象类,在一些情况下父类可以胜任很多工作,但有时候需要子类对象来扩展一些工作,而不得不实例化子类对象,这时候就需要要求在实例化子类对象时,他要能够完成其父类角色的任务。

即:在大多数情况下Parent a = new Parent();a.fun1();即可完成工作,但有时候需要使用到子类:Parent b = new Child();b.fun1();b.fun2();才能够完成工作。那么就需要子类的fun1()方法能够像父类的该方法一样承担应有的工作。

LSP的解释如下:子类型必须能够替换掉他们的基类型。并且在替换掉基类型之后,程序依然能够运行。

1. 一个例子

假设父类的sort函数可以实现对数组的升序排序:

class Parent {
    int[] array;
    public Parent(int[] a) {
        this.array = a;
    }
    
    public void sort() {
        实现对array的升序排序
        ...
    }
}

class Child {
    ...
    @override  
    public void sort(){
        实现对array的降序排序
    }
}

很显然,上述的子类虽然是继承了父类,并且重写了父类的sort方法,但是将全局的所有父类对象的实现变为子类对象,那么程序肯定会出问题。这就是LSP所约束的问题。

2. 小结

LSP和OCP看起来都是在说继承的问题,但是他们所关注的场景不同,而且可以看出OCP显然是遵从了LSP的,因为OCP的背景为不同子类去扩展抽象类所定义的行为的。

而LSP则告诉我们,子类在覆盖父类的方法的时候不能够任意实现,而是要遵循父类对该方法的期望与要求。为此,有人提出了一种契约设计的方式:

  1. 契约是通过为每一个方法声明的前置条件和后置条件来指定的;
  2. 要使一个方法得以执行,前置条件必须要为真;执行完毕后,后置条件必须为真;
  3. 派生类的前置条件和后置条件的规则为:在重新声明派生类中的例程时,只能使用相等或更弱的前置条件来替换基类的前置条件;只能使用相等或更强的后置条件来替换基类的后置条件。

四、依赖倒置原则(DIP)

开门见山,该原则的解释为:

  1. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象;
  2. 抽象不应该依赖于细节,细节应该依赖于抽象。

如果高层模块依赖于低层模块,那么在不同的上下文中重用高层模块会变得非常困难。

首先要明确一点的是,既然是高层模块,那么其势必要使用低层模块提供的功能或者说服务,如何做到高层不依赖于低层呢?答案就在后半句,抽象,不过要加上一种要求:抽象由高层模块来定义,低层模块去实现它。这样一来,高层模块就可以通过抽象类对象来使用想要的服务,而不必理会低层模块是如何实现它的,从而避免了对低层模块的依赖。这也体现了第二个要求,是细节依赖抽象而不是抽象依赖细节,即高层模块一旦定义了想要的服务,就不必理会低层模块的具体实现方式,即这种服务一定要抽象到不用去理会实现细节。

很明显,一个典型的例子就是计算机网络协议的设计,高层协议通过服务访问点来使用下层协议所提供的服务,而不必理会下层协议是何种协议。例如TCP是传输层协议,其只需要下层能够将数据传输到端即可,而不关心你是IPv4还是IPv6.

五、接口隔离原则(ISP)

接口隔离原则很简单:

不应该强迫客户依赖于他们不用的方法

因为一旦这些方法发生变化,他们不得不做出相应的改变。

很显然,出现上述情况时,应该对接口进行拆分了。

你可能感兴趣的:(程序设计几大原则)