面向对象7大设计原则

2021/04/07 Java高级知识 共 3329 字,约 10 分钟
闷骚的程序员

面向对象思想支撑的架构设计体系中,逐渐的衍生出7大设计原则。

1. 开闭原则

1.1 什么是开闭原则?

对扩展开放,对修改关闭。
在程序需要扩展时,不能修改原有的逻辑代码,实现一个热插拔的效果。
目的就是使程序的扩展性好,易于维护和升级。

1.2 为何遵循开闭原则?

  1. 稳定性。开闭原则要求系统在扩展功能时不能修改原有代码逻辑,这样可以保证软件系统在运行过程中保持稳定。
  2. 扩展性。开闭原则要求系统必须对扩展开放,这样可以为软件系统提供新的功能,让软件系统具有灵活的可扩展性。

1.3 如何实现开闭原则?

  1. 将代码逻辑中始终保持不变的不分抽象成不变的接口。
  2. 一个接口的功能尽可以最小,不要太分散,后续需要扩展直接新增接口即可。
  3. 模块之间调用通过抽象接口进行即可,这样即便代码实现层发生了变化,对于外部调用方来说是无感知的。

2. 接口隔离原则

2.1 什么是接口隔离原则?

接口的设计应该遵循最小接口原则。
不要把多个功能模块的方法都抽象到同一个接口里,这样会导致功能模块和接口的耦合度太高。
接口中不要定义在一个功能块不使用的方法。如果一个接口中存在没有被某一个功能使用到的方法,则说明该接口过胖,应该立即将接口分隔为几个功能单一的接口。

2.2 为何遵循接口隔离原则?

说白了就是避免接口之间的依赖。
如果多个功能共享同一个接口设计,那么当其中一个功能需要修改接口时,其他依赖该接口的功能都将受到影响。这显然违反开闭原则。
尽量细化接口,接口中抽象的方法尽可能少,接口的功能尽量单一。
接口定义一定不要臃肿!
同样,一个接口b继承了接口a,那么接口b就继承了接口a中的所有方法。此时,接口b也应该遵循接口隔离原则。
否则,则说明接口a被接口b污染了。

2.3 如何实现接口隔离原则?

接口隔离,说白了就是一个接口层面的解耦工作,降低接口的类之间的依赖关系。
只需要保证:
一个接口的设计功能尽量专一。
方法尽可能少。

3. 迪米特原则

3.1 什么是迪米特原则?

一个实体,应当尽可能少的和其他实体发生相互作用。

3.2 为何遵循迪米特原则?

迪米特原则目的也是为了降低实体类之间的耦合度。
但是在实际中,实际上并不是一味的遵循迪米特原则的。
因为迪米特原则要求类和类之间不能存在任何直接耦合关系,如果非要存在,也必须建立一个中间类来表达这种类和类之间的依赖关系。
这样依赖,会导致实体类中存在大量的中间类,系统因此会变得复杂。

3.3 如何实现迪米特原则?

实体类之间尽量不要直接耦合;
非要耦合,借助一个中间类来表达这种耦合/依赖/委托关系。

4. 单一职责原则

4.1 什么是单一职责原则?

应该有且只有一个原因引起类的变更。
换句话说,一个类只能实现一个职责。
如果一个类有多个职责,那么就会有多个原因引起该类的变更。

4.2 为何遵循单一职责原则?

  1. 单一职责原则可以使类的复杂度降低,一个类实现什么职责都有明确的定义。
  2. 类的可读性提高,复杂度江都,代码更容易维护。
  3. 变更引起的风险更低。

4.3 如何实现单一职责原则?

  1. 单一职责的实现,在实现类时,类的功能必须单一。
  2. 实际上单一职责更多的是方法的实现,谋求方法的功能单一,即一个方法只应该做一件事。

5. 里氏替换原则

5.1 什么是里氏替换原则?

在代码使用中,任何使用父类的地方,都可以被子类无条件的替换,并且功能不发生任何异常。
直白点说,里氏替换原则为我们在子类继承父类方面提供了判断标准。如果一个子类继承了父类不满足里氏替换原则,我们应该考虑废除该子类和父类的继承关系。

5.2 为何遵循里氏替换原则?

  1. 加强程序的健壮性,降低变更引入的风险。
  2. 对继承进行约束,是对开闭原则的一种体现和补充。

