## 0、设计模式之六大原则
如果想要很好地了解IOC的思想,那么我觉得了解设计模式是必不可少的。今天现在这里讨论一下设计模式的六大原则。
> 参考: [设计模式之六大原则(转载)](https://www.cnblogs.com/dolphin0520/p/3919839.html)
### 单一职责原则 Single Responsibility Principle SRP
一个类所负担的方法不能太多。一个类的职责越多,他被复用的可能性就越小。**并且一个类的职责过多,就相当于把职责耦合到一起。** 当一个职责变化时,可能会影响其他的职责的运作。
因此,把职责分离,**其实也是一种解耦。**
==单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则。== 需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
内聚性:又称块内联系。指模块的功能强度的度量,**即一个模块内部各个元素彼此结合的紧密程度的度量。** 若一个模块内各元素(语名之间、程序段之间)联系的越紧密,则它的内聚性就越高。
耦合性:也称块间联系。指软件系统结构中各模块间相互联系紧密程度的一种度量。**模块之间联系越紧密,其耦合性就越强,模块的独立性则越差。** 模块间耦合高低取决于模块间接口的复杂性、调用的方式及传递的信息
### 开闭原则 Open-Closed Principle OCP
**一个软件实体应当对==扩展开放,对修改关闭==。即软件实体应尽量在不修改原有代码的情况下进行扩展。**
这条规则限定了我们应该在最大程度上限定已经开发好的类的修改。在考虑扩展功能的时候,首先要考虑的通过扩展原来的类,编写新的类去增加新的功能,而不是考虑去类的内部修改代码
软体都会面临一个问题就是,他们的需求会随着时间的推移发生变化。当软件系统面对新的需求的时候,我们**应该尽量保证系统的设计框架是稳定的**(既有的代码结构和内容是稳定的)。
当一个软体符合OCP,那么他就会拥有良好的适应性和灵活性,同时具备较好的稳定性和延续性。
#### 如何实现OCP?
**对系统进行抽象化设计**,是开闭原则的关键所在。可以为系统定义一个相对稳定的抽象层(如抽象类、接口等),而将不同的实现行为移至具体的实现层完成。
当我们需要扩展业务功能时,无需对抽象层进行任何改动,只需要增加新的具体类来实现业务功能即可,而不需要对已有的代码进行改动。
### 里氏代换原则 Liskov Substitution Principle LSP
**所有引用基类(父类)的地方必须能够透明地使用其子类的对象,反之则不成立**
**通俗来讲,就是子类可以扩展父类的功能,但不能改变父类原有的功能**
在Java里编译器会检查子类向父类的因式转换(向上升级),所以Java实现了一部分的里氏代换原则
**在软件中将一个基类对象替换成他的子类对象,程序将不会产生任何错误和异常。反过来则不成立。**
这一句话隐含了一个意思,就是在继承的时候遵循里氏代换原则。**除了添加新的方法完成新的业务之外,不要重写或者重载父类的方法。**
意思就是,**在父类中凡是实现好的方法,实际上是设定一系列的规范和规约。** 虽然它不强制要求所有子类必须遵循这些规约,但是如果子类对这些非抽象的方法任意修改,就会对整个继承体系造成破坏。里氏代换原则就是表达了这个含义
如果子类重写或者重载了父类的方法,那么就==做不到==LSP中所说的引用基类的地方==能够透明使用==其子类,因为父类的方法已经被修改,规约被破坏,所以在使用父类的地方如果使用子类就有可能造成因为方法的实现过程不同而导致表现的不同。
### 依赖倒转原则 Dependency Inversion Principle DIP
依赖倒转就是OOP的主要实现机制之一
**DIP要求我们在代码中传递参数时或者在关联关系中,尽量引用层次高的抽象类,即使用接口和抽象类进行各种各样的声明,而不要用具体类来做这些事情。**
**抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。**
既然是依赖倒转,那么我们应该先弄明白原来的依赖是怎么样的。
> 引用: [轻松学,浅析依赖倒置(DIP)、控制反转(IOC)和依赖注入(DI)](https://blog.csdn.net/briblue/article/details/75093382)
```java
public class Person {
private Bike mBike; //具体实现类
public Person() {
mBike = new Bike();
}
public void chumen() {
System.out.println("出门了");
mBike.drive();
}
}
```
在这里是个典型的依赖。在我们需要某个类的时候,会在类的内部主动创建一个对象去依赖他。这个**主动的创建其实就是一个高耦合的表现**。
那么有没有方法能够使需求在变动的时候(例如我想使用飞机或者火车出行,甚至步行),能够不更改Person类内部的代码?
首先我们看到,上面人的出行能力依赖于Bike的能力。这其实就是高层依赖低层的典型例子。也就是说,我们这么编程其实是面向实现编程的(事实上我们也没有引进任何的抽象)。这样造成的结果就是**Person的泛化性不足,在出现需求变动的时候无法很好地适应。**
那么如果针对抽象编程呢?
```java
public class Person {
private Driveable mDriveable; //是接口,不是具体实现类
public Person() {
mDriveable = new Train();
}
public void chumen() {
System.out.println("出门了");
mDriveable.drive();
}
}
```
现在的Person的出门方法依赖于Driveable接口的抽象,他没有限定自己出门的可能性,任何继承并实现了Driveable的实现类都是可以的。
在这里实现了细节依赖于抽象。就是人出行的这个细节,依赖于Driveable这个抽象接口。那么什么叫抽象依赖于细节呢?
我的认为是,当具体的实现类设计出来之后,再将他的方法抽象出来,这是抽象依赖于抽象。抽象出来的接口并不具有很强的泛化性。而先抽象出接口再去进行具体实现,这个抽象就会具有比较强的泛化性。
那么这个依赖倒转的倒转是什么意思呢?
原作者说到,这个倒转其实是改变依赖关系。高层模块不应该依赖于底层模块,而是两者都依赖于抽象。
那么这里倒转在哪里呢?
其实是这样的。**这个接口的产生是来自于上层类对他的需求的一个抽象。**
在我们刚刚的例子里面,我们的人需要出行。人把出行这个行为抽象成接口,然后底层去实现。
这里的倒转的具体含义就出来了————不是高层依赖于底层的实现,而是高层抽象自己的需求,两者共同依赖于抽象(这个抽象其实也是属于高层的)
那么这里引入我们的大BOSS,我一直搞不懂的——控制翻转IOC与依赖注入,就恰到好处了
#### 1、控制反转IOC和依赖注入DI————OCP和DIP的具体实现
刚刚我们在上面实现了依赖转置,解决了软体泛化性不强的问题。但是因为实现类仍然是在类内部创建的,一旦我对人出行的需求需要更改,比如我要把出行方式从火车改成飞机,那么仍然要进到类的内部更改代码,不符合开闭原则OCP。
而控制翻转就是来解决违反OCP的情况的。控制反转是一种思想,一个重要的面向对象变成的法则。他能够让我们设计出松耦合,更加优良的程序。
与DIP不同的地方时,DIP强调对于传统的、源于面向过程设计软体思想的层次概念的“倒置”,而IOC则是强调对程序流程控制权的反转。IOC强调将传统设计上在类内创建的对象交由调用我这个类的外部来创建,然后通过依赖注入的方式放到当前类的内部,最终实现对依赖类的使用。
在传统的应用程序中,在我们需要一个类的时候,我们都是在现在的**这个类的内部主动地去创建我们依赖的对象,** 从而导致对象之间的高耦合。
再举个栗子叭
某天,公司领导找到开发人员,说要开发一个微信支付宝的收款明细获取功能。
案例精简:把任务指派给开发人员完成。本句话中,有两个名词:“任务”和“开发人员”,所以我们考虑设计两个对象(任务和开发人员)。
开发人员对象:
```java
package DependencyInjectionDemo;
public class Javaer {
private String name;
public Javaer(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
public void WriteCode() {
System.out.println(this.name + " writting java code...");
}
}
```
任务对象:
```java
package DependencyInjectionDemo;
public class NewTask {
private String name;
private Javaer javaer;
public NewTask(String name) {
this.name = name;
this.javaer = new Javaer("张三"); //主动创建要依赖的对象
}
public void Start() {
System.out.println(this.name + " started ..");
this.javaer.WriteCode();
}
}
```
场景类
```java
package DependencyInjectionDemo;
public class DependencyInjectionDemo {
public static void main(String[] args) {
NewTask task = new NewTask("开发微信支付宝收款明细获取工具");
task.Start();
}
}
```
上面这是一个很典型的Java SE设计。我们来看看他有什么问题
+ 当如果我们需要的开发人员不是张三了,而是李四了,那么我们就要进到代码内部去修改代码
+ 如果有人需要复用我们的实现,我们不能直接打包成jar直接给他们用,因为他不能从jar文件外部修改任务和开发人员。
上面两点造成的结果就是,复用性不强,鲁棒性不强。一旦当需求出现了更改(例如需要更换任务或者更换开发人员或者更换人员种类)那么这个代码就没法用了
---
那么怎么样解决上面的问题呢?
这里就是依赖注入出场的时候了。我们不要让依赖的类在类的内部主动生成,而是通过**构造器或者Setter或者接口让外部环境去给他一个类**,这就是控制反转的核心:需要的时候不要再类的内部主动创建其他的类,而是在类的外部创建类,然后由外部通过方法注入类内,最终实现依赖。**让主动创建变成被动接受依赖。** 同时通过这种方法也实现了解耦。看下面的例子
任务对象;
```java
package DependencyInjectionDemo;
public class NewTask {
private String name;
private Javaer javaer;
public NewTask(String name) {
this.name = name;
//this.javaer = new Javaer("张三");
}
public void SetJavaer(Javaer javaer) {
this.Javaer = javaer;
}
public void Start() {
System.out.println(this.name + " started ..");
this.javaer.WriteCode();
}
}
```
场景类
```java
package DependencyInjectionDemo;
public class DependencyInjectionDemo {
public static void main(String[] args) {
NewTask task = new NewTask("开发微信支付宝收款明细获取工具");
task.SetJavaer(new Javaer("张三")); //构造方法中进行依赖注入
task.Start();
}
}
```
上面的例子就是一个基于依赖注入DI的控制反转IOC。
在这里高层不再依赖于底层了,因为在类内没有代码显式依赖低层。
上层对下层类的控制体现在哪呢?主要体现在上层现在可以决定从什么地方进行注入。依赖注入有三种,分别是构造器注入(要求注入的类的生命周期较长),Setter注入和接口注入。
同时,对反转有另外一种的理解就是,现在类不是主动创建依赖的类了,而是交由外部创建,然后我被动地接受我需要依赖的类。
基于这么一个思想,一个专门分析和创建类的实例、管理类的生命周期的框架就显得很必要了。那么,这样的一个框架就叫做IOC容器。
这里,场景类也作为一个IOC容器,负责创建类并且注入不同的类。
在Spring里,是采用反射的方式根据注解或者xml配置文件来生成类的。这是对工厂模式的一种升华,采用反射去生成类与工厂模式相比,在更改或者扩展业务的时候,不需要进到工厂类中更改相应判断不同类或者接口的代码,这进一步保证了OCP。
可以参考下面的一些文献便于理解:
> [理解依赖注入和控制反转](https://www.cnblogs.com/zanpen2000/p/7810884.html)
> [Spring IoC有什么好处呢?](https://www.zhihu.com/question/23277575)
### 接口隔离原则 Interface Segregation Principle ISP
**使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。**
每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
这里的接口有两种不同的含义:
1. 一类所具有的方法的特征的集合,仅仅是逻辑上的一种抽象。
2. 语言中具体的接口(Interface)的定义,有严格的定义和结构。
ISP对不同的含义的定义也不同:
1. 对接口的划分直接带来类型的划分。可以把接口理解成角色,一个接口代表一个角色,每个角色都有和他直接相关的接口。这里ISP也可以叫做“角色隔离原则”
2. 接口仅仅提供客户想要的行为(例如上面DIP所定义的下层接口),客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的、单独的接口,而不要提供大的接口。
接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或业务逻辑类)所需的方法即可,这种机制也称为“定制服务”,即为不同的客户端提供宽窄不同的接口。
具体地,如果在java设计中接口过大,在扩展的过程中发现我不需要的但是又在接口中定义了的方法。我这个类继承了接口,就一定要实现这些方法。**所以最终可能会导致我对我不需要的方法定义了空的代码段,造成代码上的冗余。**
同时,在设计接口的时候还是要控制接口的粒度。太大在扩展的时候可能会带来代码的冗余,接口太小会造成接口的泛滥,难以维护。
### 迪米特法则 Law Of Demetter LOD
**一个软件实体应当尽可能少地与其他实体发生相互作用。**
迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度。
图灵杯中设计的事件驱动模式,是符合OCP、LOD、ISP的。
## 2、面向切面编程 AOP
spring框架中,显著的特点除了IOC、DI之外,就是AOP了。那么什么是AOP呢?
AOP,面向切面编程,是Spring框架中一个重要的内容,是函数式编程的一种衍生泛型。
AOP利用一种“横切”的技术,能够剖开封装的对象内部,并且将那些影响了多个类并且和具体业务无关的公共行为,封装成一个独立的模块
例如之前在SpringData内使用的审计模块,自动为数据库实体类添加创造和更改时间。
又比如很多的权限审核、日志记录、性能统计。安全控制、事务处理、异常处理等等,这些很多类需要用到的,但是实际上和核心业务逻辑没有什么关系的代码,都可以通过AOP横切出来,单独组织成切面类。
更重要的是,他又能巧妙地将这些剖开的切面复原,不留痕迹地融入核心业务逻辑当中。
AOP技术的实现,无非就是通过动态代理技术、或者在程序编译期间进行静态地“织入”方式。
### AOP和OOP的区别?
OOP:针对业务处理过程中的实体及其属性和行为进行抽象封装,以获得更加清晰、高效的逻辑单元划分。
AOP:针对业务处理过程中的某个具体过程——切面进行提取,他面对的是业务逻辑中的步骤或者片段,提取出公共的步骤和片段,从而达到降低各逻辑之间降低耦合的隔离效果。
对于“雇员”这样业务实体的封装,自然是OOP的任务,AOP的设计思想对“雇员”的封装无从谈起。
而对于“权限检查”这样的动作片段进行划分,则是AOP的目标领域。使用OOP对一个动作进行封装,显得有点不伦不类。
<br />
<br />
<br />
<br />
<div align="right">
Chen Sicong
搬运时间:2019年8月4日 11:27:13
</div>
【学习】设计模式六大原则、控制翻转、依赖注入、AOP