5.3 如何实现里氏替换原则?

  1. 子类可以实现父类中的抽象方法,但是不能覆盖父类中的非抽象方法。
    假如有父类 superClass,其中有一个非抽象方法eat(),表示父类在吃米饭;此时有一个子类subClass继承了superClass,覆盖了eat(),覆盖后的方法表示子类在吃大便。
    这已经完全违背了里氏替换原则,因为此时使用父类的地方不能无条件的使用子类替换了。
    public class SuperClass{
    public void eat(){
        System.out.println("superClass在吃饭);
    }
    }
    
    public class SubClass extends SuperClass{
    @Override
    public void eat(){
        System.out.println("subClass在吃屎);
    }
    }
    
  2. 当我们迫不得已需要在子类中添加一个跟父类方法名称相同的方法时,子类方法的形参必须比父类方法的形参范围大(这种情况不牵扯覆盖)。
    注意:无论是子类方法的形参范围比父类的大、还是比父类的小,只要子类的形参和父类不同,就不是覆盖。
    如果下面,testList方法中,在父类和子类中参数类型都是List的话,那么此时就形成了方法的覆盖;此时这种覆盖也违反了里氏替换原则,因为里氏替换原则不允许覆盖父类中已实现好的方法。
    实际上,子类的方法形参必须比父类的方法形参范围大,目的只有一个,就是保证在使用父类对象的地方能够完全替换为子类对象,即保证替换后使用的也是父类中的方法而不是子类中定义的方法。
    如果一旦子类的方法参数范围小于或者等于父类中的方法参数,则此时使用父类的地方被替换为子类对象时,使用的将是子类中定义的方法。
    public class SuperClass{
    public void testList(ArrayList list){
        System.out.println("superClass list is :" + list);
    }
    }
    
    public class SubClass extends SuperClass{
    public void testList(List list){
        System.out.println("subClass list is :" + list);
    }
    }
    

6. 依赖倒转原则

6.1 什么是依赖倒转原则?

依赖倒转原则是开闭原则的基础。
具体内容为:针对接口编程,而不是针对具体实现编程;依赖于高层抽象,而不是依赖于低层实现。
ps:依赖倒转?
因为面向过程的结构化开发中,总是高层模块直接依赖底层模块;而在面向对象中,这种依赖被倒转了,即低层模块之间通过高层模块发生依赖关系。
因此叫做依赖倒转。

6.2 为何遵循依赖倒转原则?

java中的抽象指的就是抽象类或者接口,两者都不能直接被实例化;而细节就是实现类,实现接口或者继承抽象类的过程就是细节,细节是可以直接通过new关键字去实例化的。
所有,高层模块就是抽象层;低层模块就是具体实现类。
如果高层模块总是直接依赖低层模块,或者低层模块之间直接依赖(表现为具体实现类和实现类之间产生依赖关系),这样将会将具体的细节耦合起来,导致耦合度变高,一旦细节发生更改,则所有依赖的高层都需要进行相关变动。

6.3 如何实现依赖倒转原则?

依赖倒置原则在java中的表现就是:

  1. 模块间的依赖通过抽象发生,实现类之间不直接发生依赖关系,依赖关系就是通过接口或者抽象类产生的。
    说白了就是两个模块之间产生依赖关系,都应该直接通过彼此的高层模块即抽象类或者对应的接口去发生相关依赖关系,而不是直接通过具体的实现类去产生这种依赖关系。
  2. 针对接口编程,而不是针对实现编程。
  3. 如果抽象非要和细节产生依赖关系,那么,抽象不应该依赖于细节,应该是细节依赖于抽象。
    总结:所有的依赖关系都应该直接和抽象产生,绝不允许直接和具体的实现细节即class发生依赖关系。

7. 组合/聚合复用原则

7.1 什么是组合/聚合复用原则?

在增强对象功能时,尽量通过组合/聚合,而不要使用继承。

7.2 为何遵循组合/聚合复用原则?

继承可以达到增强一个对象功能的目的,但是继承是垂直扩展,会造成继承树庞大冗余沉重,后期特别不好维护;并且在继承关系中,父类的内部实现细节对于子类是完全继承暴露的,一旦父类改变,子类就必须改变。
而聚合是通过水平扩展增强一个对象的功能,通过委托/依赖一个现有的对象,对现有的对象进行水平组装,来复用某些功能,这样便增强了当前对象的功能,并且对象依赖之间不暴露具体的实现细节。

7.3 如何实现组合/聚合复用原则?

在一个类中直接委托另外一个接口或者抽象类的对象作为成员,达到当前对象功能扩展的目的。

文档信息

Search

    Table of Contents